Skip to content

Commit dbb056b

Browse files
Merge branch 'development' into claude/issue-10004-20260314-1653
2 parents 35fc185 + 444659b commit dbb056b

66 files changed

Lines changed: 3316 additions & 307 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/dbatools-buildref-index.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"LastUpdated": "2026-03-12T00:00:00",
2+
"LastUpdated": "2026-03-13T00:00:00",
33
"Data": [
44
{
55
"Version": "8.0.47",
@@ -5082,6 +5082,11 @@
50825082
"Version": "16.0.4240",
50835083
"KBList": "5077464"
50845084
},
5085+
{
5086+
"CU": "CU24",
5087+
"Version": "16.0.4245",
5088+
"KBList": "5080999"
5089+
},
50855090
{
50865091
"Version": "17.0.100",
50875092
"Name": "2025"
@@ -5125,6 +5130,11 @@
51255130
{
51265131
"Version": "17.0.4020",
51275132
"KBList": "5077466"
5133+
},
5134+
{
5135+
"CU": "CU3",
5136+
"Version": "17.0.4025",
5137+
"KBList": "5077896"
51285138
}
51295139
]
51305140
}

bin/dbatools-index.json

22.4 KB
Binary file not shown.

dbatools.psd1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
RootModule = 'dbatools.psm1'
1212

1313
# Version number of this module.
14-
ModuleVersion = '2.7.26'
14+
ModuleVersion = '2.7.27'
1515

1616
# ID used to uniquely identify this module
1717
GUID = '9d139310-ce45-41ce-8e8b-d76335aa1789'
@@ -196,6 +196,7 @@
196196
'Get-DbaAgHadr',
197197
'Get-DbaAgListener',
198198
'Get-DbaAgReplica',
199+
'Get-DbaAgRingBuffer',
199200
'Get-DbaAvailabilityGroup',
200201
'Get-DbaAvailableCollation',
201202
'Get-DbaBackupDevice',
@@ -539,6 +540,7 @@
539540
'Remove-DbaAgentAlert',
540541
'Remove-DbaAgentJob',
541542
'Remove-DbaAgentJobCategory',
543+
'Remove-DbaAgentJobSchedule',
542544
'Remove-DbaAgentJobStep',
543545
'Remove-DbaAgentOperator',
544546
'Remove-DbaAgentSchedule',
@@ -621,6 +623,7 @@
621623
'Set-DbaDbFileGroup',
622624
'Set-DbaDbFileGrowth',
623625
'Set-DbaDbIdentity',
626+
'Set-DbaDbMailAccount',
624627
'Set-DbaDbMirror',
625628
'Set-DbaDbOwner',
626629
'Set-DbaDbQueryStoreOption',
@@ -668,6 +671,7 @@
668671
'Sync-DbaLoginPassword',
669672
'Sync-DbaLoginPermission',
670673
'Test-DbaAgentJobOwner',
674+
'Test-DbaAgPolicyState',
671675
'Test-DbaAvailabilityGroup',
672676
'Test-DbaBackupInformation',
673677
'Test-DbaBuild',
@@ -703,6 +707,7 @@
703707
'Test-DbaOptimizeForAdHoc',
704708
'Test-DbaPath',
705709
'Test-DbaPowerPlan',
710+
'Test-DbaInstantFileInitialization',
706711
'Test-DbaReplLatency',
707712
'Test-DbaSpn',
708713
'Test-DbaTempDbConfig',

