Skip to content

Commit daf3590

Browse files
feat(migration): Convert Disable-DbaAgHadr and Enable-DbaAgHadr to C# binary cmdlets
WMI-based cmdlets using DbaBaseCmdlet (not DbaInstanceCmdlet) with Windows Credential. ShouldProcess with ConfirmImpact.High, Force for auto-restart, TestElevation for localhost. Static ScriptBlocks for Get-WmiHadr, Invoke-ManagedComputerCommand, Stop/Start-DbaService. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 58535f8 commit daf3590

4 files changed

Lines changed: 623 additions & 2 deletions

File tree

dbatools.library.psd1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
'Add-DbaAgReplica',
4545
'Clear-DbaConnectionPool',
4646
'Connect-DbaInstance',
47+
'Disable-DbaAgHadr',
4748
'Disconnect-DbaInstance',
49+
'Enable-DbaAgHadr',
4850
'Export-DbatoolsConfig',
4951
'Get-DbaAgBackupHistory',
5052
'Get-DbaAgDatabase',

docs/plan/TRACKER-MIGRATE-AG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
| 14 | Add-DbaAgDatabase | DONE | AddDbaAgDatabaseCommand.cs | OK | 100% | 1/1 param (common param diff expected); integration pre-existing infra issue | ShouldProcess, ConfirmImpact.Low, 5-step workflow (seeding/backup/restore/add/sync), dual parameter sets, progress bars, SQL injection hardened, COLLATE-safe DMV queries |
2424
| 15 | Add-DbaAgListener | DONE | AddDbaAgListenerCommand.cs | OK | 100% | 1/1 param (common param diff expected); integration pre-existing infra issue | ShouldProcess, ConfirmImpact.Low, static ScriptBlocks, subnet auto-calculation, DHCP support, Passthru, ValidateRange on Port |
2525
| 16 | Add-DbaAgReplica | DONE | AddDbaAgReplicaCommand.cs | OK | 100% | 25/25 unit pass | ShouldProcess, ConfirmImpact.Low, 20+ params, config-backed defaults, endpoint auto-creation, WSFC permissions, XE session config, Passthru, anchored regex (ReDoS fix), ValidateRange on BackupPriority/SessionTimeout, fixed PS1 $second.Name bug |
26-
| 17 | Disable-DbaAgHadr | PENDING | | | | | ShouldProcess required |
27-
| 18 | Enable-DbaAgHadr | PENDING | | | | | ShouldProcess required |
26+
| 17 | Disable-DbaAgHadr | DONE | DisableDbaAgHadrCommand.cs | OK | 100% | 1/1 param (common param diff expected); integration pre-existing infra issue | ShouldProcess, ConfirmImpact.High, WMI-based (DbaBaseCmdlet), static ScriptBlocks, Force restart, TestElevation |
27+
| 18 | Enable-DbaAgHadr | DONE | EnableDbaAgHadrCommand.cs | OK | 100% | 1/1 param (common param diff expected); integration pre-existing infra issue | ShouldProcess, ConfirmImpact.High, WMI-based (DbaBaseCmdlet), static ScriptBlocks, Force restart, TestElevation |
2828
| 19 | Remove-DbaAgDatabase | PENDING | | | | | ShouldProcess required, depends on Get-DbaAgDatabase |
2929
| 20 | Remove-DbaAgListener | PENDING | | | | | ShouldProcess required, depends on Get-DbaAgListener |
3030
| 21 | Remove-DbaAgReplica | PENDING | | | | | ShouldProcess required, depends on Get-DbaAgReplica |
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
using System;
2+
using System.Collections.ObjectModel;
3+
using System.Management.Automation;
4+
using Dataplat.Dbatools.Message;
5+
using Dataplat.Dbatools.Parameter;
6+
7+
namespace Dataplat.Dbatools.Commands
8+
{
9+
/// <summary>
10+
/// Disables the High Availability Disaster Recovery (HADR) service setting on SQL Server instances.
11+
/// Changes the WMI setting but requires a service restart to take effect. Use -Force to automatically restart.
12+
/// </summary>
13+
[Cmdlet("Disable", "DbaAgHadr", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
14+
public class DisableDbaAgHadrCommand : DbaBaseCmdlet
15+
{
16+
#region Parameters
17+
18+
/// <summary>
19+
/// The target SQL Server instance or instances.
20+
/// </summary>
21+
[Parameter(Mandatory = true, ValueFromPipeline = true)]
22+
public DbaInstanceParameter[] SqlInstance { get; set; }
23+
24+
/// <summary>
25+
/// Windows credential object used to connect to the target server as a different user.
26+
/// </summary>
27+
[Parameter()]
28+
public PSCredential Credential { get; set; }
29+
30+
/// <summary>
31+
/// Automatically restarts both SQL Server and SQL Server Agent services to immediately apply the HADR setting change.
32+
/// Without this switch, the HADR disable setting is changed but requires manual service restart to take effect.
33+
/// </summary>
34+
[Parameter()]
35+
public SwitchParameter Force { get; set; }
36+
37+
#endregion Parameters
38+
39+
#region Static ScriptBlocks
40+
41+
/// <summary>
42+
/// Tests whether the console has elevation when targeting localhost.
43+
/// </summary>
44+
private static readonly ScriptBlock _testElevationScript = ScriptBlock.Create(@"
45+
param($instance)
46+
Test-ElevationRequirement -ComputerName $instance
47+
");
48+
49+
/// <summary>
50+
/// Gets the current HADR state via WMI.
51+
/// </summary>
52+
private static readonly ScriptBlock _getWmiHadrScript = ScriptBlock.Create(@"
53+
param($instance, $credential, $hasCred)
54+
$params = @{ SqlInstance = $instance }
55+
if ($hasCred) { $params['Credential'] = $credential }
56+
Get-WmiHadr @params
57+
");
58+
59+
/// <summary>
60+
/// Disables HADR via Invoke-ManagedComputerCommand (sets ChangeHadrServiceSetting to 0).
61+
/// </summary>
62+
private static readonly ScriptBlock _disableHadrScript = ScriptBlock.Create(@"
63+
param($computerFullName, $credential, $hasCred, $instanceName)
64+
$scriptBlock = {
65+
$instance = $args[0]
66+
$sqlService = $wmi.Services | Where-Object DisplayName -eq ""SQL Server ($instance)""
67+
$sqlService.ChangeHadrServiceSetting(0)
68+
}
69+
$params = @{
70+
ComputerName = $computerFullName
71+
ScriptBlock = $scriptBlock
72+
ArgumentList = $instanceName
73+
}
74+
if ($hasCred) { $params['Credential'] = $credential }
75+
Invoke-ManagedComputerCommand @params
76+
");
77+
78+
/// <summary>
79+
/// Stops SQL Server Agent and Engine services.
80+
/// </summary>
81+
private static readonly ScriptBlock _stopServicesScript = ScriptBlock.Create(@"
82+
param($computerFullName, $instanceName)
83+
$null = Stop-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine
84+
");
85+
86+
/// <summary>
87+
/// Starts SQL Server Agent and Engine services.
88+
/// </summary>
89+
private static readonly ScriptBlock _startServicesScript = ScriptBlock.Create(@"
90+
param($computerFullName, $instanceName)
91+
$null = Start-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine
92+
");
93+
94+
#endregion Static ScriptBlocks
95+
96+
/// <summary>
97+
/// Overrides ConfirmPreference when Force is specified.
98+
/// </summary>
99+
protected override void BeginProcessing()
100+
{
101+
base.BeginProcessing();
102+
103+
if (Force.IsPresent)
104+
{
105+
// Match PS1: if ($Force) { $ConfirmPreference = 'none' }
106+
SessionState.PSVariable.Set("ConfirmPreference", "None");
107+
}
108+
}
109+
110+
/// <summary>
111+
/// Processes each SQL Server instance to disable the HADR setting.
112+
/// </summary>
113+
protected override void ProcessRecord()
114+
{
115+
if (TestFunctionInterrupt())
116+
return;
117+
118+
foreach (DbaInstanceParameter instance in SqlInstance)
119+
{
120+
ProcessInstance(instance);
121+
}
122+
}
123+
124+
/// <summary>
125+
/// Processes a single instance to disable HADR.
126+
/// </summary>
127+
private void ProcessInstance(DbaInstanceParameter instance)
128+
{
129+
string computer = instance.ComputerName;
130+
string instanceName = instance.InstanceName;
131+
132+
// Test elevation requirement for localhost
133+
if (!TestElevation(instance))
134+
{
135+
return;
136+
}
137+
138+
// Check current HADR state
139+
PSObject currentState;
140+
try
141+
{
142+
WriteMessageVerbose(String.Format("Checking current Hadr setting for {0}", computer));
143+
currentState = GetWmiHadr(instance);
144+
}
145+
catch (Exception ex)
146+
{
147+
StopFunction(
148+
String.Format("Failure to pull current state of Hadr setting on {0}", computer),
149+
errorRecord: new ErrorRecord(ex, "DisableDbaAgHadr_GetState", ErrorCategory.ConnectionError, instance),
150+
target: instance, isContinue: true, category: ErrorCategory.ConnectionError);
151+
TestFunctionInterrupt();
152+
return;
153+
}
154+
155+
string isHadrEnabled = currentState != null
156+
? (GetPropertyString(currentState, "IsHadrEnabled") ?? "Unknown")
157+
: "Unknown";
158+
WriteMessageAtLevel(
159+
String.Format("{0} Hadr current value: {1}", instance, isHadrEnabled),
160+
MessageLevel.InternalComment, null);
161+
162+
// Disable HADR via WMI
163+
if (ShouldProcess(instance.ToString(),
164+
String.Format("Changing Hadr from {0} to 0 for {1}", isHadrEnabled, instance)))
165+
{
166+
try
167+
{
168+
InvokeCommand.InvokeScript(false, _disableHadrScript, null,
169+
new object[] { computer, Credential, Credential != null, instanceName });
170+
}
171+
catch (Exception ex)
172+
{
173+
StopFunction(
174+
String.Format("Failure on {0} | This may be because AlwaysOn Availability Groups feature requires " +
175+
"the x86(non-WOW) or x64 Enterprise Edition of SQL Server 2012 (or later version) running on " +
176+
"Windows Server 2008 (or later version) with WSFC hotfix KB 2494036 installed.", instance.FullName),
177+
errorRecord: new ErrorRecord(ex, "DisableDbaAgHadr_DisableHadr", ErrorCategory.InvalidOperation, instance),
178+
target: instance, isContinue: true);
179+
TestFunctionInterrupt();
180+
return;
181+
}
182+
}
183+
184+
// Force restart services
185+
if (TestBound("Force"))
186+
{
187+
if (ShouldProcess(instance.ToString(),
188+
String.Format("Force provided, restarting Engine and Agent service for {0} on {1}", instance, computer)))
189+
{
190+
try
191+
{
192+
InvokeCommand.InvokeScript(false, _stopServicesScript, null,
193+
new object[] { computer, instanceName });
194+
InvokeCommand.InvokeScript(false, _startServicesScript, null,
195+
new object[] { computer, instanceName });
196+
}
197+
catch (Exception ex)
198+
{
199+
StopFunction(
200+
String.Format("Issue restarting {0}", instance),
201+
errorRecord: new ErrorRecord(ex, "DisableDbaAgHadr_Restart", ErrorCategory.InvalidOperation, instance),
202+
target: instance, isContinue: true);
203+
TestFunctionInterrupt();
204+
return;
205+
}
206+
}
207+
}
208+
209+
// Get new state
210+
PSObject newState = null;
211+
try
212+
{
213+
newState = GetWmiHadr(instance);
214+
}
215+
catch (Exception)
216+
{
217+
WriteMessageWarning(String.Format("Could not retrieve updated HADR state for {0}. The change may still be pending a service restart.", instance));
218+
}
219+
220+
// Warn if restart is needed
221+
if (TestBoundNot("Force"))
222+
{
223+
WriteMessageWarning("You must restart the SQL Server for it to take effect.");
224+
}
225+
226+
// Output result
227+
PSObject output = new PSObject();
228+
string compName = newState != null ? GetPropertyString(newState, "ComputerName") : computer;
229+
string instName = newState != null ? GetPropertyString(newState, "InstanceName") : instanceName;
230+
string sqlInst = newState != null ? GetPropertyString(newState, "SqlInstance") : instance.FullName;
231+
232+
output.Properties.Add(new PSNoteProperty("ComputerName", compName));
233+
output.Properties.Add(new PSNoteProperty("InstanceName", instName));
234+
output.Properties.Add(new PSNoteProperty("SqlInstance", sqlInst));
235+
output.Properties.Add(new PSNoteProperty("IsHadrEnabled", false));
236+
WriteObject(output);
237+
}
238+
239+
#region Helpers
240+
241+
/// <summary>
242+
/// Tests elevation requirement for the given instance.
243+
/// Returns false if elevation is required but not available.
244+
/// </summary>
245+
private bool TestElevation(DbaInstanceParameter instance)
246+
{
247+
try
248+
{
249+
Collection<PSObject> results = InvokeCommand.InvokeScript(
250+
false, _testElevationScript, null,
251+
new object[] { instance });
252+
253+
// If no result returned, assume check passed (remote host)
254+
if (results == null || results.Count == 0 || results[0] == null)
255+
return true;
256+
257+
object val = results[0].BaseObject ?? results[0];
258+
if (val is bool boolVal)
259+
return boolVal;
260+
261+
// Non-bool result: treat as pass
262+
return true;
263+
}
264+
catch (Exception)
265+
{
266+
// Test-ElevationRequirement calls Stop-Function internally when not elevated,
267+
// which throws. The error is already reported to the user via that path.
268+
}
269+
return false;
270+
}
271+
272+
/// <summary>
273+
/// Gets the current HADR state from WMI.
274+
/// </summary>
275+
private PSObject GetWmiHadr(DbaInstanceParameter instance)
276+
{
277+
Collection<PSObject> results = InvokeCommand.InvokeScript(
278+
false, _getWmiHadrScript, null,
279+
new object[] { instance, Credential, Credential != null });
280+
281+
if (results != null && results.Count > 0 && results[0] != null)
282+
return results[0] as PSObject ?? PSObject.AsPSObject(results[0]);
283+
return null;
284+
}
285+
286+
/// <summary>
287+
/// Gets a string property value from a PSObject.
288+
/// </summary>
289+
internal static string GetPropertyString(PSObject obj, string propertyName)
290+
{
291+
if (obj == null)
292+
return null;
293+
try
294+
{
295+
PSPropertyInfo prop = obj.Properties[propertyName];
296+
if (prop != null && prop.Value != null)
297+
return prop.Value.ToString();
298+
}
299+
catch (Exception)
300+
{
301+
// Property may not exist
302+
}
303+
return null;
304+
}
305+
306+
#endregion Helpers
307+
}
308+
}

0 commit comments

Comments
 (0)