From a6e79c50765e465b5bcbaac6f56e3e9efeacfeb0 Mon Sep 17 00:00:00 2001 From: Dan Reist Date: Fri, 27 Oct 2017 08:31:18 -0400 Subject: [PATCH 1/3] Updated to be cluster aware. --- CHANGELOG.md | 4 +++ ...OnAvailabilityGroupDatabaseMembership.psm1 | 33 +++++++++++++++++-- ...labilityGroupDatabaseMembership.schema.mof | 2 ++ ...bilityGroupDatabaseMembership.strings.psd1 | 1 + ...ailabilityGroupDatabaseMembership.help.txt | 3 ++ README.md | 8 +++++ ...ilabilityGroupDatabaseMembership.Tests.ps1 | 25 ++++++++++++++ 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c55042..2a257ca1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ - Made the resource cluster aware. When ProcessOnlyOnActiveNode is specified, the resource will only determine if a change is needed if the target node is the active host of the SQL Server instance ([issue #868](https://github.com/PowerShell/xSQLServer/issues/868)). +- Changes to xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership + - Made the resource cluster aware. When ProcessOnlyOnActiveNode is specified, + the resource will only determine if a change is needed if the target node + is the active host of the SQL Server instance ([issue #869](https://github.com/PowerShell/xSQLServer/issues/869)). - Changes to xSQLServerAlwaysOnAvailabilityGroupReplica - Made the resource cluster aware. When ProcessOnlyOnActiveNode is specified, the resource will only determine if a change is needed if the target node is diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.psm1 b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.psm1 index ccd54697d..5e4cc5283 100644 --- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.psm1 +++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.psm1 @@ -68,11 +68,15 @@ function Get-TargetResource Ensure = '' Force = $false MatchDatabaseOwner = $false + IsActiveNode = $false } # Connect to the instance $serverObject = Connect-SQL -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName + # Is this node actively hosting the SQL instance? + $currentConfiguration.IsActiveNode = Test-ActiveNode -ServerObject $serverObject + # Get the Availability group object $availabilityGroup = $serverObject.AvailabilityGroups[$AvailabilityGroupName] @@ -139,6 +143,10 @@ function Get-TargetResource If set to $false, the owner of the database will be the PSDscRunAsCredential. The default is '$true'. + + .PARAMETER ProcessOnlyOnActiveNode + Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. + Not used in Set-TargetResource. #> function Set-TargetResource { @@ -176,7 +184,11 @@ function Set-TargetResource [Parameter()] [Boolean] - $MatchDatabaseOwner + $MatchDatabaseOwner, + + [Parameter()] + [Boolean] + $ProcessOnlyOnActiveNode ) Import-SQLPSModule @@ -597,6 +609,9 @@ function Set-TargetResource If set to $false, the owner of the database will be the PSDscRunAsCredential. The default is '$true'. + + .PARAMETER ProcessOnlyOnActiveNode + Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. #> function Test-TargetResource { @@ -635,7 +650,11 @@ function Test-TargetResource [Parameter()] [Boolean] - $MatchDatabaseOwner + $MatchDatabaseOwner, + + [Parameter()] + [Boolean] + $ProcessOnlyOnActiveNode ) $configurationInDesiredState = $true @@ -649,6 +668,16 @@ function Test-TargetResource } $currentConfiguration = Get-TargetResource @getTargetResourceParameters + <# + If this is supposed to process only the active node, and this is not the + active node, don't bother evaluating the test. + #> + if ( $ProcessOnlyOnActiveNode -and -not $currentConfiguration.IsActiveNode ) + { + Write-Verbose -Message ( $script:localizedData.NotActiveNode -f $env:COMPUTERNAME,$SQLInstanceName ) + return $configurationInDesiredState + } + # Connect to the defined instance $serverObject = Connect-SQL -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.schema.mof b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.schema.mof index e55bdabfa..0df29e907 100644 --- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.schema.mof +++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.schema.mof @@ -9,4 +9,6 @@ class MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership : OMI_BaseResou [Write, Description("Specifies the membership of the database(s) in the availability group. The options are: Present: The defined database(s) are added to the availability group. All other databases that may be a member of the availability group are ignored. Absent: The defined database(s) are removed from the availability group. All other databases that may be a member of the availability group are ignored. The default is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Write, Description("When used with 'Ensure = 'Present'' it ensures the specified database(s) are the only databases that are a member of the specified Availability Group. This parameter is ignored when 'Ensure' is 'Absent'.")] Boolean Force; [Write, Description("If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available as a login on all replicas and that the PSDscRunAsAccount has impersonate permissions. If set to $false, the owner of the database will be the PSDscRunAsAccount. The default is '$true'")] Boolean MatchDatabaseOwner; + [Write, Description("Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance.")] Boolean ProcessOnlyOnActiveNode; + [Read, Description("Determines if the current node is actively hosting the SQL Server instance.")] Boolean IsActiveNode; }; diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 index 114b3727f..052e8ea5b 100644 --- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 +++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 @@ -8,6 +8,7 @@ ConvertFrom-StringData @' DatabaseShouldNotBeMember = The following databases should not be a member of the availability group '{0}': {1}. DatabasesNotFound = The following databases were not found in the instance: {0}. ImpersonatePermissionsMissing = The login '{0}' is missing impersonate permissions in the instances '{1}'. + NotActiveNode = The node "{0}" is not actively hosting the instance "{1}". Exiting the test. ParameterNotOfType = The parameter '{0}' is not of the type '{1}'. ParameterNullOrEmpty = The parameter '{0}' is NULL or empty. RemovingDatabasesToAvailabilityGroup = Removing the following databases from the '{0}' availability group: {1}. diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/about_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.help.txt b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/about_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.help.txt index 36968209b..eaab5b464 100644 --- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/about_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.help.txt +++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/about_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.help.txt @@ -45,3 +45,6 @@ PARAMETER MatchDatabaseOwner If set to $false, the owner of the database will be the PSDscRunAsCredential. The default is '$true'. + +.PARAMETER ProcessOnlyOnActiveNode + Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. diff --git a/README.md b/README.md index c647d64e6..7034f1e58 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,14 @@ group. login on all replicas and that the PSDscRunAsAccount has impersonate permissions. If set to $false, the owner of the database will be the PSDscRunAsAccount. The default is '$true'. +* **`[Boolean]` ProcessOnlyOnActiveNode** _(Write)_: Specifies that the resource + will only determine if a change is needed if the target node is the active + host of the SQL Server Instance. + +#### Read-Only Properties from Get-TargetResource + +* **`[Boolean]` IsActiveNode** _(Read)_: Determines if the current node is + actively hosting the SQL Server instance. ### xSQLServerAlwaysOnAvailabilityGroupReplica diff --git a/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.Tests.ps1 b/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.Tests.ps1 index 435fc1231..95e108627 100644 --- a/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.Tests.ps1 +++ b/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.Tests.ps1 @@ -56,6 +56,8 @@ try $mockBackupPath = 'X:\Backup' + $mockProcessOnlyOnActiveNode = $false + #endregion Parameter Mocks #region mock names @@ -1140,6 +1142,9 @@ WITH NORECOVERY' Mock -CommandName Connect-SQL -MockWith { return $mockServerObject } -Verifiable Mock -CommandName Get-PrimaryReplicaServerObject -MockWith { return $mockServerObject } -Verifiable -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Mock -CommandName Get-PrimaryReplicaServerObject -MockWith { return $mockServer2Object } -Verifiable -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Mock -CommandName Test-ActiveNode -MockWith { + return -not $mockProcessOnlyOnActiveNode + } -Verifiable } BeforeEach { @@ -1152,6 +1157,7 @@ WITH NORECOVERY' Ensure = 'Present' Force = $false MatchDatabaseOwner = $false + ProcessOnlyOnActiveNode = $false } } @@ -1164,6 +1170,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 2 -Exactly Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 1 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly } It 'Should return $false when the specified availability group is not found' { @@ -1174,6 +1181,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 2 -Exactly Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly } It 'Should return $false when no matching databases are found' { @@ -1184,6 +1192,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 2 -Exactly Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 1 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly } It 'Should return $false when databases are found to add to the availability group' { @@ -1192,6 +1201,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 2 -Exactly Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 1 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly } It 'Should return $true when the configuration is in the desired state and the primary replica is on another server' { @@ -1203,6 +1213,21 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 2 -Exactly Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 1 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly + } + + It 'Should return $true when ProcessOnlyOnActiveNode is "$true" and the current node is not actively hosting the instance' { + $mockProcessOnlyOnActiveNode = $true + + $mockTestTargetResourceParameters.DatabaseName = $mockAvailabilityDatabaseNames.Clone() + $mockTestTargetResourceParameters.ProcessOnlyOnActiveNode = $mockProcessOnlyOnActiveNode + + Test-TargetResource @mockTestTargetResourceParameters | Should -Be $true + + Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server1' } + Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } + Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly } } From 44d20968ad7228de10f33ee1c5d2bc48e99fce1f Mon Sep 17 00:00:00 2001 From: Dan Reist Date: Fri, 27 Oct 2017 09:15:12 -0400 Subject: [PATCH 2/3] Updated the example to be cluster aware. --- .../1-AddDatabasesToAvailabilityGroup.ps1 | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/1-AddDatabasesToAvailabilityGroup.ps1 b/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/1-AddDatabasesToAvailabilityGroup.ps1 index e19b88445..b2869c583 100644 --- a/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/1-AddDatabasesToAvailabilityGroup.ps1 +++ b/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/1-AddDatabasesToAvailabilityGroup.ps1 @@ -1,6 +1,11 @@ <# .EXAMPLE This example shows how to ensure that the databases 'DB*' and 'AdventureWorks' are members in the Availability Group 'TestAG'. + + In the event this is applied to a Failover Cluster Instance (FCI), the + ProcessOnlyOnActiveNode property will tell the Test-TargetResource function + to evaluate if any changes are needed if the node is actively hosting the + SQL Server Instance. #> $ConfigurationData = @{ @@ -101,13 +106,14 @@ Configuration Example { xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership 'TestAGDatabaseMemberships' { - AvailabilityGroupName = $Node.AvailabilityGroupName - BackupPath = '\\SQL1\AgInitialize' - DatabaseName = 'DB*', 'AdventureWorks' - SQLInstanceName = $Node.SQLInstanceName - SQLServer = $Node.NodeName - Ensure = 'Present' - PsDscRunAsCredential = $SysAdminAccount + AvailabilityGroupName = $Node.AvailabilityGroupName + BackupPath = '\\SQL1\AgInitialize' + DatabaseName = 'DB*', 'AdventureWorks' + SQLInstanceName = $Node.SQLInstanceName + SQLServer = $Node.NodeName + Ensure = 'Present' + ProcessOnlyOnActiveNode = $true + PsDscRunAsCredential = $SysAdminAccount } } } From 8cf6be224a007cba849daf177cf1b21cf823d9ef Mon Sep 17 00:00:00 2001 From: Dan Reist Date: Fri, 27 Oct 2017 20:53:10 -0400 Subject: [PATCH 3/3] Changed to single quotes for consistency. --- ...rverAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 index 052e8ea5b..4a29519a9 100644 --- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 +++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership/en-US/MSFT_xSQLServerAlwaysOnAvailabilityGroupDatabaseMembership.strings.psd1 @@ -8,7 +8,7 @@ ConvertFrom-StringData @' DatabaseShouldNotBeMember = The following databases should not be a member of the availability group '{0}': {1}. DatabasesNotFound = The following databases were not found in the instance: {0}. ImpersonatePermissionsMissing = The login '{0}' is missing impersonate permissions in the instances '{1}'. - NotActiveNode = The node "{0}" is not actively hosting the instance "{1}". Exiting the test. + NotActiveNode = The node '{0}' is not actively hosting the instance '{1}'. Exiting the test. ParameterNotOfType = The parameter '{0}' is not of the type '{1}'. ParameterNullOrEmpty = The parameter '{0}' is NULL or empty. RemovingDatabasesToAvailabilityGroup = Removing the following databases from the '{0}' availability group: {1}.