|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Collections.ObjectModel; |
| 4 | +using System.Management.Automation; |
| 5 | +using Dataplat.Dbatools.Message; |
| 6 | +using Dataplat.Dbatools.Parameter; |
| 7 | + |
| 8 | +namespace Dataplat.Dbatools.Commands |
| 9 | +{ |
| 10 | + /// <summary> |
| 11 | + /// Removes databases from availability groups on SQL Server instances. |
| 12 | + /// This stops replication and high availability protection for those databases while preserving |
| 13 | + /// the actual database files on each replica. Supports pipeline input from Get-DbaAgDatabase. |
| 14 | + /// </summary> |
| 15 | + [Cmdlet("Remove", "DbaAgDatabase", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] |
| 16 | + public class RemoveDbaAgDatabaseCommand : DbaBaseCmdlet |
| 17 | + { |
| 18 | + #region Parameters |
| 19 | + |
| 20 | + /// <summary> |
| 21 | + /// The target SQL Server instance or instances. Server version must be SQL Server version 2012 or higher. |
| 22 | + /// </summary> |
| 23 | + [Parameter()] |
| 24 | + public DbaInstanceParameter[] SqlInstance { get; set; } |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// Login to the target instance using alternative credentials. Accepts PowerShell credentials (Get-Credential). |
| 28 | + /// Windows Authentication, SQL Server Authentication, Active Directory - Password, and Active Directory - Integrated are all supported. |
| 29 | + /// For MFA support, please use Connect-DbaInstance. |
| 30 | + /// </summary> |
| 31 | + [Parameter()] |
| 32 | + public PSCredential SqlCredential { get; set; } |
| 33 | + |
| 34 | + /// <summary> |
| 35 | + /// Specifies which databases to remove from their availability groups. Accepts multiple database names as an array. |
| 36 | + /// Required when using the SqlInstance parameter. |
| 37 | + /// </summary> |
| 38 | + [Parameter()] |
| 39 | + public string[] Database { get; set; } |
| 40 | + |
| 41 | + /// <summary> |
| 42 | + /// Limits the operation to databases within specific availability groups. When specified, only databases |
| 43 | + /// belonging to these availability groups will be removed. |
| 44 | + /// </summary> |
| 45 | + [Parameter()] |
| 46 | + public string[] AvailabilityGroup { get; set; } |
| 47 | + |
| 48 | + /// <summary> |
| 49 | + /// Accepts availability group database objects from Get-DbaAgDatabase or database objects from Get-DbaDatabase |
| 50 | + /// through the pipeline. This enables efficient batch operations and complex filtering scenarios. |
| 51 | + /// </summary> |
| 52 | + [Parameter(ValueFromPipeline = true)] |
| 53 | + public object[] InputObject { get; set; } |
| 54 | + |
| 55 | + #endregion Parameters |
| 56 | + |
| 57 | + #region Static ScriptBlocks |
| 58 | + |
| 59 | + /// <summary> |
| 60 | + /// Calls Get-DbaAgDatabase with appropriate parameters. |
| 61 | + /// </summary> |
| 62 | + private static readonly ScriptBlock _getDbaAgDatabaseScript = ScriptBlock.Create(@" |
| 63 | +param($si, $sc, $db, $ag, $hasCred, $hasDb, $hasAg) |
| 64 | +$params = @{ SqlInstance = $si } |
| 65 | +if ($hasCred) { $params['SqlCredential'] = $sc } |
| 66 | +if ($hasDb) { $params['Database'] = $db } |
| 67 | +if ($hasAg) { $params['AvailabilityGroup'] = $ag } |
| 68 | +Get-DbaAgDatabase @params |
| 69 | +"); |
| 70 | + |
| 71 | + /// <summary> |
| 72 | + /// Gets the server name from the database's AG parent hierarchy. |
| 73 | + /// AvailabilityDatabase.Parent = AvailabilityGroup, AvailabilityGroup.Parent = Server. |
| 74 | + /// </summary> |
| 75 | + private static readonly ScriptBlock _getServerNameScript = ScriptBlock.Create(@" |
| 76 | +param($db) $db.Parent.Parent.Name |
| 77 | +"); |
| 78 | + |
| 79 | + /// <summary> |
| 80 | + /// Drops the database from the availability group and returns the AG name. |
| 81 | + /// Uses the Parent.AvailabilityDatabases collection to ensure correct object reference. |
| 82 | + /// </summary> |
| 83 | + private static readonly ScriptBlock _dropAgDatabaseScript = ScriptBlock.Create(@" |
| 84 | +param($db) |
| 85 | +$agName = $db.Parent.Name |
| 86 | +$db.Parent.AvailabilityDatabases[$db.Name].Drop() |
| 87 | +$agName |
| 88 | +"); |
| 89 | + |
| 90 | + #endregion Static ScriptBlocks |
| 91 | + |
| 92 | + /// <summary> |
| 93 | + /// Processes each pipeline item or SqlInstance-based query to remove databases from availability groups. |
| 94 | + /// </summary> |
| 95 | + protected override void ProcessRecord() |
| 96 | + { |
| 97 | + if (TestFunctionInterrupt()) |
| 98 | + return; |
| 99 | + |
| 100 | + // Validate: need SqlInstance or InputObject |
| 101 | + if (TestBoundNot("SqlInstance", "InputObject")) |
| 102 | + { |
| 103 | + StopFunction("You must supply either -SqlInstance or an Input Object"); |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + // Validate: SqlInstance requires Database |
| 108 | + if (TestBound("SqlInstance") && TestBoundNot("Database")) |
| 109 | + { |
| 110 | + StopFunction("You must specify one or more databases and one or more Availability Groups when using the SqlInstance parameter."); |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + // Handle Database-type InputObject: extract names to Database array |
| 115 | + // PS1: if ($InputObject[0].GetType().Name -eq 'Database') { $Database += $InputObject.Name } |
| 116 | + if (InputObject != null && InputObject.Length > 0) |
| 117 | + { |
| 118 | + string typeName = GetBaseTypeName(InputObject[0]); |
| 119 | + if (string.Equals(typeName, "Database", StringComparison.OrdinalIgnoreCase)) |
| 120 | + { |
| 121 | + List<string> dbNames = new List<string>(); |
| 122 | + if (Database != null) |
| 123 | + { |
| 124 | + dbNames.AddRange(Database); |
| 125 | + } |
| 126 | + foreach (object obj in InputObject) |
| 127 | + { |
| 128 | + string name = GetPropertyString(PSObject.AsPSObject(obj), "Name"); |
| 129 | + if (name != null) |
| 130 | + { |
| 131 | + dbNames.Add(name); |
| 132 | + } |
| 133 | + } |
| 134 | + Database = dbNames.ToArray(); |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + // If SqlInstance provided, fetch AG databases and add to InputObject |
| 139 | + // PS1: $InputObject += Get-DbaAgDatabase -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $Database |
| 140 | + if (TestBound("SqlInstance")) |
| 141 | + { |
| 142 | + List<object> items = new List<object>(); |
| 143 | + if (InputObject != null) |
| 144 | + { |
| 145 | + items.AddRange(InputObject); |
| 146 | + } |
| 147 | + |
| 148 | + Collection<PSObject> agDbs = GetDbaAgDatabase(); |
| 149 | + if (agDbs != null) |
| 150 | + { |
| 151 | + foreach (PSObject db in agDbs) |
| 152 | + { |
| 153 | + if (db != null) |
| 154 | + { |
| 155 | + items.Add(db.BaseObject ?? db); |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + InputObject = items.ToArray(); |
| 160 | + } |
| 161 | + |
| 162 | + if (InputObject == null) |
| 163 | + return; |
| 164 | + |
| 165 | + foreach (object dbObj in InputObject) |
| 166 | + { |
| 167 | + if (dbObj == null) |
| 168 | + continue; |
| 169 | + ProcessDatabase(dbObj); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + /// <summary> |
| 174 | + /// Processes a single database object: validates via ShouldProcess, drops from AG, and outputs result. |
| 175 | + /// </summary> |
| 176 | + private void ProcessDatabase(object dbObj) |
| 177 | + { |
| 178 | + PSObject db = PSObject.AsPSObject(dbObj); |
| 179 | + string dbName = GetPropertyString(db, "Name"); |
| 180 | + string serverName = GetServerName(dbObj); |
| 181 | + |
| 182 | + // PS1: if ($Pscmdlet.ShouldProcess($db.Parent.Parent.Name, "Removing availability group database $db")) |
| 183 | + if (ShouldProcess(serverName ?? dbName ?? "Unknown", |
| 184 | + String.Format("Removing availability group database {0}", dbName ?? "Unknown"))) |
| 185 | + { |
| 186 | + try |
| 187 | + { |
| 188 | + string agName = DropAgDatabase(dbObj); |
| 189 | + |
| 190 | + PSObject output = new PSObject(); |
| 191 | + output.Properties.Add(new PSNoteProperty("ComputerName", GetPropertyString(db, "ComputerName"))); |
| 192 | + output.Properties.Add(new PSNoteProperty("InstanceName", GetPropertyString(db, "InstanceName"))); |
| 193 | + output.Properties.Add(new PSNoteProperty("SqlInstance", GetPropertyString(db, "SqlInstance"))); |
| 194 | + output.Properties.Add(new PSNoteProperty("AvailabilityGroup", agName)); |
| 195 | + output.Properties.Add(new PSNoteProperty("Database", dbName)); |
| 196 | + output.Properties.Add(new PSNoteProperty("Status", "Removed")); |
| 197 | + WriteObject(output); |
| 198 | + } |
| 199 | + catch (Exception ex) |
| 200 | + { |
| 201 | + StopFunction( |
| 202 | + String.Format("Failed to remove {0} from availability group", dbName), |
| 203 | + errorRecord: new ErrorRecord(ex, "RemoveDbaAgDatabase", ErrorCategory.InvalidOperation, dbObj), |
| 204 | + target: dbObj, isContinue: true); |
| 205 | + TestFunctionInterrupt(); |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + #region Helpers |
| 211 | + |
| 212 | + /// <summary> |
| 213 | + /// Gets the simple type name of an object's base type, unwrapping PSObject if necessary. |
| 214 | + /// </summary> |
| 215 | + private static string GetBaseTypeName(object obj) |
| 216 | + { |
| 217 | + if (obj == null) |
| 218 | + return null; |
| 219 | + object baseObj = obj; |
| 220 | + if (obj is PSObject pso && pso.BaseObject != null) |
| 221 | + baseObj = pso.BaseObject; |
| 222 | + return baseObj.GetType().Name; |
| 223 | + } |
| 224 | + |
| 225 | + /// <summary> |
| 226 | + /// Gets the server name from the AG database's parent hierarchy via ScriptBlock. |
| 227 | + /// </summary> |
| 228 | + private string GetServerName(object dbObj) |
| 229 | + { |
| 230 | + try |
| 231 | + { |
| 232 | + Collection<PSObject> results = InvokeCommand.InvokeScript( |
| 233 | + false, _getServerNameScript, null, new object[] { dbObj }); |
| 234 | + if (results != null && results.Count > 0 && results[0] != null) |
| 235 | + return results[0].BaseObject as string ?? results[0].ToString(); |
| 236 | + } |
| 237 | + catch (Exception ex) |
| 238 | + { |
| 239 | + WriteMessageAtLevel( |
| 240 | + String.Format("Could not resolve server name from object hierarchy: {0}", ex.Message), |
| 241 | + MessageLevel.Debug, null); |
| 242 | + } |
| 243 | + return null; |
| 244 | + } |
| 245 | + |
| 246 | + /// <summary> |
| 247 | + /// Drops the database from the availability group and returns the AG name. |
| 248 | + /// </summary> |
| 249 | + private string DropAgDatabase(object dbObj) |
| 250 | + { |
| 251 | + Collection<PSObject> results = InvokeCommand.InvokeScript( |
| 252 | + false, _dropAgDatabaseScript, null, new object[] { dbObj }); |
| 253 | + if (results != null && results.Count > 0 && results[0] != null) |
| 254 | + return results[0].BaseObject as string ?? results[0].ToString(); |
| 255 | + return null; |
| 256 | + } |
| 257 | + |
| 258 | + /// <summary> |
| 259 | + /// Calls Get-DbaAgDatabase with the current SqlInstance, SqlCredential, Database, and AvailabilityGroup parameters. |
| 260 | + /// </summary> |
| 261 | + private Collection<PSObject> GetDbaAgDatabase() |
| 262 | + { |
| 263 | + try |
| 264 | + { |
| 265 | + return InvokeCommand.InvokeScript(false, _getDbaAgDatabaseScript, null, |
| 266 | + new object[] |
| 267 | + { |
| 268 | + SqlInstance, |
| 269 | + SqlCredential, |
| 270 | + Database, |
| 271 | + AvailabilityGroup, |
| 272 | + SqlCredential != null, |
| 273 | + Database != null && Database.Length > 0, |
| 274 | + TestBound("AvailabilityGroup") |
| 275 | + }); |
| 276 | + } |
| 277 | + catch (Exception ex) |
| 278 | + { |
| 279 | + StopFunction( |
| 280 | + "Failed to get availability group databases", |
| 281 | + errorRecord: new ErrorRecord(ex, "RemoveDbaAgDatabase_GetAgDb", ErrorCategory.ConnectionError, SqlInstance), |
| 282 | + target: SqlInstance, isContinue: true); |
| 283 | + TestFunctionInterrupt(); |
| 284 | + return null; |
| 285 | + } |
| 286 | + } |
| 287 | + |
| 288 | + /// <summary> |
| 289 | + /// Gets a string property value from a PSObject. |
| 290 | + /// </summary> |
| 291 | + internal static string GetPropertyString(PSObject obj, string propertyName) |
| 292 | + { |
| 293 | + if (obj == null) |
| 294 | + return null; |
| 295 | + try |
| 296 | + { |
| 297 | + PSPropertyInfo prop = obj.Properties[propertyName]; |
| 298 | + if (prop != null && prop.Value != null) |
| 299 | + return prop.Value.ToString(); |
| 300 | + } |
| 301 | + catch (Exception) |
| 302 | + { |
| 303 | + // Property may not exist |
| 304 | + } |
| 305 | + return null; |
| 306 | + } |
| 307 | + |
| 308 | + #endregion Helpers |
| 309 | + } |
| 310 | +} |
0 commit comments