From a7122369e029f446ded0021d2885b52b9e2fd1b3 Mon Sep 17 00:00:00 2001
From: Dan Reist <djreist@outlook.com>
Date: Fri, 20 Oct 2017 03:31:02 -0400
Subject: [PATCH] xSQLServerAlwaysOnAvailabilityGroup: Make Cluster Aware
 (#890)

- 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).
---
 CHANGELOG.md                                  |  3 +
 ...T_xSQLServerAlwaysOnAvailabilityGroup.psm1 | 82 ++++++++++++-------
 ...ServerAlwaysOnAvailabilityGroup.schema.mof |  2 +
 .../3-CreateAvailabilityGroupDetailed.ps1     | 13 ++-
 README.md                                     |  5 ++
 ...LServerAlwaysOnAvailabilityGroup.Tests.ps1 | 63 +++++++++++++-
 Tests/Unit/Stubs/SMO.cs                       |  1 +
 Tests/Unit/xSQLServerHelper.Tests.ps1         | 45 ++++++++++
 xSQLServerHelper.psm1                         | 41 ++++++++++
 9 files changed, 221 insertions(+), 34 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79b9ecbe9..c310c0afd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)).
diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.psm1 b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.psm1
index f239a9dd3..1c87a2bb6 100644
--- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.psm1
+++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.psm1
@@ -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 )
@@ -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
 }
@@ -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
 {
@@ -224,7 +225,11 @@ function Set-TargetResource
 
         [Parameter()]
         [UInt32]
-        $HealthCheckTimeout = 30000
+        $HealthCheckTimeout = 30000,
+
+        [Parameter()]
+        [Boolean]
+        $ProcessOnlyOnActiveNode
     )
 
     Import-SQLPSModule
@@ -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
 {
@@ -590,7 +598,11 @@ function Test-TargetResource
 
         [Parameter()]
         [UInt32]
-        $HealthCheckTimeout = 30000
+        $HealthCheckTimeout = 30000,
+
+        [Parameter()]
+        [Boolean]
+        $ProcessOnlyOnActiveNode
     )
 
     $getTargetResourceParameters = @{
@@ -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
 
diff --git a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.schema.mof b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.schema.mof
index 6dfa752bc..7cf156426 100644
--- a/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.schema.mof
+++ b/DSCResources/MSFT_xSQLServerAlwaysOnAvailabilityGroup/MSFT_xSQLServerAlwaysOnAvailabilityGroup.schema.mof
@@ -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;
 };
diff --git a/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroup/3-CreateAvailabilityGroupDetailed.ps1 b/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroup/3-CreateAvailabilityGroupDetailed.ps1
index 0e6a6bfd4..51ddbe8ee 100644
--- a/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroup/3-CreateAvailabilityGroupDetailed.ps1
+++ b/Examples/Resources/xSQLServerAlwaysOnAvailabilityGroup/3-CreateAvailabilityGroupDetailed.ps1
@@ -1,6 +1,11 @@
 <#
 .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 = @{
@@ -8,6 +13,7 @@ $ConfigurationData = @{
         @{
             NodeName                      = '*'
             SQLInstanceName               = 'MSSQLSERVER'
+            ProcessOnlyOnActiveNode       = $true
 
             AutomatedBackupPreference     = 'Primary'
             AvailabilityMode              = 'SynchronousCommit'
@@ -16,7 +22,7 @@ $ConfigurationData = @{
             ConnectionModeInSecondaryRole = 'AllowNoConnections'
             FailoverMode                  = 'Automatic'
             HealthCheckTimeout            = 15000
-            
+
             BasicAvailabilityGroup        = $False
             DatabaseHealthTrigger         = $True
             DtcSupportEnabled             = $True
@@ -83,7 +89,8 @@ Configuration Example
                 Name                          = 'TestAG'
                 SQLInstanceName               = $Node.SQLInstanceName
                 SQLServer                     = $Node.NodeName
-                
+                ProcessOnlyOnActiveNode       = $Node.ProcessOnlyOnActiveNode
+
                 AutomatedBackupPreference     = $Node.AutomatedBackupPreference
                 AvailabilityMode              = $Node.AvailabilityMode
                 BackupPriority                = $Node.BackupPriority
@@ -91,7 +98,7 @@ Configuration Example
                 ConnectionModeInSecondaryRole = $Node.ConnectionModeInSecondaryRole
                 FailoverMode                  = $Node.FailoverMode
                 HealthCheckTimeout            = $Node.HealthCheckTimeout
-                
+
                 # sql server 2016 or later only
                 BasicAvailabilityGroup        = $Node.BasicAvailabilityGroup
                 DatabaseHealthTrigger         = $Node.DatabaseHealthTrigger
diff --git a/README.md b/README.md
index ced143560..af8c35a98 100644
--- a/README.md
+++ b/README.md
@@ -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
 
@@ -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
 
diff --git a/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroup.Tests.ps1 b/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroup.Tests.ps1
index 1cab112e8..0a0409bca 100644
--- a/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroup.Tests.ps1
+++ b/Tests/Unit/MSFT_xSQLServerAlwaysOnAvailabilityGroup.Tests.ps1
@@ -69,6 +69,8 @@ try
             'NamedInstance'
         )
 
+        $mockProcessOnlyOnActiveNode = $true
+
         #endregion parameter mocks
 
         #region property mocks
@@ -148,6 +150,7 @@ try
         $testTargetResourceEndpointIncorrectTestCases = @()
         $testTargetResourcePresentTestCases = @()
         $testTargetResourcePropertyIncorrectTestCases = @()
+        $testTargetResourceProcessOnlyOnActiveNodeTestCases = @()
 
         $majorVersionsToTest = @(12,13)
         $ensureCasesToTest = @('Absent','Present')
@@ -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'
@@ -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 )
                         {
@@ -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' {
@@ -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
                 }
             }
 
@@ -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
                 }
             }
 