dbatools.psm1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ if ($PSVersionTable.PSVersion.Major -lt 5) {
555555
'Remove-DbaAgentJob',
556556
'New-DbaAgentJobStep',
557557
'Set-DbaAgentJobStep',
558+
'Remove-DbaAgentJobSchedule',
558559
'Remove-DbaAgentJobStep',
559560
'New-DbaAgentSchedule',
560561
'Set-DbaAgentSchedule',
@@ -607,6 +608,7 @@ if ($PSVersionTable.PSVersion.Major -lt 5) {
607608
'Mount-DbaDatabase',
608609
'Dismount-DbaDatabase',
609610
'Get-DbaAgReplica',
611+
'Get-DbaAgRingBuffer',
610612
'Get-DbaAgDatabase',
611613
'Get-DbaAgDatabaseReplicaState',
612614
'Get-DbaModule',
@@ -710,6 +712,7 @@ if ($PSVersionTable.PSVersion.Major -lt 5) {
710712
'New-DbaDbMailServer',
711713
'New-DbaDbMailAccount',
712714
'New-DbaDbMailProfile',
715+
'Set-DbaDbMailAccount',
713716
'Get-DbaResourceGovernor',
714717
'Get-DbaRgResourcePool',
715718
'Get-DbaRgWorkloadGroup',
@@ -893,6 +896,7 @@ if ($PSVersionTable.PSVersion.Major -lt 5) {
893896
'Remove-DbaDbFileGroup',
894897
'Set-DbaDbFileGroup',
895898
'Remove-DbaLinkedServer',
899+
'Test-DbaAgPolicyState',
896900
'Test-DbaAvailabilityGroup',
897901
'Export-DbaUser',
898902
'Get-DbaSsisExecutionHistory',
@@ -1085,6 +1089,7 @@ if ($PSVersionTable.PSVersion.Major -lt 5) {
10851089
'Get-DbaDiskSpace',
10861090
'Test-DbaDiskAllocation',
10871091
'Test-DbaPowerPlan',
1092+
'Test-DbaInstantFileInitialization',
10881093
'Set-DbaPowerPlan',
10891094
'Test-DbaDiskAlignment',
10901095
'Get-DbaStartupParameter',

private/functions/Start-DbccCheck.ps1

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function Start-DbccCheck {
88
)
99

1010
$servername = $server.name
11+
$escapedDbName = $DbName.Replace("]", "]]")
1112

1213
if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $DbName on $servername")) {
1314
if ($server.ConnectionContext.StatementTimeout -ne 0) {
@@ -17,17 +18,24 @@ function Start-DbccCheck {
1718
try {
1819
if ($table) {
1920
$null = $server.databases[$DbName].CheckTables('None')
20-
Write-Verbose "Dbcc CheckTables finished successfully for $DbName on $servername"
21+
Write-Verbose "DBCC CheckTables finished successfully for $DbName on $servername"
22+
return [PSCustomObject]@{
23+
Status = "Success"
24+
Output = $null
25+
}
2126
} else {
2227
if ($MaxDop) {
23-
$null = $server.Query("DBCC CHECKDB ([$DbName]) WITH MAXDOP = $MaxDop")
24-
Write-Verbose "Dbcc CHECKDB finished successfully for $DbName on $servername"
28+
$query = "DBCC CHECKDB ([$escapedDbName]) WITH MAXDOP = $MaxDop"
2529
} else {
26-
$null = $server.Query("DBCC CHECKDB ([$DbName])")
27-
Write-Verbose "Dbcc CHECKDB finished successfully for $DbName on $servername"
30+
$query = "DBCC CHECKDB ([$escapedDbName])"
31+
}
32+
$dbccOutput = Invoke-DbaQuery -SqlInstance $server -Query $query -MessagesToOutput -EnableException
33+
Write-Verbose "DBCC CHECKDB finished successfully for $DbName on $servername"
34+
return [PSCustomObject]@{
35+
Status = "Success"
36+
Output = $dbccOutput
2837
}
2938
}
30-
return "Success"
3139
} catch {
3240
$originalException = $_.Exception
3341
$loopNo = 0
@@ -61,7 +69,10 @@ function Start-DbccCheck {
6169
} catch {
6270
$null
6371
}
64-
return $message.Trim()
72+
return [PSCustomObject]@{
73+
Status = $message.Trim()
74+
Output = $null
75+
}
6576
}
6677
}
67-
}
78+
}

private/functions/Update-ServiceStatus.ps1

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ function Update-ServiceStatus {
7171
$svcControlBlock = {
7272
$group = $_.Group
7373
$computerName = $_.Name
74+
# Create a CIM session preferring DCOM to avoid requiring WinRM on the target machine.
75+
# CIM instances become deserialized when crossing runspace boundaries and lose their
76+
# session context, causing Invoke-CimMethod to attempt a new WinRM connection by default.
77+
# Using DCOM avoids this WinRM dependency for machines where WinRM is not configured.
78+
$splatCimDcomOption = @{
79+
Protocol = "Dcom"
80+
}
81+
$cimDcomOption = New-CimSessionOption @splatCimDcomOption
82+
$splatCimSessionDcom = @{
83+
ComputerName = $computerName
84+
SessionOption = $cimDcomOption
85+
ErrorAction = "Stop"
86+
}
87+
try {
88+
$cimSession = New-CimSession @splatCimSessionDcom
89+
} catch {
90+
# Fall back to default protocol (WinRM) if DCOM is unavailable
91+
try {
92+
$cimSession = New-CimSession -ComputerName $computerName -ErrorAction "Stop"
93+
} catch {
94+
$cimSession = $null
95+
}
96+
}
7497
$servicePriorityCollection = $group.ServicePriority | Select-Object -unique | Sort-Object -Property @{ Expression = { [int]$_ }; Descending = $action -ne 'stop' }
7598
foreach ($priority in $servicePriorityCollection) {
7699
$services = $group | Where-Object { $_.ServicePriority -eq $priority }
@@ -104,6 +127,23 @@ function Update-ServiceStatus {
104127
$invokeResults = @()
105128
foreach ($service in $servicesToRestart) {
106129
if ($Pscmdlet.ShouldProcess("Sending $action request to service $($service.ServiceName) on $($service.ComputerName)")) {
130+
# Get a fresh CIM instance via the DCOM session to avoid issues with deserialized
131+
# CIM objects crossing runspace boundaries without their session context.
132+
if ($cimSession) {
133+
try {
134+
$splatGetFreshCim = @{
135+
CimSession = $cimSession
136+
Namespace = "root\cimv2"
137+
Query = "SELECT * FROM Win32_Service WHERE Name = '$($service.ServiceName)'"
138+
}
139+
$freshCimObj = Get-CimInstance @splatGetFreshCim
140+
if ($freshCimObj) {
141+
$service._CimObject = $freshCimObj
142+
}
143+
} catch {
144+
# Fall back to using the existing deserialized CIM object if session refresh fails
145+
}
146+
}
107147
#Invoke corresponding CIM method
108148
$invokeResult = Invoke-CimMethod -InputObject $service._CimObject -MethodName $methodName
109149
$invokeResults += [psobject]@{
@@ -123,7 +163,19 @@ function Update-ServiceStatus {
123163
foreach ($result in ($invokeResults | Where-Object CheckPending -eq $true)) {
124164
try {
125165
#Refresh Cim instance - not using Get-DbaCmObject because module is not loaded here, but it only refreshes existing object
126-
$result.Service._CimObject = $result.Service._CimObject | Get-CimInstance
166+
if ($cimSession) {
167+
$splatRefreshCim = @{
168+
CimSession = $cimSession
169+
Namespace = "root\cimv2"
170+
Query = "SELECT State FROM Win32_Service WHERE Name = '$($result.Service.ServiceName)'"
171+
}
172+
$refreshedCimObj = Get-CimInstance @splatRefreshCim
173+
if ($refreshedCimObj) {
174+
$result.Service._CimObject = $refreshedCimObj
175+
}
176+
} else {
177+
$result.Service._CimObject = $result.Service._CimObject | Get-CimInstance
178+
}
127179
} catch {
128180
$result.ServiceExitCode = -3
129181
$result.ServiceState = 'Unknown'
@@ -177,6 +229,11 @@ function Update-ServiceStatus {
177229
$result
178230
}
179231
}
232+
# Clean up the CIM session created for DCOM connections
233+
if ($cimSession) {
234+
Remove-CimSession -CimSession $cimSession -ErrorAction "SilentlyContinue"
235+
$cimSession = $null
236+
}
180237
}
181238

182239
$actionText = switch ($action) { stop { 'stopped' }; start { 'started' }; restart { 'restarted' } }

public/Add-DbaAgDatabase.ps1

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ function Add-DbaAgDatabase {
8383
When enabled, Restore-DbaDatabase uses the replica's default data and log directories instead of attempting to replicate the primary's folder structure.
8484
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).
8585
86+
.PARAMETER MasterKeySecurePassword
87+
Password for creating or opening the database master key on secondary replicas when adding TDE-encrypted databases.
88+
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.
89+
Providing this parameter together with SharedPath allows the command to automatically copy the TDE certificate from the primary to each secondary replica.
90+
If the secondary already has a master key, this password is used to create one if it is missing.
91+
8692
.PARAMETER WhatIf
8793
Shows what would happen if the command were to run. No actions are actually performed.
8894
@@ -242,6 +248,9 @@ function Add-DbaAgDatabase {
242248
[switch]$SkipReuseSourceFolderStructure,
243249
[Parameter(ParameterSetName = 'NonPipeline')]
244250
[Parameter(ParameterSetName = 'Pipeline')]
251+
[Security.SecureString]$MasterKeySecurePassword,
252+
[Parameter(ParameterSetName = 'NonPipeline')]
253+
[Parameter(ParameterSetName = 'Pipeline')]
245254
[switch]$EnableException
246255
)
247256

@@ -365,6 +374,49 @@ function Add-DbaAgDatabase {
365374
}
366375
}
367376

377+
# For TDE-encrypted databases, the master certificate must exist on every secondary replica
378+
# before a backup can be restored or automatic seeding can succeed.
379+
if ($db.EncryptionEnabled -and $db.HasDatabaseEncryptionKey -and $db.DatabaseEncryptionKey.EncryptorType -eq "ServerCertificate") {
380+
$encryptorName = $db.DatabaseEncryptionKey.EncryptorName
381+
Write-Message -Level Verbose -Message "Database $($db.Name) is TDE-encrypted using certificate '$encryptorName'. Checking secondary replicas."
382+
if ($SharedPath) {
383+
$failure = $false
384+
foreach ($replicaName in $replicaServerSMO.Keys) {
385+
$replicaServer = $replicaServerSMO[$replicaName]
386+
$existingCert = Get-DbaDbCertificate -SqlInstance $replicaServer -Database master -Certificate $encryptorName
387+
if (-not $existingCert) {
388+
if ($Pscmdlet.ShouldProcess($replicaServer, "Copy TDE certificate '$encryptorName' from primary to replica $replicaName")) {
389+
try {
390+
Write-Message -Level Verbose -Message "TDE certificate '$encryptorName' not found on $replicaName. Copying from primary."
391+
$splatTdeCert = @{
392+
Source = $server
393+
Destination = $replicaServer
394+
Database = "master"
395+
Certificate = $encryptorName
396+
SharedPath = $SharedPath
397+
EnableException = $true
398+
}
399+
if ($MasterKeySecurePassword) {
400+
$splatTdeCert.MasterKeyPassword = $MasterKeySecurePassword
401+
}
402+
$null = Copy-DbaDbCertificate @splatTdeCert
403+
} catch {
404+
$failure = $true
405+
Stop-Function -Message "Failed to copy TDE certificate '$encryptorName' to replica $replicaName." -ErrorRecord $_ -Continue
406+
}
407+
}
408+
} else {
409+
Write-Message -Level Verbose -Message "TDE certificate '$encryptorName' already exists on replica $replicaName."
410+
}
411+
}
412+
if ($failure) {
413+
Stop-Function -Message "Failed to copy TDE certificate to all replicas for database $($db.Name)." -Continue
414+
}
415+
} else {
416+
Write-Message -Level Warning -Message "Database $($db.Name) is TDE-encrypted with certificate '$encryptorName', but no SharedPath was provided. The TDE certificate must exist on all secondary replicas before the database can be added. Use -SharedPath and optionally -MasterKeySecurePassword to enable automatic certificate copying."
417+
}
418+
}
419+
368420
$progress['Status'] = "Step 2/5: Running backup and restore if needed"
369421
Write-Message -Level Verbose -Message $progress['Status']
370422
Write-Progress @progress

0 commit comments

Comments
 (0)