From ce786852a984ea364e1213d936f647bc5ebe4a30 Mon Sep 17 00:00:00 2001 From: Fiander <51764122+Fiander@users.noreply.github.com> Date: Wed, 25 Nov 2020 14:45:52 +0100 Subject: [PATCH] BREAKING CHANGE: SqlWaitForAG: Evaluate so that Availability Group is available (#1636) - SqlWaitForAG - BREAKING CHANGE: Fix for issue (issue #1569) The resource now waits for the Availability Group to become Available. - Two parameters where added to test get and set resource at instance level. --- CHANGELOG.md | 4 + .../DSC_SqlWaitForAG/DSC_SqlWaitForAG.psm1 | 104 ++++++++++- .../DSC_SqlWaitForAG.schema.mof | 2 + .../DSCResources/DSC_SqlWaitForAG/README.md | 14 +- .../en-US/DSC_SqlWaitForAG.strings.psd1 | 2 + .../1-WaitForASingleClusterGroup.ps1 | 1 + .../2-WaitForMultipleClusterGroups.ps1 | 2 + tests/Unit/DSC_SqlWaitForAG.Tests.ps1 | 171 +++++++++++++++--- 8 files changed, 257 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68abe05ea..a86094dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- WaitForAG + - BREAKING CHANGE: Fix for issue ([issue #1569](https://github.com/dsccommunity/SqlServerDsc/issues/1569)) + The resource now waits for the Availability Group to become Available. + - Two parameters where added to test get and set resource at instance level. - SqlRole - Major overhaul of resource. - BREAKING CHANGE: Removed decision making from get-TargetResource; this diff --git a/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.psm1 b/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.psm1 index 787d6d6b1..3a5c25b14 100644 --- a/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.psm1 +++ b/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.psm1 @@ -11,6 +11,12 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' Returns the cluster role/group that is waiting to be created, along with the time and number of times to wait. + .PARAMETER ServerName + Hostname of the SQL Server to be configured. + + .PARAMETER InstanceName + Name of the SQL instance to be configured. + .PARAMETER Name Name of the cluster role/group to look for (normally the same as the Availability Group name). @@ -18,7 +24,8 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' .PARAMETER RetryIntervalSec The interval, in seconds, to check for the presence of the cluster role/group. Default value is 20 seconds. When the cluster role/group has been found the - resource will wait for this amount of time once more before returning. + resource will check if the AG group exist. When the availability group has + been found the resource will also wait this amount of time before returning. .PARAMETER RetryCount Maximum number of retries until the resource will timeout and throw an error. @@ -30,6 +37,16 @@ function Get-TargetResource [OutputType([System.Collections.Hashtable])] param ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName = $env:COMPUTERNAME, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InstanceName, + [Parameter(Mandatory = $true)] [System.String] $Name, @@ -49,14 +66,43 @@ function Get-TargetResource $clusterGroupFound = $false + # No ClusterName specified, so defaults to cluster on this node. $clusterGroup = Get-ClusterGroup -Name $Name -ErrorAction SilentlyContinue + if ($null -ne $clusterGroup) { Write-Verbose -Message ( $script:localizedData.FoundClusterGroup -f $Name ) - $clusterGroupFound = $true + # Connect to the instance + $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName + + if ($serverObject) + { + # Determine if HADR is enabled on the instance. If not, AG group can not exist. + if ($serverObject.IsHadrEnabled ) + { + $availabilityGroup = $serverObject.AvailabilityGroups[$Name] + + if ( $availabilityGroup ) + { + $clusterGroupFound = $true + } + else + { + Write-Verbose -Message ( + $script:localizedData.AGNotFound -f $name, $InstanceName, $RetryIntervalSec + ) + } + } + else + { + Write-Verbose -Message ( + $script:localizedData.HadrNotEnabled -f $InstanceName + ) + } + } } else { @@ -66,6 +112,8 @@ function Get-TargetResource } return @{ + ServerName = $ServerName + InstanceName = $InstanceName Name = $Name RetryIntervalSec = $RetryIntervalSec RetryCount = $RetryCount @@ -77,14 +125,22 @@ function Get-TargetResource .SYNOPSIS Waits for a cluster role/group to be created + .PARAMETER ServerName + Hostname of the SQL Server to be configured. + + .PARAMETER InstanceName + Name of the SQL instance to be configured. + .PARAMETER Name - Name of the cluster role/group to look for (normally the same as the Availability - Group name). + Name of the cluster role/group to look for (normally the same as the + Availability Group name). .PARAMETER RetryIntervalSec The interval, in seconds, to check for the presence of the cluster role/group. Default value is 20 seconds. When the cluster role/group has been found the - resource will wait for this amount of time once more before returning. + resource will check if the AG group exist. When the availability group has + been found the resource will also wait this amount of time before returning. + .PARAMETER RetryCount Maximum number of retries until the resource will timeout and throw an error. @@ -95,6 +151,16 @@ function Set-TargetResource [CmdletBinding()] param ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName = $env:COMPUTERNAME, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InstanceName, + [Parameter(Mandatory = $true)] [System.String] $Name, @@ -113,6 +179,8 @@ function Set-TargetResource ) $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName Name = $Name RetryIntervalSec = $RetryIntervalSec RetryCount = $RetryCount @@ -153,14 +221,22 @@ function Set-TargetResource .SYNOPSIS Tests if the cluster role/group has been created. + .PARAMETER ServerName + Hostname of the SQL Server to be configured. + + .PARAMETER InstanceName + Name of the SQL instance to be configured. + .PARAMETER Name - Name of the cluster role/group to look for (normally the same as the Availability - Group name). + Name of the cluster role/group to look for (normally the same as the + Availability Group name). .PARAMETER RetryIntervalSec The interval, in seconds, to check for the presence of the cluster role/group. Default value is 20 seconds. When the cluster role/group has been found the - resource will wait for this amount of time once more before returning. + resource will check if the AG group exist. When the availability group has + been found the resource will also wait this amount of time before returning. + .PARAMETER RetryCount Maximum number of retries until the resource will timeout and throw an error. @@ -172,6 +248,16 @@ function Test-TargetResource [OutputType([System.Boolean])] param ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName = $env:COMPUTERNAME, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InstanceName, + [Parameter(Mandatory = $true)] [System.String] $Name, @@ -190,6 +276,8 @@ function Test-TargetResource ) $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName Name = $Name RetryIntervalSec = $RetryIntervalSec RetryCount = $RetryCount diff --git a/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.schema.mof b/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.schema.mof index ebf1662ba..95281a6d1 100644 --- a/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.schema.mof +++ b/source/DSCResources/DSC_SqlWaitForAG/DSC_SqlWaitForAG.schema.mof @@ -1,6 +1,8 @@ [ClassVersion("1.0.0.0"), FriendlyName("SqlWaitForAG")] class DSC_SqlWaitForAG : OMI_BaseResource { + [Key, Description("The name of the _SQL Server_ instance to be configured.")] String InstanceName; + [Write, Description("The host name of the _SQL Server_ to be configured. Default value is `$env:COMPUTERNAME`.")] String ServerName; [Key, Description("Name of the cluster role/group to look for (normally the same as the _Availability Group_ name).")] String Name; [Write, Description("The interval, in seconds, to check for the presence of the cluster role/group. Default value is `20` seconds. When the cluster role/group has been found the resource will wait for this amount of time once more before returning.")] UInt64 RetryIntervalSec; [Write, Description("Maximum number of retries until the resource will timeout and throw an error. Default values is `30` times.")] UInt32 RetryCount; diff --git a/source/DSCResources/DSC_SqlWaitForAG/README.md b/source/DSCResources/DSC_SqlWaitForAG/README.md index a48095d9e..d0c52bc76 100644 --- a/source/DSCResources/DSC_SqlWaitForAG/README.md +++ b/source/DSCResources/DSC_SqlWaitForAG/README.md @@ -1,8 +1,9 @@ # Description The `SqlWaitForAG` DSC resource will wait for a cluster role/group to be -created. This is used to wait for an Availability Group to create the -cluster role/group in the cluster. +created. When the cluster group is found it will wait for the availability group to become available. +When the availability group has been found the resource will wait the amount of time specified +in the parameter RetryIntervalSec before returning. ## Requirements @@ -17,13 +18,4 @@ cluster role/group in the cluster. ## Known issues -* This resource evaluates if the Windows Failover Cluster role/group - has been created. But the Windows Failover Cluster role/group is created - before the Availability Group is in a ready state. When the Windows Failover - Cluster role/group is found the resource will wait one more time - according to the value of `RetryIntervalSec` before returning. There is - currently no check to validate that the Availability Group was successfully - created and is in a ready state. A workaround is instead use [`WaitForAny`](https://docs.microsoft.com/en-us/powershell/scripting/dsc/reference/resources/windows/waitforanyresource?view=powershell-7) - resource. This is being tracked in [issue #1569](https://github.com/dsccommunity/SqlServerDsc/issues/1569). - All issues are not listed here, see [here for all open issues](https://github.com/dsccommunity/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlWaitForAG). diff --git a/source/DSCResources/DSC_SqlWaitForAG/en-US/DSC_SqlWaitForAG.strings.psd1 b/source/DSCResources/DSC_SqlWaitForAG/en-US/DSC_SqlWaitForAG.strings.psd1 index 88bbfbf71..1e19df990 100644 --- a/source/DSCResources/DSC_SqlWaitForAG/en-US/DSC_SqlWaitForAG.strings.psd1 +++ b/source/DSCResources/DSC_SqlWaitForAG/en-US/DSC_SqlWaitForAG.strings.psd1 @@ -7,4 +7,6 @@ ConvertFrom-StringData @' RetryMessage = Will retry again after {0} seconds. FailedMessage = Did not find the cluster group '{0}' within the timeout period. TestingConfiguration = Determines the current state of the Always On Availability Group with the cluster group name '{0}'. + HadrNotEnabled = Hadr is not enabled on the sql server instance '{0}'. + AGNotFound = Availibility Group '{0}' not found on instance '{1}'. Waiting an other {2} seconds. '@ diff --git a/source/Examples/Resources/SqlWaitForAG/1-WaitForASingleClusterGroup.ps1 b/source/Examples/Resources/SqlWaitForAG/1-WaitForASingleClusterGroup.ps1 index 479bfeaf3..fa29bdd52 100644 --- a/source/Examples/Resources/SqlWaitForAG/1-WaitForASingleClusterGroup.ps1 +++ b/source/Examples/Resources/SqlWaitForAG/1-WaitForASingleClusterGroup.ps1 @@ -20,6 +20,7 @@ Configuration Example Name = 'AGTest1' RetryIntervalSec = 20 RetryCount = 30 + InstanceName = 'MSSQLSERVER' PsDscRunAsCredential = $SqlAdministratorCredential } diff --git a/source/Examples/Resources/SqlWaitForAG/2-WaitForMultipleClusterGroups.ps1 b/source/Examples/Resources/SqlWaitForAG/2-WaitForMultipleClusterGroups.ps1 index 727709dc2..402d01422 100644 --- a/source/Examples/Resources/SqlWaitForAG/2-WaitForMultipleClusterGroups.ps1 +++ b/source/Examples/Resources/SqlWaitForAG/2-WaitForMultipleClusterGroups.ps1 @@ -20,6 +20,7 @@ Configuration Example Name = 'AGTest1' RetryIntervalSec = 20 RetryCount = 30 + InstanceName = 'MSSQLSERVER' PsDscRunAsCredential = $SqlAdministratorCredential } @@ -29,6 +30,7 @@ Configuration Example Name = 'AGTest2' RetryIntervalSec = 20 RetryCount = 30 + InstanceName = 'MSSQLSERVER' PsDscRunAsCredential = $SqlAdministratorCredential } diff --git a/tests/Unit/DSC_SqlWaitForAG.Tests.ps1 b/tests/Unit/DSC_SqlWaitForAG.Tests.ps1 index 869b891de..f5d563b42 100644 --- a/tests/Unit/DSC_SqlWaitForAG.Tests.ps1 +++ b/tests/Unit/DSC_SqlWaitForAG.Tests.ps1 @@ -1,7 +1,6 @@ <# .SYNOPSIS Automated unit test for DSC_SqlWaitForAG DSC resource. - #> Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') @@ -11,8 +10,8 @@ if (-not (Test-BuildCategory -Type 'Unit')) return } -$script:dscModuleName = 'SqlServerDsc' -$script:dscResourceName = 'DSC_SqlWaitForAG' +$script:dscModuleName = 'SqlServerDsc' +$script:dscResourceName = 'DSC_SqlWaitForAG' function Invoke-TestSetup { @@ -30,6 +29,12 @@ function Invoke-TestSetup -DSCResourceName $script:dscResourceName ` -ResourceType 'Mof' ` -TestType 'Unit' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Stubs') -ChildPath 'SMO.cs') + + # Load the default SQL Module stub + Import-SQLModuleStub } function Invoke-TestCleanup @@ -42,11 +47,16 @@ Invoke-TestSetup try { InModuleScope $script:dscResourceName { + $script:moduleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent + Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'Tests' -ChildPath (Join-Path -Path 'TestHelpers' -ChildPath 'CommonTestHelper.psm1'))) -Force + $mockClusterGroupName = 'AGTest' $mockRetryInterval = 1 $mockRetryCount = 2 $mockOtherClusterGroupName = 'UnknownAG' + $mockIsHadrEnabled = $true + $mockIsHadrDisabled = $false # Function stub of Get-ClusterGroup (when we do not have Failover Cluster powershell module available) function Get-ClusterGroup { @@ -82,15 +92,98 @@ try # Default parameters that are used for the It-blocks $mockDefaultParameters = @{ + ServerName = $env:COMPUTERNAME + InstanceName = 'MSSQLSERVER' Name = $mockClusterGroupName RetryIntervalSec = $mockRetryInterval RetryCount = $mockRetryCount } + $mockConnectSql = { + param + ( + [Parameter()] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName + ) + + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.IsHadrEnabled = $mockIsHadrEnabled + $mockServerObject.Name = $ServerName + $mockServerObject.ServiceName = $InstanceName + + # Define the availability group object + $mockAvailabilityGroupObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityGroup + $mockAvailabilityGroupObject.Name = $mockClusterGroupName + + # Add the availability group to the server object + $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupObject) + + return $mockServerObject + } + + $mockConnectSqlDisabled = { + param + ( + [Parameter()] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName + ) + + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.IsHadrEnabled = $mockIsHadrDisabled + $mockServerObject.Name = $ServerName + $mockServerObject.ServiceName = $InstanceName + + # Define the availability group object + $mockAvailabilityGroupObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityGroup + $mockAvailabilityGroupObject.Name = $mockClusterGroupName + + # Add the availability group to the server object + $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupObject) + + return $mockServerObject + } + + $mockConnectSqlWrongAG = { + param + ( + [Parameter()] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName + ) + + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.IsHadrEnabled = $mockIsHadrEnabled + $mockServerObject.Name = $ServerName + $mockServerObject.ServiceName = $InstanceName + + # Define the availability group object + $mockAvailabilityGroupObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityGroup + $mockAvailabilityGroupObject.Name = 'OtherAG' + + # Add the availability group to the server object + $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupObject) + + return $mockServerObject + } + Describe 'SqlWaitForAG\Get-TargetResource' -Tag 'Get' { - BeforeEach { + BeforeAll { $testParameters = $mockDefaultParameters.Clone() - + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -Verifiable Mock -CommandName Get-ClusterGroup -MockWith $mockGetClusterGroup -ParameterFilter $mockGetClusterGroup_ParameterFilter_KnownGroup -Verifiable Mock -CommandName Get-ClusterGroup -MockWith { return $null @@ -98,7 +191,9 @@ try } Context 'When the system is in the desired state' { - $mockExpectedClusterGroupName = $mockClusterGroupName + BeforeAll { + $mockExpectedClusterGroupName = $mockClusterGroupName + } It 'Should return the same values as passed as parameters' { $result = Get-TargetResource @testParameters @@ -116,17 +211,16 @@ try It 'Should return that the group exist' { $result = Get-TargetResource @testParameters - $result.GroupExist | Should -Be $true + $result.GroupExist | Should -BeTrue } } - Context 'When the system is not in the desired state' { - BeforeEach { + Context 'When the system is not in the desired state with good configuration' { + BeforeAll { $testParameters.Name = $mockOtherClusterGroupName + $mockExpectedClusterGroupName = $mockOtherClusterGroupName } - $mockExpectedClusterGroupName = $mockOtherClusterGroupName - It 'Should return the same values as passed as parameters' { $result = Get-TargetResource @testParameters $result.RetryIntervalSec | Should -Be $mockRetryInterval @@ -143,7 +237,37 @@ try It 'Should return that the group does not exist' { $result = Get-TargetResource @testParameters - $result.GroupExist | Should -Be $false + $result.GroupExist | Should -BeFalse + } + } + + Context 'When the system is not in the desired state with bad configuration' { + BeforeAll { + $testParameters.Name = $mockClusterGroupName + $mockExpectedClusterGroupName = $mockClusterGroupName + + Mock -CommandName Connect-SQL -MockWith $mockConnectSqlDisabled -Verifiable + } + + It 'Should return that the group does not exist' { + $result = Get-TargetResource @testParameters + $result.GroupExist | Should -BeFalse + } + } + + Assert-VerifiableMock + + Context 'When the system is not in the desired state when Cluster Resource is there, but Availibility Group is not.' { + BeforeAll { + $testParameters.Name = $mockClusterGroupName + $mockExpectedClusterGroupName = $mockClusterGroupName + + Mock -CommandName Connect-SQL -MockWith $mockConnectSqlWrongAG -Verifiable + } + + It 'Should return that the group does not exist' { + $result = Get-TargetResource @testParameters + $result.GroupExist | Should -BeFalse } } @@ -152,9 +276,10 @@ try Describe 'SqlWaitForAG\Test-TargetResource' -Tag 'Test'{ - BeforeEach { + BeforeAll { $testParameters = $mockDefaultParameters.Clone() + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -Verifiable Mock -CommandName Get-ClusterGroup -MockWith $mockGetClusterGroup -ParameterFilter $mockGetClusterGroup_ParameterFilter_KnownGroup -Verifiable Mock -CommandName Get-ClusterGroup -MockWith { return $null @@ -162,11 +287,11 @@ try } Context 'When the system is in the desired state' { - $mockExpectedClusterGroupName = $mockClusterGroupName - It 'Should return that desired state is present ($true)' { + $mockExpectedClusterGroupName = $mockClusterGroupName + $result = Test-TargetResource @testParameters - $result | Should -Be $true + $result | Should -BeTrue Assert-MockCalled -CommandName Get-ClusterGroup ` -ParameterFilter $mockGetClusterGroup_ParameterFilter_KnownGroup ` @@ -179,13 +304,12 @@ try } Context 'When the system is not in the desired state' { - $mockExpectedClusterGroupName = $mockOtherClusterGroupName - It 'Should return that desired state is absent ($false)' { + $mockExpectedClusterGroupName = $mockOtherClusterGroupName $testParameters.Name = $mockOtherClusterGroupName $result = Test-TargetResource @testParameters - $result | Should -Be $false + $result | Should -BeFalse Assert-MockCalled -CommandName Get-ClusterGroup ` -ParameterFilter $mockGetClusterGroup_ParameterFilter_KnownGroup ` @@ -201,9 +325,10 @@ try } Describe 'SqlWaitForAG\Set-TargetResource' -Tag 'Set'{ - BeforeEach { + BeforeAll { $testParameters = $mockDefaultParameters.Clone() + Mock -CommandName Connect-SQL -MockWith $mockConnectSql -Verifiable Mock -CommandName Start-Sleep Mock -CommandName Get-ClusterGroup -MockWith $mockGetClusterGroup -ParameterFilter $mockGetClusterGroup_ParameterFilter_KnownGroup -Verifiable Mock -CommandName Get-ClusterGroup -MockWith { @@ -212,9 +337,8 @@ try } Context 'When the system is in the desired state' { - $mockExpectedClusterGroupName = $mockClusterGroupName - It 'Should find the cluster group and return without throwing' { + $mockExpectedClusterGroupName = $mockClusterGroupName { Set-TargetResource @testParameters } | Should -Not -Throw Assert-MockCalled -CommandName Get-ClusterGroup ` @@ -227,9 +351,8 @@ try } Context 'When the system is not in the desired state' { - $mockExpectedClusterGroupName = $mockOtherClusterGroupName - It 'Should throw the correct error message' { + $mockExpectedClusterGroupName = $mockOtherClusterGroupName $testParameters.Name = $mockOtherClusterGroupName { Set-TargetResource @testParameters } | Should -Throw ($script:localizedData.FailedMessage -f $mockOtherClusterGroupName)