@@ -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
                 }
             }
 
@@ -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
                 }
             }
         }
diff --git a/Tests/Unit/Stubs/SMO.cs b/Tests/Unit/Stubs/SMO.cs
index c712299ce..577722d08 100644
--- a/Tests/Unit/Stubs/SMO.cs
+++ b/Tests/Unit/Stubs/SMO.cs
@@ -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;
diff --git a/Tests/Unit/xSQLServerHelper.Tests.ps1 b/Tests/Unit/xSQLServerHelper.Tests.ps1
index 646fd7713..e8fb6d30b 100644
--- a/Tests/Unit/xSQLServerHelper.Tests.ps1
+++ b/Tests/Unit/xSQLServerHelper.Tests.ps1
@@ -1683,4 +1683,49 @@ InModuleScope $script:moduleName {
             }
         }
     }
+
+    Describe 'Testing Test-ActiveNode' {
+        BeforeAll {
+            $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server
+
+            $failoverClusterInstanceTestCases = @(
+                @{
+                    ComputerNamePhysicalNetBIOS = $env:COMPUTERNAME
+                    Result = $true
+                },
+                @{
+                    ComputerNamePhysicalNetBIOS = 'AnotherNode'
+                    Result = $false
+                }
+            )
+        }
+
+        Context 'When function is executed on a standalone instance' {
+            BeforeAll {
+                $mockServerObject.IsMemberOfWsfcCluster = $false
+            }
+
+            It 'Should return "$true"' {
+                Test-ActiveNode -ServerObject $mockServerObject | Should Be $true
+            }
+        }
+
+        Context 'When function is executed on a failover cluster instance (FCI)' {
+            BeforeAll {
+                $mockServerObject.IsMemberOfWsfcCluster = $true
+            }
+
+            It 'Should return "<Result>" when the node name is "<ComputerNamePhysicalNetBIOS>"' -TestCases $failoverClusterInstanceTestCases {
+                param
+                (
+                    $ComputerNamePhysicalNetBIOS,
+                    $Result
+                )
+
+                $mockServerObject.ComputerNamePhysicalNetBIOS = $ComputerNamePhysicalNetBIOS
+
+                Test-ActiveNode -ServerObject $mockServerObject | Should Be $Result
+            }
+        }
+    }
 }
diff --git a/xSQLServerHelper.psm1 b/xSQLServerHelper.psm1
index 771db880e..62e6e1d25 100644
--- a/xSQLServerHelper.psm1
+++ b/xSQLServerHelper.psm1
@@ -1321,3 +1321,44 @@ function Test-ClusterPermissions
 
     return $clusterPermissionsPresent
 }
+
+<#
+    .SYNOPSIS
+        Determine if the current node is hosting the instance.
+
+    .PARAMETER ServerObject
+        The server object on which to perform the test.
+#>
+function Test-ActiveNode
+{
+    [CmdletBinding()]
+    [OutputType([System.Boolean])]
+    param
+    (
+        [Parameter(Mandatory = $true)]
+        [Microsoft.SqlServer.Management.Smo.Server]
+        $ServerObject
+    )
+
+    $result = $false
+
+    # Determine if this is a failover cluster instance (FCI)
+    if ( $ServerObject.IsMemberOfWsfcCluster )
+    {
+        <#
+            If the current node name is the same as the name the instances is
+            running on, then this is the active node
+        #>
+        $result = $ServerObject.ComputerNamePhysicalNetBIOS -eq $env:COMPUTERNAME
+    }
+    else
+    {
+        <#
+            This is a standalone instance, therefore the node will always host
+            the instance.
+        #>
+        $result = $true
+    }
+
+    return $result
+}