Repository navigation
Expand file tree
/
Copy pathAdd-DbaAgDatabase.ps1
More file actions
751 lines (655 loc) 路 50.3 KB
/
Copy pathAdd-DbaAgDatabase.ps1
File metadata and controls
751 lines (655 loc) 路 50.3 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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
function Add-DbaAgDatabase {
<#
.SYNOPSIS
Adds databases to an Availability Group with automated backup, restore, and synchronization handling.
.DESCRIPTION
Adds databases to an Availability Group and handles the complete process from backup through synchronization. This command eliminates the manual steps typically required when expanding Availability Groups with new databases, automatically managing seeding modes, backup/restore operations, and replica synchronization.
The command executes a comprehensive five-step process for each database:
* Step 1: Setting seeding mode if needed.
- If -SeedingMode is used and the current seeding mode of the replica is not in the desired mode, the seeding mode of the replica is changed.
- The seeding mode will not be changed back but stay in this mode.
- If the seeding mode is changed to Automatic, the necessary rights to create databases will be granted.
* Step 2: Running backup and restore if needed.
- Action is only taken for replicas with a desired seeding mode of Manual and where the database does not yet exist.
- If -UseLastBackup is used, the restore will be performed based on the backup history of the database.
- Otherwise a full and log backup will be taken at the primary and those will be restored at the replica using the same folder structure.
* Step 3: Add the database to the Availability Group on the primary replica.
- This step is skipped, if the database is already part of the Availability Group.
* Step 4: Add the database to the Availability Group on the secondary replicas.
- This step is skipped for those replicas, where the database is already joined to the Availability Group.
* Step 5: Wait for the database to finish joining the Availability Group on the secondary replicas.
Use Test-DbaAvailabilityGroup with -AddDatabase to test if all prerequisites are met before running this command.
For custom backup and restore requirements, perform those operations with Backup-DbaDatabase and Restore-DbaDatabase in advance, ensuring the last log backup has been restored before running Add-DbaAgDatabase.
.PARAMETER SqlInstance
The primary replica of the Availability Group. Server version must be SQL Server version 2012 or higher.
.PARAMETER SqlCredential
Login to the target instance using alternative credentials. Accepts PowerShell credentials (Get-Credential).
Windows Authentication, SQL Server Authentication, Active Directory - Password, and Active Directory - Integrated are all supported.
For MFA support, please use Connect-DbaInstance.
.PARAMETER Database
Specifies which databases to add to the Availability Group. Accepts single database names, arrays, or wildcard patterns.
Use this when you need to add specific databases rather than piping database objects from Get-DbaDatabase.
.PARAMETER AvailabilityGroup
Specifies the target Availability Group name where databases will be added. The AG must already exist and be configured.
Use this to identify which existing Availability Group should receive the new database members.
.PARAMETER Secondary
Specifies secondary replica instances to target for database addition. Auto-discovered if not specified.
Use this when replicas use non-standard ports or when you want to limit the operation to specific secondary replicas rather than all replicas in the AG.
.PARAMETER SecondarySqlCredential
Authentication credentials for connecting to secondary replica instances when they require different credentials than the primary.
Use this when secondary replicas are in different domains, use SQL authentication, or require service accounts with specific permissions for backup/restore operations.
.PARAMETER InputObject
Accepts database objects from pipeline input, typically from Get-DbaDatabase or Get-DbaDbSharePoint.
Use this for workflow scenarios where you want to filter databases first, then pipe the results directly into the AG addition process.
.PARAMETER SeedingMode
Controls how database data is transferred to secondary replicas during AG addition. Valid values are 'Automatic' or 'Manual'.
Automatic seeding transfers data directly over the network without requiring backup/restore operations, but needs sufficient network bandwidth and proper endpoint configuration.
Manual seeding uses traditional backup/restore through shared storage, giving you more control over timing and storage location but requiring accessible file shares.
.PARAMETER SharedPath
Specifies the UNC network path where backups are stored during manual seeding operations. Required when using Manual seeding mode.
All SQL Server service accounts from primary and secondary replicas must have read/write access to this location. Backup files remain on the share after completion for potential reuse or cleanup.
.PARAMETER UseLastBackup
Uses existing backup history instead of creating new backups for manual seeding. The most recent log backup must be newer than the most recent full backup.
Use this when you have recent backups available and want to avoid taking additional backups, reducing backup storage requirements and time.
.PARAMETER AdvancedBackupParams
Passes additional parameters to Backup-DbaDatabase as a hashtable when creating backups during manual seeding.
Use this to control backup compression, file count, or other backup-specific settings like @{CompressBackup=$true; FileCount=4} for faster backup operations.
.PARAMETER NoWait
Skips waiting for the database seeding and synchronization to complete on secondary replicas (Step 5).
The underlying SQL command ALTER AVAILABILITY GROUP ... ADD DATABASE is immediate and does not wait for seeding to finish.
Use this when you want the command to return immediately after adding the database to the AG, allowing seeding to continue in the background.
This is particularly useful in deployments where seeding can take a long time and you want to start using the environment before synchronization completes.
.PARAMETER SkipReuseSourceFolderStructure
Prevents restores from using the source server's folder structure when restoring databases to secondary replicas.
When enabled, Restore-DbaDatabase uses the replica's default data and log directories instead of attempting to replicate the primary's folder structure.
This is automatically set to true when the primary and replica servers run on different operating system platforms (e.g., Windows primary with Linux replica).
.PARAMETER MasterKeySecurePassword
Password for creating or opening the database master key on secondary replicas when adding TDE-encrypted databases.
When a database is protected by Transparent Data Encryption (TDE), the certificate used to protect the Database Encryption Key must exist on every secondary replica.
Providing this parameter together with SharedPath allows the command to automatically copy the TDE certificate from the primary to each secondary replica.
If the secondary already has a master key, this password is used to create one if it is missing.
.PARAMETER WhatIf
Shows what would happen if the command were to run. No actions are actually performed.
.PARAMETER Confirm
Prompts you for confirmation before executing any changing operations within the command.
.PARAMETER EnableException
By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.
.NOTES
Tags: AG, HA
Author: Chrissy LeMaire (@cl), netnerds.net | Andreas Jordan (@JordanOrdix), ordix.de
Website: https://dbatools.io
Copyright: (c) 2018 by dbatools, licensed under MIT
License: MIT https://opensource.org/licenses/MIT
.LINK
https://dbatools.io/Add-DbaAgDatabase
.EXAMPLE
PS C:\> Add-DbaAgDatabase -SqlInstance sql2017a -AvailabilityGroup ag1 -Database db1, db2 -Confirm
Adds db1 and db2 to ag1 on sql2017a. Prompts for confirmation.
.EXAMPLE
PS C:\> Get-DbaDatabase -SqlInstance sql2017a | Out-GridView -Passthru | Add-DbaAgDatabase -AvailabilityGroup ag1
Adds selected databases from sql2017a to ag1
.EXAMPLE
PS C:\> Get-DbaDbSharePoint -SqlInstance sqlcluster | Add-DbaAgDatabase -AvailabilityGroup SharePoint
Adds SharePoint databases as found in SharePoint_Config on sqlcluster to ag1 on sqlcluster
.EXAMPLE
PS C:\> Get-DbaDbSharePoint -SqlInstance sqlcluster -ConfigDatabase SharePoint_Config_2019 | Add-DbaAgDatabase -AvailabilityGroup SharePoint
Adds SharePoint databases as found in SharePoint_Config_2019 on sqlcluster to ag1 on sqlcluster
.EXAMPLE
PS C:\> $adv_param = @{
>> CompressBackup = $true
>> FileCount = 3
>> }
PS C:\> $splat = @{
>> SqlInstance = 'sql2017a'
>> AvailabilityGroup = 'ag1'
>> Database = 'db1'
>> Secondary = 'sql2017b'
>> SeedingMode = 'Manual'
>> SharedPath = '\\FS\Backup'
>> }
PS C:\> Add-DbaAgDatabase @splat -AdvancedBackupParams $adv_param
Adds db1 to ag1 on sql2017a and sql2017b. Uses compression and three files while taking the backups.
.EXAMPLE
PS C:\> Add-DbaAgDatabase -SqlInstance sql2017a -AvailabilityGroup ag1 -Database db1 -NoWait
Adds db1 to ag1 on sql2017a and returns immediately without waiting for seeding to complete on secondary replicas. Seeding will continue in the background.
.OUTPUTS
Microsoft.SqlServer.Management.Smo.AvailabilityDatabase
Returns one AvailabilityDatabase object per replica where the database was added. For example, adding one database to an AG with two replicas returns two objects - one for the primary and one for each secondary.
Default display properties (via Select-DefaultView):
- ComputerName: The computer name of the SQL Server instance
- InstanceName: The SQL Server instance name
- SqlInstance: The full SQL Server instance name (computer\instance)
- AvailabilityGroup: Name of the availability group
- LocalReplicaRole: Role of this replica (Primary or Secondary)
- Name: Database name
- SynchronizationState: Current synchronization state (NotSynchronizing, Synchronizing, Synchronized, Reverting, Initializing)
- IsFailoverReady: Boolean indicating if the database is ready for failover
- IsJoined: Boolean indicating if the database has joined the availability group
- IsSuspended: Boolean indicating if data movement is suspended
Additional properties available (from SMO AvailabilityDatabase object):
- DatabaseGuid: Unique identifier for the database
- EstimatedDataLoss: Estimated data loss in seconds
- EstimatedRecoveryTime: Estimated recovery time in seconds
- FileStreamSendRate: Rate of FILESTREAM data being sent (bytes/sec)
- GroupDatabaseId: Unique identifier for the database within the AG
- ID: Internal object ID
- IsAvailabilityDatabaseSuspended: Boolean indicating suspension state
- IsDatabaseDiskHealthy: Boolean indicating if database disk health is good
- IsDatabaseJoined: Boolean indicating database join state
- IsInstanceDiskHealthy: Boolean indicating if instance disk health is good
- IsInstanceHealthy: Boolean indicating overall instance health
- IsPendingSecondarySuspend: Boolean indicating if secondary suspend is pending
- LastCommitLsn: Last commit log sequence number
- LastCommitTime: Timestamp of last committed transaction
- LastHardenedLsn: Last hardened log sequence number
- LastHardenedTime: Timestamp when last LSN was hardened
- LastReceivedLsn: Last received log sequence number
- LastReceivedTime: Timestamp when last LSN was received
- LastRedoneLsn: Last redone log sequence number
- LastRedoneTime: Timestamp when last LSN was redone
- LastSentLsn: Last sent log sequence number
- LastSentTime: Timestamp when last LSN was sent
- LogSendQueue: Size of log send queue in KB
- LogSendRate: Rate of log sending (bytes/sec)
- LowWaterMarkForGhostCleanup: Low water mark LSN for ghost cleanup
- Parent: Reference to parent AvailabilityGroup SMO object
- RecoveryLsn: Recovery log sequence number
- RedoQueue: Size of redo queue in KB
- RedoRate: Rate of redo operations (bytes/sec)
- SecondaryLagSeconds: Lag in seconds for secondary replica
- State: SMO object state (Existing, Creating, Pending, etc.)
- SuspendReason: Reason for suspension if database is suspended
- Urn: Uniform Resource Name for the SMO object
- UserAccess: User access state
All properties from the base SMO object are accessible even though only default properties are displayed without using Select-Object *.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
param (
[Parameter(ParameterSetName = 'NonPipeline', Mandatory = $true, Position = 0)]
[DbaInstanceParameter]$SqlInstance,
[Parameter(ParameterSetName = 'NonPipeline')]
[PSCredential]$SqlCredential,
[Parameter(ParameterSetName = 'NonPipeline', Mandatory = $true)]
[Parameter(ParameterSetName = 'Pipeline', Mandatory = $true, Position = 0)]
[string]$AvailabilityGroup,
[Parameter(ParameterSetName = 'NonPipeline', Mandatory = $true)]
[string[]]$Database,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[DbaInstanceParameter[]]$Secondary,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[PSCredential]$SecondarySqlCredential,
[parameter(ValueFromPipeline, ParameterSetName = 'Pipeline', Mandatory = $true)]
[Microsoft.SqlServer.Management.Smo.Database[]]$InputObject,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[ValidateSet('Automatic', 'Manual')]
[string]$SeedingMode,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[string]$SharedPath,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[switch]$UseLastBackup,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[hashtable]$AdvancedBackupParams,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[switch]$NoWait,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[switch]$SkipReuseSourceFolderStructure,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[Security.SecureString]$MasterKeySecurePassword,
[Parameter(ParameterSetName = 'NonPipeline')]
[Parameter(ParameterSetName = 'Pipeline')]
[switch]$EnableException
)
begin {
# We have three while loops, that need a timeout to not loop forever if somethings goes wrong:
# while ($agDb.State -ne 'Existing') - should only take milliseconds, so we set a default timeout of one minute
# while ($replicaAgDb.State -ne 'Existing') - should only take milliseconds, so we set a default timeout of one minute
# while ($stillWaiting) - can take a long time with automatic seeding, but progress is displayed, so we set a default timeout of one day
# We will use two timeout configuration values, as we don't want to add more timeout parameters to the command. We will store the timeouts in seconds.
# The timeout for synchronization can be set to a lower value to end the command even when the synchronization is not finished yet.
# The synchronization will continue even the command or the powershell session stops.
# Even when the SQL Server instance is restarted, the synchronization will continue after the restart.
# Set-DbatoolsConfig -FullName commands.add-dbaagdatabase.timeout.existing -Value 60
# Set-DbatoolsConfig -FullName commands.add-dbaagdatabase.timeout.synchronization -Value 86400
$timeoutExisting = Get-DbatoolsConfigValue -FullName commands.add-dbaagdatabase.timeout.existing -Fallback 60
$timeoutSynchronization = Get-DbatoolsConfigValue -FullName commands.add-dbaagdatabase.timeout.synchronization -Fallback 86400
# While in a while loop, configure the time in milliseconds to wait for the next test:
# Set-DbatoolsConfig -FullName commands.add-dbaagdatabase.wait.while -Value 100
$waitWhile = Get-DbatoolsConfigValue -FullName commands.add-dbaagdatabase.wait.while -Fallback 100
# With automatic seeding we add the current seeding progress in verbose output and a progress bar. This can be disabled:
# Set-DbatoolsConfig -FullName commands.add-dbaagdatabase.report.seeding -Value $true
$reportSeeding = Get-DbatoolsConfigValue -FullName commands.add-dbaagdatabase.report.seeding -Fallback $true
}
process {
# We store information for the progress bar in a hashtable suitable for splatting.
$progress = @{ }
$progress['Id'] = Get-Random
$progress['Activity'] = "Adding database(s) to Availability Group $AvailabilityGroup"
# A throw from Stop-Function, a stopped pipeline and Select-Object -First all leave the process block early, so the bars are completed in finally
try {
$testResult = @( )
foreach ($dbName in $Database) {
try {
$progress['Status'] = "Test prerequisites for joining database $dbName"
Write-Progress @progress
$testSplat = @{
SqlInstance = $SqlInstance
SqlCredential = $SqlCredential
Secondary = $Secondary
SecondarySqlCredential = $SecondarySqlCredential
AvailabilityGroup = $AvailabilityGroup
AddDatabase = $dbName
UseLastBackup = $UseLastBackup
EnableException = $true
}
if ($SeedingMode) { $testSplat['SeedingMode'] = $SeedingMode }
if ($SharedPath) { $testSplat['SharedPath'] = $SharedPath }
$testResult += Test-DbaAvailabilityGroup @testSplat
} catch {
Stop-Function -Message "Testing prerequisites for joining database $dbName to Availability Group $AvailabilityGroup failed." -ErrorRecord $_ -Continue
}
}
foreach ($db in $InputObject) {
try {
$progress['Status'] = "Test prerequisites for joining database $($db.Name)"
Write-Progress @progress
$testSplat = @{
SqlInstance = $db.Parent
Secondary = $Secondary
SecondarySqlCredential = $SecondarySqlCredential
AvailabilityGroup = $AvailabilityGroup
AddDatabase = $db.Name
UseLastBackup = $UseLastBackup
EnableException = $true
}
if ($SeedingMode) { $testSplat['SeedingMode'] = $SeedingMode }
if ($SharedPath) { $testSplat['SharedPath'] = $SharedPath }
$testResult += Test-DbaAvailabilityGroup @testSplat
} catch {
Stop-Function -Message "Testing prerequisites for joining database $($db.Name) to Availability Group $AvailabilityGroup failed." -ErrorRecord $_ -Continue
}
}
Write-Message -Level Verbose -Message "Test for prerequisites returned $($testResult.Count) databases that will be joined to the Availability Group $AvailabilityGroup."
foreach ($result in $testResult) {
$server = $result.PrimaryServerSMO
$ag = $result.AvailabilityGroupSMO
$db = $result.DatabaseSMO
$replicaServerSMO = $result.ReplicaServerSMO
$restoreNeeded = $result.RestoreNeeded
$backups = $result.Backups
$replicaAgDbSMO = @{ }
$targetSynchronizationState = @{ }
$output = @( )
$progress['Activity'] = "Adding database $($db.Name) to Availability Group $AvailabilityGroup"
$progress['Status'] = "Step 1/5: Setting seeding mode if needed"
Write-Message -Level Verbose -Message $progress['Status']
Write-Progress @progress
if ($SeedingMode) {
Write-Message -Level Verbose -Message "Setting seeding mode to $SeedingMode."
$failure = $false
foreach ($replicaName in $replicaServerSMO.Keys) {
$replica = $ag.AvailabilityReplicas[$replicaName]
if ($replica.SeedingMode -ne $SeedingMode) {
if ($Pscmdlet.ShouldProcess($server, "Setting seeding mode for replica $replica to $SeedingMode")) {
try {
Write-Message -Level Verbose -Message "Setting seeding mode for replica $replica to $SeedingMode."
$replica.SeedingMode = $SeedingMode
$replica.Alter()
if ($SeedingMode -eq 'Automatic') {
Write-Message -Level Verbose -Message "Setting GrantAvailabilityGroupCreateDatabasePrivilege on server $($replicaServerSMO[$replicaName]) for Availability Group $AvailabilityGroup."
$null = Grant-DbaAgPermission -SqlInstance $replicaServerSMO[$replicaName] -Type AvailabilityGroup -AvailabilityGroup $AvailabilityGroup -Permission CreateAnyDatabase
}
} catch {
$failure = $true
Stop-Function -Message "Failed setting seeding mode for replica $replica to $SeedingMode." -ErrorRecord $_ -Continue
}
}
}
}
if ($failure) {
Stop-Function -Message "Failed setting seeding mode to $SeedingMode." -Continue
}
}
# For TDE-encrypted databases, the master certificate must exist on every secondary replica
# before a backup can be restored or automatic seeding can succeed.
if ($db.EncryptionEnabled -and $db.HasDatabaseEncryptionKey -and $db.DatabaseEncryptionKey.EncryptorType -eq "ServerCertificate") {
$encryptorName = $db.DatabaseEncryptionKey.EncryptorName
Write-Message -Level Verbose -Message "Database $($db.Name) is TDE-encrypted using certificate '$encryptorName'. Checking secondary replicas."
try {
$sourceTdeCert = Get-DbaDbCertificate -SqlInstance $server -Database "master" -Certificate $encryptorName -EnableException | Select-Object -First 1
} catch {
Stop-Function -Message "Failed to validate TDE certificate '$encryptorName' on primary instance $($server.Name)." -ErrorRecord $_ -Continue
}
if (-not $sourceTdeCert) {
Stop-Function -Message "Database $($db.Name) is encrypted by certificate '$encryptorName', but that certificate was not found in master on $($server.Name)." -Continue
}
if (-not $sourceTdeCert.PrivateKeyExists) {
Stop-Function -Message "Database $($db.Name) is encrypted by certificate '$encryptorName', but the certificate on $($server.Name) does not have an accessible private key." -Continue
}
$failure = $false
foreach ($replicaName in $replicaServerSMO.Keys) {
$replicaServer = $replicaServerSMO[$replicaName]
try {
$existingCert = Get-DbaDbCertificate -SqlInstance $replicaServer -Database "master" -Certificate $encryptorName -EnableException | Select-Object -First 1
} catch {
$failure = $true
Stop-Function -Message "Failed to validate TDE certificate '$encryptorName' on replica $replicaName." -ErrorRecord $_ -Continue
}
if (-not $existingCert) {
if (-not $SharedPath) {
$failure = $true
Stop-Function -Message "Replica $replicaName is missing TDE certificate '$encryptorName'. Provide -SharedPath and optionally -MasterKeySecurePassword to copy it automatically, or pre-stage the matching certificate before adding the database." -Continue
}
if ($Pscmdlet.ShouldProcess($replicaServer, "Copy TDE certificate '$encryptorName' from primary to replica $replicaName")) {
try {
Write-Message -Level Verbose -Message "TDE certificate '$encryptorName' not found on $replicaName. Copying from primary."
$splatTdeCert = @{
Source = $server
Destination = $replicaServer
Database = "master"
Certificate = $encryptorName
SharedPath = $SharedPath
EnableException = $true
}
if ($MasterKeySecurePassword) {
$splatTdeCert.MasterKeyPassword = $MasterKeySecurePassword
}
$null = Copy-DbaDbCertificate @splatTdeCert
} catch {
$failure = $true
Stop-Function -Message "Failed to copy TDE certificate '$encryptorName' to replica $replicaName." -ErrorRecord $_ -Continue
}
}
continue
}
if (-not $existingCert.PrivateKeyExists) {
$failure = $true
Stop-Function -Message "TDE certificate '$encryptorName' exists on replica $replicaName but does not include a private key. Restore or automatic seeding cannot use it." -Continue
}
if ($existingCert.Thumbprint -ne $sourceTdeCert.Thumbprint) {
$failure = $true
Stop-Function -Message "TDE certificate '$encryptorName' on replica $replicaName does not match the primary certificate on $($server.Name)." -Continue
}
Write-Message -Level Verbose -Message "TDE certificate '$encryptorName' already exists on replica $replicaName and matches the primary certificate."
}
if ($failure) {
Stop-Function -Message "Failed to validate or copy TDE certificate '$encryptorName' for all replicas of database $($db.Name)." -Continue
}
}
$progress['Status'] = "Step 2/5: Running backup and restore if needed"
Write-Message -Level Verbose -Message $progress['Status']
Write-Progress @progress
if ($restoreNeeded.Count -gt 0) {
if (-not $backups) {
if ($Pscmdlet.ShouldProcess($server, "Taking full and log backup of database $($db.Name)")) {
try {
Write-Message -Level Verbose -Message "Taking full and log backup of database $($db.Name)."
if ($AdvancedBackupParams) {
$fullbackup = $db | Backup-DbaDatabase -BackupDirectory $SharedPath -Type Full -EnableException @AdvancedBackupParams
$logbackup = $db | Backup-DbaDatabase -BackupDirectory $SharedPath -Type Log -EnableException @AdvancedBackupParams
} else {
$fullbackup = $db | Backup-DbaDatabase -BackupDirectory $SharedPath -Type Full -EnableException
$logbackup = $db | Backup-DbaDatabase -BackupDirectory $SharedPath -Type Log -EnableException
}
$backups = $fullbackup, $logbackup
} catch {
Stop-Function -Message "Failed to take full and log backup of database $($db.Name)." -ErrorRecord $_ -Continue
}
}
}
$failure = $false
foreach ($replicaName in $restoreNeeded.Keys) {
if ($Pscmdlet.ShouldProcess($replicaServerSMO[$replicaName], "Restore database $($db.Name) to replica $replicaName")) {
try {
Write-Message -Level Verbose -Message "Restore database $($db.Name) to replica $replicaName."
$restoreParams = @{
SqlInstance = $replicaServerSMO[$replicaName]
NoRecovery = $true
TrustDbBackupHistory = $true
EnableException = $true
}
# Check if we should skip ReuseSourceFolderStructure
if (-not $SkipReuseSourceFolderStructure) {
# Check if primary and replica are on the same platform
$primaryPlatform = $server.HostPlatform
$replicaPlatform = $replicaServerSMO[$replicaName].HostPlatform
if ($primaryPlatform -ne $replicaPlatform) {
Write-Message -Level Verbose -Message "Primary platform ($primaryPlatform) does not match replica platform ($replicaPlatform). Setting SkipReuseSourceFolderStructure."
$SkipReuseSourceFolderStructure = $true
}
}
# Only use ReuseSourceFolderStructure if not skipped
if (-not $SkipReuseSourceFolderStructure) {
Write-Message -Level Verbose -Message "Using ReuseSourceFolderStructure to maintain consistent folder layout."
$restoreParams['ReuseSourceFolderStructure'] = $true
} else {
Write-Message -Level Verbose -Message "Using replica's default paths for database files."
}
$sourceOwner = $db.Owner
$replicaOwner = $replicaServerSMO[$replicaName].ConnectedAs
if ($sourceOwner -ne $replicaOwner) {
Write-Message -Level Verbose -Message "Source database owner is $sourceOwner, replica database owner would be $replicaOwner."
if ($replicaServerSMO[$replicaName].Logins[$db.Owner]) {
Write-Message -Level Verbose -Message "Source database owner is found on replica, so using ExecuteAs with Restore-DbaDatabase to set correct owner."
$restoreParams['ExecuteAs'] = $db.Owner
} else {
Write-Message -Level Verbose -Message "Source database owner is not found on replica, so there is nothing we can do."
}
}
$null = $backups | Restore-DbaDatabase @restoreParams
} catch {
$failure = $true
Stop-Function -Message "Failed to restore database $($db.Name) to replica $replicaName." -ErrorRecord $_ -Continue
}
}
}
if ($failure) {
Stop-Function -Message "Failed to restore database $($db.Name)." -Continue
}
}
$progress['Status'] = "Step 3/5: Add the database to the Availability Group on the primary replica"
Write-Message -Level Verbose -Message $progress['Status']
if ($Pscmdlet.ShouldProcess($server, "Add database $($db.Name) to Availability Group $AvailabilityGroup on the primary replica")) {
try {
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) on is not yet known"
Write-Message -Level Verbose -Message "Object of type AvailabilityDatabase for $($db.Name) will be created. $($progress['CurrentOperation'])"
Write-Progress @progress
if ($ag.AvailabilityDatabases.Name -contains $db.Name) {
Write-Message -Level Verbose -Message "Database $($db.Name) is already joined to Availability Group $AvailabilityGroup. No action will be taken on the primary replica."
} else {
$agDb = Get-DbaAgDatabase -SqlInstance $server -AvailabilityGroup $ag.Name -Database $db.Name
$agDb = New-Object Microsoft.SqlServer.Management.Smo.AvailabilityDatabase($ag, $db.Name)
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) is $($agDb.State)"
Write-Message -Level Verbose -Message "Object of type AvailabilityDatabase for $($db.Name) is created. $($progress['CurrentOperation'])"
Write-Progress @progress
$agDb.Create()
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) is $($agDb.State)"
Write-Message -Level Verbose -Message "Method Create of AvailabilityDatabase for $($db.Name) is executed. $($progress['CurrentOperation'])"
Write-Progress @progress
# Wait for state to become Existing
# https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.sqlsmostate
$timeout = (Get-Date).AddSeconds($timeoutExisting)
while ($agDb.State -ne 'Existing') {
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) is $($agDb.State), waiting for Existing"
Write-Message -Level Verbose -Message $progress['CurrentOperation']
Write-Progress @progress
if ((Get-Date) -gt $timeout) {
Stop-Function -Message "Failed to add database $($db.Name) to Availability Group $AvailabilityGroup. Timeout of $timeoutExisting seconds is reached. State of AvailabilityDatabase for $($db.Name) is still $($agDb.State)." -Continue
}
Start-Sleep -Milliseconds $waitWhile
$agDb.Refresh()
}
# Get customized SMO for the output
$output += Get-DbaAgDatabase -SqlInstance $server -AvailabilityGroup $AvailabilityGroup -Database $db.Name -EnableException
}
} catch {
Stop-Function -Message "Failed to add database $($db.Name) to Availability Group $AvailabilityGroup" -ErrorRecord $_ -Continue
}
}
$progress['Status'] = "Step 4/5: Add the database to the Availability Group on the secondary replicas"
Write-Message -Level Verbose -Message $progress['Status']
$failure = $false
foreach ($replicaName in $replicaServerSMO.Keys) {
if ($Pscmdlet.ShouldProcess($replicaServerSMO[$replicaName], "Add database $($db.Name) to Availability Group $AvailabilityGroup on replica $replicaName")) {
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) on replica $replicaName is not yet known"
Write-Message -Level Verbose -Message $progress['CurrentOperation']
Write-Progress @progress
try {
$replicaAgDb = Get-DbaAgDatabase -SqlInstance $replicaServerSMO[$replicaName] -AvailabilityGroup $AvailabilityGroup -Database $db.Name -EnableException
} catch {
$failure = $true
Stop-Function -Message "Failed to get database $($db.Name) on replica $replicaName." -ErrorRecord $_ -Continue
}
if ($replicaAgDb.IsJoined) {
Write-Message -Level Verbose -Message "Database $($db.Name) is already joined to Availability Group $AvailabilityGroup. No action will be taken on the replica $replicaName."
$replicaAgDbSMO[$replicaName] = $replicaAgDb
} else {
# Save SMO in array for the output
$output += $replicaAgDb
# Save SMO in hashtable for further processing
$replicaAgDbSMO[$replicaName] = $replicaAgDb
# Save target targetSynchronizationState for further processing
# https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.availabilityreplicaavailabilitymode
# https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.availabilitydatabasesynchronizationstate
$availabilityMode = $ag.AvailabilityReplicas[$replicaName].AvailabilityMode
if ($availabilityMode -eq 'AsynchronousCommit') {
$targetSynchronizationState[$replicaName] = 'Synchronizing'
} elseif ($availabilityMode -eq 'SynchronousCommit') {
$targetSynchronizationState[$replicaName] = 'Synchronized'
} else {
$failure = $true
Stop-Function -Message "Unexpected value '$availabilityMode' for AvailabilityMode on replica $replicaName." -Continue
}
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) on replica $replicaName is $($replicaAgDb.State)"
Write-Message -Level Verbose -Message $progress['CurrentOperation']
Write-Progress @progress
# https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.sqlsmostate
$timeout = (Get-Date).AddSeconds($timeoutExisting)
while ($replicaAgDb.State -ne 'Existing') {
$progress['CurrentOperation'] = "State of AvailabilityDatabase for $($db.Name) on replica $replicaName is $($replicaAgDb.State), waiting for Existing."
Write-Message -Level Verbose -Message $progress['CurrentOperation']
Write-Progress @progress
if ((Get-Date) -gt $timeout) {
Stop-Function -Message "Failed to add database $($db.Name) on replica $replicaName. Timeout of $timeoutExisting seconds is reached. State of AvailabilityDatabase for $db is still $($replicaAgDb.State)." -Continue
}
Start-Sleep -Milliseconds $waitWhile
$replicaAgDb.Refresh()
}
# With automatic seeding, .JoinAvailablityGroup() is not needed, just wait for the magic to happen
if ($ag.AvailabilityReplicas[$replicaName].SeedingMode -ne 'Automatic') {
try {
$progress['CurrentOperation'] = "Joining database $($db.Name) on replica $replicaName"
Write-Message -Level Verbose -Message $progress['CurrentOperation']
Write-Progress @progress
# DO NOT fix the typo in "JoinAvailablityGroup()" as it is a typo the SMO.
$replicaAgDb.JoinAvailablityGroup()
} catch {
$failure = $true
Stop-Function -Message "Failed to join database $($db.Name) on replica $replicaName." -ErrorRecord $_ -Continue
}
}
}
}
}
if ($failure) {
Stop-Function -Message "Failed to add or join database $($db.Name)." -Continue
}
# Now we have configured everything and we only have to wait...
$progress['Status'] = "Step 5/5: Wait for the database to finish joining the Availability Group on the secondary replicas"
$progress['CurrentOperation'] = ''
Write-Message -Level Verbose -Message $progress['Status']
Write-Progress @progress
if ($NoWait) {
Write-Message -Level Verbose -Message "NoWait parameter specified. Skipping wait for database $($db.Name) to finish joining the Availability Group $AvailabilityGroup on the secondary replicas. Synchronization will continue in the background."
} elseif ($Pscmdlet.ShouldProcess($server, "Wait for the database $($db.Name) to finish joining the Availability Group $AvailabilityGroup on the secondary replicas.")) {
# We need to setup a progress bar for every replica to display them all at once.
$syncProgressId = @{ }
foreach ($replicaName in $replicaServerSMO.Keys) {
$syncProgressId[$replicaName] = Get-Random
}
# A throw from Stop-Function or a stopped pipeline leaves the wait early, so the replica bars are completed in finally
try {
$stillWaiting = $true
$timeout = (Get-Date).AddSeconds($timeoutSynchronization)
while ($stillWaiting) {
$stillWaiting = $false
$failure = $false
foreach ($replicaName in $replicaServerSMO.Keys) {
if (-not $targetSynchronizationState[$replicaName]) {
Write-Message -Level Verbose -Message "Database $($db.Name) is already joined to Availability Group $AvailabilityGroup. No action will be taken on the replica $replicaName."
continue
}
if (-not $replicaAgDbSMO[$replicaName].IsJoined -or $replicaAgDbSMO[$replicaName].SynchronizationState -ne $targetSynchronizationState[$replicaName]) {
$stillWaiting = $true
}
$syncProgress = @{ }
$syncProgress['Id'] = $syncProgressId[$replicaName]
$syncProgress['ParentId'] = $progress['Id']
$syncProgress['Activity'] = "Adding database $($db.Name) to Availability Group $AvailabilityGroup on replica $replicaName"
if ($replicaAgDbSMO[$replicaName].SynchronizationState -ne $targetSynchronizationState[$replicaName]) {
$syncProgress['Status'] = "IsJoined is $($replicaAgDbSMO[$replicaName].IsJoined), SynchronizationState is $($replicaAgDbSMO[$replicaName].SynchronizationState), waiting for $($targetSynchronizationState[$replicaName])"
} else {
$syncProgress['Status'] = "IsJoined is $($replicaAgDbSMO[$replicaName].IsJoined), SynchronizationState is $($replicaAgDbSMO[$replicaName].SynchronizationState), replica is in desired state"
}
if ($ag.AvailabilityReplicas[$replicaName].SeedingMode -eq 'Automatic' -and $reportSeeding) {
$physicalSeedingStats = $server.Query("SELECT TOP 1 * FROM sys.dm_hadr_physical_seeding_stats WHERE local_database_name = '$($db.Name)' AND remote_machine_name = '$($ag.AvailabilityReplicas[$replicaName].EndpointUrl)' ORDER BY start_time_utc DESC")
if ($physicalSeedingStats) {
if ($physicalSeedingStats.failure_message -ne [DBNull]::Value) {
$failure = $true
Stop-Function -Message "Failed while seeding database $($db.Name) to $replicaName. failure_message: $($physicalSeedingStats.failure_message)." -Continue
}
$syncProgress['PercentComplete'] = [int]($physicalSeedingStats.transferred_size_bytes * 100.0 / $physicalSeedingStats.database_size_bytes)
$syncProgress['SecondsRemaining'] = [int](($physicalSeedingStats.estimate_time_complete_utc - (Get-Date).ToUniversalTime()).TotalSeconds)
$syncProgress['CurrentOperation'] = "Seeding state: $($physicalSeedingStats.internal_state_desc), $([int]($physicalSeedingStats.transferred_size_bytes/1024/1024)) out of $([int]($physicalSeedingStats.database_size_bytes/1024/1024)) MB transferred"
}
$automaticSeeding = $server.Query("SELECT TOP 1 * FROM sys.dm_hadr_automatic_seeding WHERE ag_id = '$($ag.UniqueId.Guid.ToUpper())' AND ag_db_id = '$($ag.AvailabilityDatabases[$db.Name].UniqueId.Guid.ToUpper())' AND ag_remote_replica_id = '$($ag.AvailabilityReplicas[$replicaName].UniqueId.Guid.ToUpper())' ORDER BY start_time DESC")
Write-Message -Level Verbose -Message "Current automatic seeding state: $($automaticSeeding.current_state)"
if ($automaticSeeding.current_state -eq 'FAILED') {
$failure = $true
Stop-Function -Message "Failed while seeding database $($db.Name) to $replicaName. failure_message: $($automaticSeeding.failure_state_desc)." -Continue
}
}
Write-Message -Level Verbose -Message ($syncProgress['Status'] + $syncProgress['CurrentOperation'])
Write-Progress @syncProgress
}
if ($failure) {
$stillWaiting = $false
Stop-Function -Message "Failed while seeding database $($db.Name)." -Continue
}
if ((Get-Date) -gt $timeout) {
$stillWaiting = $false
$failure = $true
Stop-Function -Message "Failed to join or synchronize database $($db.Name). Timeout of $timeoutSynchronization seconds is reached. $progressOperation" -Continue
}
Start-Sleep -Milliseconds $waitWhile
foreach ($replicaName in $replicaServerSMO.Keys) {
$replicaAgDbSMO[$replicaName].Refresh()
}
}
} finally {
# This runs before the outer finally completes the parent bar, the host would keep the replica bars otherwise
foreach ($replicaName in $replicaServerSMO.Keys) {
Write-Progress -Id $syncProgressId[$replicaName] -ParentId $progress['Id'] -Activity Completed -Completed
}
}
if ($failure) {
Stop-Function -Message "Failed to join or synchronize database $($db.Name)." -Continue
}
}
$output
}
} finally {
Write-Progress @progress -Completed
}
}
}