Skip to content

Commit

Permalink
xSQLServerAlwaysOnAvailabilityGroup: Make Cluster Aware (#890)
Browse files Browse the repository at this point in the history
- Changes to xSQLServerAlwaysOnAvailabilityGroup
  - 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).
  • Loading branch information
randomnote1 authored and johlju committed Oct 20, 2017
1 parent 5d7af04 commit a712236
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 34 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
- Fixed the logic so that if a parameter is not supplied to the resource, the
resource will not attempt to apply the defaults on subsequent checks
([issue #517](https://github.com/PowerShell/xSQLServer/issues/517)).
- 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)).
- Added the CommonTestHelper.psm1 to store common testing functions.
- Added the Import-SQLModuleStub function to ensure the correct version of the
module stubs are loaded ([issue #784](https://github.com/PowerShell/xSQLServer/issues/784)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,34 @@ function Get-TargetResource
# Get the Availability Group
$availabilityGroup = $serverObject.AvailabilityGroups[$Name]

# Is this node actively hosting the SQL instance?
$isActiveNode = Test-ActiveNode -ServerObject $serverObject

# Create the return object. Default ensure to Absent.
$alwaysOnAvailabilityGroupResource = @{
Name = $Name
SQLServer = $SQLServer
SQLInstanceName = $SQLInstanceName
Ensure = 'Absent'
IsActiveNode = $isActiveNode
}

if ( $availabilityGroup )
{
# Get all of the properties that can be set using this resource
$alwaysOnAvailabilityGroupResource = @{
Name = $Name
SQLServer = $SQLServer
SQLInstanceName = $SQLInstanceName
Ensure = 'Present'
AutomatedBackupPreference = $availabilityGroup.AutomatedBackupPreference
AvailabilityMode = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].AvailabilityMode
BackupPriority = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].BackupPriority
ConnectionModeInPrimaryRole = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].ConnectionModeInPrimaryRole
ConnectionModeInSecondaryRole = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].ConnectionModeInSecondaryRole
FailureConditionLevel = $availabilityGroup.FailureConditionLevel
FailoverMode = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].FailoverMode
HealthCheckTimeout = $availabilityGroup.HealthCheckTimeout
EndpointURL = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].EndpointUrl
EndpointPort = $endpointPort
SQLServerNetName = $serverObject.NetName
Version = $sqlMajorVersion
}
$alwaysOnAvailabilityGroupResource.Ensure = 'Present'
$alwaysOnAvailabilityGroupResource.AutomatedBackupPreference = $availabilityGroup.AutomatedBackupPreference
$alwaysOnAvailabilityGroupResource.AvailabilityMode = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].AvailabilityMode
$alwaysOnAvailabilityGroupResource.BackupPriority = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].BackupPriority
$alwaysOnAvailabilityGroupResource.ConnectionModeInPrimaryRole = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].ConnectionModeInPrimaryRole
$alwaysOnAvailabilityGroupResource.ConnectionModeInSecondaryRole = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].ConnectionModeInSecondaryRole
$alwaysOnAvailabilityGroupResource.FailureConditionLevel = $availabilityGroup.FailureConditionLevel
$alwaysOnAvailabilityGroupResource.FailoverMode = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].FailoverMode
$alwaysOnAvailabilityGroupResource.HealthCheckTimeout = $availabilityGroup.HealthCheckTimeout
$alwaysOnAvailabilityGroupResource.EndpointURL = $availabilityGroup.AvailabilityReplicas[$serverObject.DomainInstanceName].EndpointUrl
$alwaysOnAvailabilityGroupResource.EndpointPort = $endpointPort
$alwaysOnAvailabilityGroupResource.SQLServerNetName = $serverObject.NetName
$alwaysOnAvailabilityGroupResource.Version = $sqlMajorVersion

# Add properties that are only present in SQL 2016 or newer
if ( $sqlMajorVersion -ge 13 )
Expand All @@ -80,16 +87,6 @@ function Get-TargetResource
$alwaysOnAvailabilityGroupResource.Add('DtcSupportEnabled', $availabilityGroup.DtcSupportEnabled)
}
}
else
{
# Return the minimum amount of properties showing that the Availability Group is absent
$alwaysOnAvailabilityGroupResource = @{
Name = $Name
SQLServer = $SQLServer
SQLInstanceName = $SQLInstanceName
Ensure = 'Absent'
}
}

return $alwaysOnAvailabilityGroupResource
}
Expand Down Expand Up @@ -142,6 +139,10 @@ function Get-TargetResource
.PARAMETER HealthCheckTimeout
Specifies the length of time, in milliseconds, after which AlwaysOn availability groups declare an unresponsive server to be unhealthy. Default is 30,000.
.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
{
Expand Down Expand Up @@ -224,7 +225,11 @@ function Set-TargetResource

[Parameter()]
[UInt32]
$HealthCheckTimeout = 30000
$HealthCheckTimeout = 30000,

[Parameter()]
[Boolean]
$ProcessOnlyOnActiveNode
)

Import-SQLPSModule
Expand Down Expand Up @@ -513,6 +518,9 @@ function Set-TargetResource
.PARAMETER HealthCheckTimeout
Specifies the length of time, in milliseconds, after which AlwaysOn availability groups declare an unresponsive server to be unhealthy. Default is 30,000.
.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
{
Expand Down Expand Up @@ -590,7 +598,11 @@ function Test-TargetResource

[Parameter()]
[UInt32]
$HealthCheckTimeout = 30000
$HealthCheckTimeout = 30000,

[Parameter()]
[Boolean]
$ProcessOnlyOnActiveNode
)

$getTargetResourceParameters = @{
Expand All @@ -604,6 +616,16 @@ function Test-TargetResource

$getTargetResourceResult = 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 $getTargetResourceResult.IsActiveNode )
{
New-VerboseMessage -Message ( 'The node "{0}" is not actively hosting the instance "{1}". Exiting the test.' -f $env:COMPUTERNAME,$SQLInstanceName )
return $result
}

# Define current version for check compatibility
$sqlMajorVersion = $getTargetResourceResult.Version

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ class MSFT_xSQLServerAlwaysOnAvailabilityGroup : OMI_BaseResource
[Write, Description("Specifies the automatic failover behavior of the availability group."), ValueMap{"OnServerDown","OnServerUnresponsive","OnCriticalServerErrors","OnModerateServerErrors","OnAnyQualifiedFailureCondition"}, Values{"OnServerDown","OnServerUnresponsive","OnCriticalServerErrors","OnModerateServerErrors","OnAnyQualifiedFailureCondition"}] String FailureConditionLevel;
[Write, Description("Specifies the failover mode. Default is 'Manual'."), ValueMap{"Automatic","Manual"}, Values{"Automatic","Manual"}] String FailoverMode;
[Write, Description("Specifies the length of time, in milliseconds, after which AlwaysOn availability groups declare an unresponsive server to be unhealthy. Default is 30000.")] UInt32 HealthCheckTimeout;
[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("Gets the Endpoint URL of the availability group replica endpoint.")] String EndpointUrl;
[Read, Description("Gets the port the database mirroring endpoint is listening on.")] UInt32 EndpointPort;
[Read, Description("Gets the hostname the SQL Server instance is listening on.")] String SQLServerNetName;
[Read, Description("Gets the major version of the SQL Server instance.")] UInt32 Version;
[Read, Description("Determines if the current node is actively hosting the SQL Server instance.")] Boolean IsActiveNode;
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
<#
.EXAMPLE
This example shows how to ensure that the Availability Group 'TestAG' exists.
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 = @{
AllNodes = @(
@{
NodeName = '*'
SQLInstanceName = 'MSSQLSERVER'
ProcessOnlyOnActiveNode = $true

AutomatedBackupPreference = 'Primary'
AvailabilityMode = 'SynchronousCommit'
Expand All @@ -16,7 +22,7 @@ $ConfigurationData = @{
ConnectionModeInSecondaryRole = 'AllowNoConnections'
FailoverMode = 'Automatic'
HealthCheckTimeout = 15000

BasicAvailabilityGroup = $False
DatabaseHealthTrigger = $True
DtcSupportEnabled = $True
Expand Down Expand Up @@ -83,15 +89,16 @@ Configuration Example
Name = 'TestAG'
SQLInstanceName = $Node.SQLInstanceName
SQLServer = $Node.NodeName

ProcessOnlyOnActiveNode = $Node.ProcessOnlyOnActiveNode

AutomatedBackupPreference = $Node.AutomatedBackupPreference
AvailabilityMode = $Node.AvailabilityMode
BackupPriority = $Node.BackupPriority
ConnectionModeInPrimaryRole = $Node.ConnectionModeInPrimaryRole
ConnectionModeInSecondaryRole = $Node.ConnectionModeInSecondaryRole
FailoverMode = $Node.FailoverMode
HealthCheckTimeout = $Node.HealthCheckTimeout

# sql server 2016 or later only
BasicAvailabilityGroup = $Node.BasicAvailabilityGroup
DatabaseHealthTrigger = $Node.DatabaseHealthTrigger
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ It will also manage the Availability Group replica on the specified node.
* **`[Uint32]` HealthCheckTimeout** _(Write)_: Specifies the length of time, in
milliseconds, after which AlwaysOn availability groups declare an unresponsive
server to be unhealthy. Default is 30000.
* **`[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

Expand All @@ -254,6 +257,8 @@ It will also manage the Availability Group replica on the specified node.
instance is listening on.
* **`[Uint32]` Version** _(Read)_: Gets the major version of the SQL Server
instance.
* **`[Boolean]` IsActiveNode** _(Read)_: Determines if the current node is
actively hosting the SQL Server instance.

#### Examples

Expand Down
63 changes: 62 additions & 1 deletion Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroup.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ try
'NamedInstance'
)

$mockProcessOnlyOnActiveNode = $true

#endregion parameter mocks

#region property mocks
Expand Down Expand Up @@ -148,6 +150,7 @@ try
$testTargetResourceEndpointIncorrectTestCases = @()
$testTargetResourcePresentTestCases = @()
$testTargetResourcePropertyIncorrectTestCases = @()
$testTargetResourceProcessOnlyOnActiveNodeTestCases = @()

$majorVersionsToTest = @(12,13)
$ensureCasesToTest = @('Absent','Present')
Expand All @@ -163,7 +166,7 @@ try
( Get-Command -Name Test-TargetResource ).Parameters.Values | Where-Object -FilterScript {
(
# Ignore these specific parameters. These get tested enough.
@('Ensure', 'Name', 'SQLServer', 'SQLInstanceName', 'DtcSupportEnabled') -notcontains $_.Name
@('Ensure', 'Name', 'SQLServer', 'SQLInstanceName', 'DtcSupportEnabled', 'ProcessOnlyOnActiveNode') -notcontains $_.Name
) -and (
# Ignore the CmdletBinding parameters
$_.Attributes.TypeId.Name -notcontains 'AliasAttribute'
Expand Down Expand Up @@ -284,6 +287,18 @@ try
Version = $majorVersionToTest
}

foreach ( $processOnlyOnActiveNode in @($true,$false) )
{
$testTargetResourceProcessOnlyOnActiveNodeTestCases += @{
Ensure = 'Present'
Name = $mockNameParameters.PresentAvailabilityGroup
ProcessOnlyOnActiveNode = $processOnlyOnActiveNode
SQLServer = $mockSqlServerParameter
SQLInstanceName = $mockSqlInstanceNameParameter
Version = $majorVersionToTest
}
}

# Create test cases for Absent/Present
foreach ( $ensureCaseToTest in $ensureCasesToTest )
{
Expand Down Expand Up @@ -1193,6 +1208,9 @@ try
Describe 'xSQLServerAlwaysOnAvailabilityGroup\Test-TargetResource' -Tag 'Test' {
BeforeAll {
Mock -CommandName Connect-SQL -MockWith $mockConnectSql -Verifiable
Mock -CommandName Test-ActiveNode -MockWith {
$mockProcessOnlyOnActiveNode
} -Verifiable
}

Context 'When the Availability Group is Absent' {
Expand Down Expand Up @@ -1220,6 +1238,7 @@ try
Test-TargetResource @testTargetResourceParameters | Should -Be $Result

Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly
Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly
}
}

Expand Down Expand Up @@ -1248,6 +1267,7 @@ try
Test-TargetResource @testTargetResourceParameters | Should -Be $Result

Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly
Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly
}
}

Expand Down Expand Up @@ -1279,6 +1299,7 @@ try
Test-TargetResource @testTargetResourceParameters | Should -Be $Result

Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly
Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly
}
}

Expand Down Expand Up @@ -1324,6 +1345,46 @@ try
Test-TargetResource @testTargetResourceParameters | Should -Be $Result

Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly
Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly
}
}

Context 'When the ProcessOnlyOnActiveNode parameter is passed' {
AfterAll {
$mockProcessOnlyOnActiveNode = $mockProcessOnlyOnActiveNodeOriginal
}

BeforeAll {
$mockProcessOnlyOnActiveNodeOriginal = $mockProcessOnlyOnActiveNode
$mockProcessOnlyOnActiveNode = $false
}

It 'Should be "true" when ProcessOnlyOnActiveNode is "<ProcessOnlyOnActiveNode>", Ensure is "<Ensure>", Name is "<Name>", SQLServer is "<SQLServer>", SQLInstanceName is "<SQLInstanceName>", and the SQL version is "<Version>"' -TestCases $testTargetResourceProcessOnlyOnActiveNodeTestCases {
param
(
$Ensure,
$Name,
$ProcessOnlyOnActiveNode,
$SQLServer,
$SQLInstanceName,
$Version
)

# Ensure the correct stubs are loaded for the SQL version
Import-SQLModuleStub -SQLVersion $Version

$testTargetResourceParameters = @{
Ensure = $Ensure
Name = $Name
ProcessOnlyOnActiveNode = $ProcessOnlyOnActiveNode
SQLServer = $SQLServer
SQLInstanceName = $SQLInstanceName
}

Test-TargetResource @testTargetResourceParameters | Should Be $true

Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly
Assert-MockCalled -CommandName Test-ActiveNode -Scope It -Times 1 -Exactly
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Tests/Unit/Stubs/SMO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ public class Server
public string InstanceName;
public bool IsClustered = false;
public bool IsHadrEnabled = false;
public bool IsMemberOfWsfcCluster = false;
public Hashtable Logins = new Hashtable();
public string Name;
public string NetName;
Expand Down
Loading

0 comments on commit a712236

Please sign in to comment.