DSCResources/MSFT_xDFSReplicationGroup/MSFT_xDFSReplicationGroup.psm1

data LocalizedData
{
    # culture="en-US"
    ConvertFrom-StringData -StringData @'
GettingReplicationGroupMessage=Getting DFS Replication Group "{0}".
ReplicationGroupExistsMessage=DFS Replication Group "{0}" exists.
ReplicationGroupDoesNotExistMessage=DFS Replication Group "{0}" does not exist.
SettingRegGroupMessage=Setting DFS Replication Group "{0}".
EnsureReplicationGroupExistsMessage=Ensuring DFS Replication Group "{0}" exists.
EnsureReplicationGroupDoesNotExistMessage=Ensuring DFS Replication Group "{0}" does not exist.
ReplicationGroupCreatedMessage=DFS Replication Group "{0}" has been created.
ReplicationGroupDescriptionUpdatedMessage=DFS Replication Group "{0}" description has been updated.
ReplicationGroupMemberAddedMessage=DFS Replication Group "{0}" added member "{2}".
ReplicationGroupMemberRemovedMessage=DFS Replication Group "{0}" removed member "{2}".
ReplicationGroupFolderAddedMessage=DFS Replication Group "{0}" added folder "{2}".
ReplicationGroupFolderRemovedMessage=DFS Replication Group "{0}" removed folder "{2}".
ReplicationGroupContentPathUpdatedMessage=DFS Replication Group "{0}" Content Path for "{2}" updated.
ReplicationGroupExistsRemovedMessage=DFS Replication Group "{0}" existed, but has been removed.
ReplicationGroupFullMeshConnectionAddedMessage=DFS Replication Group "{0}" Fullmesh Connection from "{2}" to "{3}" added.
ReplicationGroupFullMeshConnectionUpdatedMessage=DFS Replication Group "{0}" Fullmesh Connection from "{2}" to "{3}" updated.
TestingRegGroupMessage=Testing DFS Replication Group "{0}".
ReplicationGroupDescriptionNeedsUpdateMessage=DFS Replication Group "{0}" description is different. Change required.
ReplicationGroupMembersNeedUpdateMessage=DFS Replication Group "{0}" members are different. Change required.
ReplicationGroupFoldersNeedUpdateMessage=DFS Replication Group "{0}" folders are different. Change required.
ReplicationGroupContentPathNeedUpdateMessage=DFS Replication Group "{0}" Content Path for "{2}" is different. Change required.
ReplicationGroupDoesNotExistButShouldMessage=DFS Replication Group "{0}" does not exist but should. Change required.
ReplicationGroupExistsButShouldNotMessage=DFS Replication Group "{0}" exists but should not. Change required.
ReplicationGroupDoesNotExistAndShouldNotMessage=DFS Replication Group "{0}" does not exist and should not. Change not required.
ReplicationGroupFullMeshMissingConnectionMessage=DFS Replication Group "{0}" Fullmesh Connection from "{2}" to "{3}" does not exist. Change required.
ReplicationGroupFullMeshDisabledConnectionMessage=DFS Replication Group "{0}" Fullmesh Connection from "{2}" to "{3}" is disabled. Change required.
ReplicationGroupDomainMismatchError=DFS Replication Group "{0}" Domain name in Member "{1}" does not match DomainName "{2}". Configuration correction required.
'@

}


function Get-TargetResource
{
    [OutputType([Hashtable])]
    param
    (
        [parameter(Mandatory = $true)]
        [String]
        $GroupName,

        [parameter(Mandatory = $true)]
        [ValidateSet('Present','Absent')]
        [String]
        $Ensure,

        [String]
        $DomainName
    )
    
    Write-Verbose -Message ( @(
        "$($MyInvocation.MyCommand): "
        $($LocalizedData.GettingReplicationGroupMessage) `
            -f $GroupName,$DomainName
        ) -join '' )

    # Lookup the existing Replication Group
    $Splat = @{ GroupName = $GroupName }
    $returnValue = $splat.Clone()
    if ($DomainName)
    {
        $Splat += @{ DomainName = $DomainName }
    }
    $ReplicationGroup = Get-DfsReplicationGroup @Splat `
        -ErrorAction Stop
    if ($ReplicationGroup)
    {
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.ReplicationGroupExistsMessage) `
                -f $GroupName,$DomainName
            ) -join '' )
          
        # Array paramters are disabled until this issue is resolved:
        # https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/11088807-get-dscconfiguration-fails-with-embedded-cim-type
        $returnValue += @{
            Ensure = 'Present'
            Description = $ReplicationGroup.Description
            DomainName = $ReplicationGroup.DomainName
            # Members = @((Get-DfsrMember @Splat -ErrorAction Stop).ComputerName)
            # Folders = @((Get-DfsReplicatedFolder @Splat -ErrorAction Stop).FolderName)
            # ContentPaths = @()
        }
    }
    else
    {       
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.ReplicationGroupDoesNotExistMessage) `
                -f $GroupName,$DomainName
            ) -join '' )
        $returnValue += @{ Ensure = 'Absent' }
    }

    $returnValue
} # Get-TargetResource


function Set-TargetResource
{
    param
    (
        [parameter(Mandatory = $true)]
        [String]
        $GroupName,

        [parameter(Mandatory = $true)]
        [ValidateSet('Present','Absent')]
        [String]
        $Ensure,

        [String]
        $Description,

        [String[]]
        $Members,

        [String[]]
        $Folders,

        [ValidateSet('Fullmesh','Manual')]
        [String]
        $Topology = 'Manual',

        [String[]]
        $ContentPaths,

        [String]
        $DomainName
    )

    Write-Verbose -Message ( @(
        "$($MyInvocation.MyCommand): "
        $($LocalizedData.SettingRegGroupMessage) `
            -f $GroupName,$DomainName
        ) -join '' )

    # Lookup the existing Replication Group
    $Splat = @{ GroupName = $GroupName }
    if ($DomainName)
    {
        $Splat += @{ DomainName = $DomainName }
    }
    $ReplicationGroup = Get-DfsReplicationGroup @Splat `
        -ErrorAction Stop

    if ($Ensure -eq 'Present')
    {
        # The rep group should exist
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.EnsureReplicationGroupExistsMessage) `
                -f $GroupName,$DomainName
            ) -join '' )

        if ($Description)
        {
            $Splat += @{ Description = $Description }
        } # if

        if ($ReplicationGroup)
        {
            # The RG exists already - Check the existing RG and members
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupExistsMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )
            # Check the description
            if (($Description) -and ($ReplicationGroup.Description -ne $Description))
            {
                Set-DfsReplicationGroup @Splat -ErrorAction Stop
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupDescriptionUpdatedMessage) `
                        -f $GroupName,$DomainName
                    ) -join '' )
            } # if
        }
        else
        {
            # Ths Rep Groups doesn't exist - Create it
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupDoesNotExistMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )
            New-DfsReplicationGroup @Splat -ErrorAction Stop
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupCreatedMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )

        } # if

        # Clean up the splat so we can use it in the next cmdlets
        $Splat.Remove('Description')
        
        # Create an array of FQDN Members from the Members Array
        $Splat += @{ ComputerName = '' }
        foreach ($Member in $Members)
        {
            $Splat.ComputerName = $Member
            $FQDNMembers += @( Get-FQDNMemberName @Splat )
        }
        $Splat.Remove('ComputerName')
        
        # Get the existing members of this DFS Rep Group
        $ExistingMembers = (Get-DfsrMember @Splat -ErrorAction Stop).DnsName

        # Add any missing members
        foreach ($Member in $FQDNMembers) 
        {
            if ($Member -notin $ExistingMembers)
            {
                # Member is missing - add it
                Add-DfsrMember @Splat `
                    -ComputerName $Member `
                    -ErrorAction Stop
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupMemberAddedMessage) `
                        -f $GroupName,$DomainName,$Member
                    ) -join '' )
            } # if
        } # foreach

        # Remove any members that shouldn't exist
        foreach ($ExistingMember in $ExistingMembers)
        {
            if ($ExistingMember -notin $FQDNMembers)
            {
                # Member exists but shouldn't - remove it
                Remove-DfsrMember @Splat `
                    -ComputerName $ExistingMember `
                    -Force `
                    -ErrorAction Stop
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupMemberRemovedMessage) `
                        -f $GroupName,$DomainName,$ExistingMember
                    ) -join '' )
            } # if
        } # foreach

        # Get the existing folders of this DFS Rep Group
        $ExistingFolders = (Get-DfsReplicatedFolder @Splat -ErrorAction Stop).FolderName

        # Add any missing folders
        foreach ($Folder in $Folders)
        {
            if ($Folder -notin $ExistingFolders)
            {
                # Folder is missing - add it
                New-DfsReplicatedFolder @Splat `
                    -FolderName $Folder `
                    -ErrorAction Stop
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupFolderAddedMessage) `
                        -f $GroupName,$DomainName,$Folder
                    ) -join '' )
            } # if
        } # foreach

        # Remove any folders that shouldn't exist
        foreach ($ExistingFolder in $ExistingFolders)
        {
            if ($ExistingFolder -notin $Folders)
            {
                # Folder exists but shouldn't - remove it
                Remove-DfsReplicatedFolder @Splat `
                    -Folder $ExistingFolder `
                    -Force `
                    -ErrorAction Stop
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupFolderRemovedMessage) `
                        -f $GroupName,$DomainName,$ExistingFolder
                    ) -join '' )
            } # if
        } # foreach

        # Set the content paths (if any were passed in the array)
        if ($ContentPaths)
        {
            # Get the current memberships for this rep group
            $memberships = Get-DfsrMembership @Splat `
                -ErrorAction Stop
            # Scan through the content paths array
            for ($i=0; $i -lt $Folders.Count; $i++)
            {
                $ContentPath = $ContentPaths[$i]
                if ($ContentPath)
                {
                     foreach ($membership in $memberships) 
                     {
                        
                        [String] $FQDNMemberName = Get-FQDNMemberName `
                            @Splat `
                            -ComputerName $membership.ComputerName
                        [Boolean] $primarymember = ($FQDNMemberName -eq $FQDNMembers[0])
                        if (($membership.FolderName -ne $Folders[$i]) `
                            -or (($membership.ContentPath -eq $ContentPath) `
                            -and ($membership.PrimaryMember -eq $primarymember)))
                        {
                            # Don't update this membership
                            continue
                        }
                        # The Content Path for this member needs to be set
                        Set-DfsrMembership @Splat `
                            -FolderName $membership.FolderName `
                            -ComputerName $membership.ComputerName `
                            -PrimaryMember $primarymember `
                            -ContentPath $ContentPath
                        Write-Verbose -Message ( @(
                            "$($MyInvocation.MyCommand): "
                            $($LocalizedData.ReplicationGroupContentPathUpdatedMessage) `
                                -f $GroupName,$DomainName,$membership.ComputerName
                            ) -join '' )
                    } # foreach
                } # if
            } # foreach
        } # if

        # If the topology is not manual, automatically configure the connections
        switch ($Topology)
        {
            'Fullmesh'
            {
                $Splat += @{
                    SourceComputerName = ''
                    DestinationComputerName = ''
                }
                # Scan through the combination of connections
                foreach ($source in $FQDNMembers)
                {
                    foreach ($dest in $FQDNMembers)
                    {
                        if ($source -eq $dest)
                        {
                            continue
                        }
                        $Splat.SourceComputerName = $source
                        $Splat.DestinationComputerName = $dest
                        $ReplicationGroupConnection = Get-DfsrConnection @Splat `
                            -ErrorAction Stop
                        if ($ReplicationGroupConnection) {
                            if (-not $ReplicationGroupConnection.Enabled) {
                                Set-DfsrConnection @Splat `
                                    -DisableConnection $false `
                                    -ErrorAction Stop
                                Write-Verbose -Message ( @(
                                    "$($MyInvocation.MyCommand): "
                                        $($LocalizedData.ReplicationGroupFullMeshConnectionUpdatedMessage) `
                                        -f  $GroupName,$DomainName,$source,$dest
                                    ) -join '' )
                            }
                        }
                        else
                        {
                            Add-DfsrConnection @Splat `
                                -ErrorAction Stop
                            Write-Verbose -Message ( @(
                                "$($MyInvocation.MyCommand): "
                                    $($LocalizedData.ReplicationGroupFullMeshConnectionAddedMessage) `
                                    -f  $GroupName,$DomainName,$source,$dest
                                ) -join '' )
                        } # if
                    } # foreach
                } # foreach
            }
        } # swtich
    }
    else
    {
        # The Rep Group should not exist
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.EnsureReplicationGroupDoesNotExistMessage) `
                -f $GroupName,$DomainName
            ) -join '' )
        if ($ReplicationGroup)
        {
            # Remove the replication group
            Remove-DfsReplicationGroup @Splat `
                -RemoveReplicatedFolders `
                -Force `
                -ErrorAction Stop
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupExistsRemovedMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )
        }
    } # if
} # Set-TargetResource


function Test-TargetResource
{
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [String]
        $GroupName,

        [parameter(Mandatory = $true)]
        [ValidateSet('Present','Absent')]
        [String]
        $Ensure,

        [String]
        $Description,

        [String[]]
        $Members,

        [String[]]
        $Folders,

        [ValidateSet('Fullmesh','Manual')]
        [String]
        $Topology = 'Manual',

        [String[]]
        $ContentPaths,

        [String]
        $DomainName
    )

    # Flag to signal whether settings are correct
    [Boolean] $desiredConfigurationMatch = $true

    Write-Verbose -Message ( @(
        "$($MyInvocation.MyCommand): "
        $($LocalizedData.TestingRegGroupMessage) `
            -f $GroupName,$DomainName
        ) -join '' )

    # Lookup the existing Replication Group
    $Splat = @{ GroupName = $GroupName }
    if ($DomainName)
    {
        $Splat += @{ DomainName = $DomainName }
    }
    $ReplicationGroup = Get-DFSReplicationGroup @Splat `
        -ErrorAction Stop

    if ($Ensure -eq 'Present')
    {
        # The RG should exist
        if ($ReplicationGroup)
        {
            # The RG exists already
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupExistsMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )

            # Check the description
            if (($Description) -and ($ReplicationGroup.Description -ne $Description))
            {
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupDescriptionNeedsUpdateMessage) `
                        -f $GroupName,$DomainName
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            # Create an array of FQDN Members from the Members Array
            $Splat += @{ ComputerName = '' }
            foreach ($Member in $Members)
            {
                $Splat.ComputerName = $Member
                $FQDNMembers += @( Get-FQDNMemberName @Splat )
            }
            $Splat.Remove('ComputerName')

            # Compare the Members
            $ExistingMembers = @((Get-DfsrMember @Splat -ErrorAction Stop).DnsName)
            if ((Compare-Object `
                -ReferenceObject $FQDNMembers `
                -DifferenceObject $ExistingMembers).Count -ne 0)
            {
                # There is a member different of some kind.
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupMembersNeedUpdateMessage) `
                        -f $GroupName,$DomainName
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            # Compare the Folders
            $ExistingFolders = @((Get-DfsReplicatedFolder @Splat -ErrorAction Stop).FolderName)
            if ((Compare-Object `
                -ReferenceObject $Folders `
                -DifferenceObject $ExistingFolders).Count -ne 0)
            {
                # There is a folder different of some kind.
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.ReplicationGroupFoldersNeedUpdateMessage) `
                        -f $GroupName,$DomainName
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            # Get the content paths (if any were passed in the array)
            if ($ContentPaths)
            {
                # Get the current memberships for this rep group
                $memberships = Get-DfsrMembership @Splat `
                    -ErrorAction Stop
                # Scan through the content paths array
                for ($i=0; $i -lt $Folders.Count; $i++)
                {
                    $ContentPath = $ContentPaths[$i]
                    if ($ContentPath)
                    {
                        Foreach ($membership in $memberships)
                        {
                            [Boolean]$primarymember = ($membership.ComputerName -eq $Members[0])
                            if (($membership.FolderName -ne $Folders[$i]) `
                                -or (($membership.ContentPath -eq $ContentPath) `
                                -and ($membership.PrimaryMember -eq $primarymember)))
                            {
                                # This membership is in the correct state.
                                continue
                            }
                            Write-Verbose -Message ( @(
                                "$($MyInvocation.MyCommand): "
                                $($LocalizedData.ReplicationGroupContentPathNeedUpdateMessage) `
                                    -f $GroupName,$DomainName,$membership.ComputerName
                                ) -join '' )
                            $desiredConfigurationMatch = $false
                        } # if
                    } # if
                } # foreach
            } # if

            # If the topology is not manual, check the connections are configured
            switch ($Topology)
            {
                'Fullmesh'
                {
                    $Splat += @{
                        SourceComputerName = ''
                        DestinationComputerName = ''
                    }
                    # Scan through the combination of connections
                    foreach ($source in $FQDNMembers)
                    {
                        foreach ($dest in $FQDNMembers)
                        {
                            if ($source -eq $dest)
                            {
                                continue
                            }
                            $Splat.SourceComputerName = $source
                            $Splat.DestinationComputerName = $dest
                            $ReplicationGroupConnection = Get-DfsrConnection @Splat `
                                -ErrorAction Stop
                            if ($ReplicationGroupConnection)
                            {
                                if (-not $ReplicationGroupConnection.Enabled)
                                {
                                    Write-Verbose -Message ( @(
                                        "$($MyInvocation.MyCommand): "
                                         $($LocalizedData.ReplicationGroupFullMeshDisabledConnectionMessage) `
                                            -f  $GroupName,$DomainName,$source,$dest
                                        ) -join '' )
                                    $desiredConfigurationMatch = $false
                                }
                            }
                            else
                            {
                                Write-Verbose -Message ( @(
                                    "$($MyInvocation.MyCommand): "
                                     $($LocalizedData.ReplicationGroupFullMeshMissingConnectionMessage) `
                                        -f  $GroupName,$DomainName,$source,$dest
                                    ) -join '' )
                                $desiredConfigurationMatch = $false
                            }
                        }
                    }
                }
            }
        }
        else
        {
            # Ths RG doesn't exist but should
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                 $($LocalizedData.ReplicationGroupDoesNotExistButShouldMessage) `
                    -f  $GroupName,$DomainName
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
    }
    else
    {
        # The RG should not exist
        if ($ReplicationGroup)
        {
            # The RG exists but should not
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                 $($LocalizedData.ReplicationGroupExistsButShouldNotMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
        else
        {
            # The RG does not exist and should not
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.ReplicationGroupDoesNotExistAndShouldNotMessage) `
                    -f $GroupName,$DomainName
                ) -join '' )
        }
    } # if
    return $desiredConfigurationMatch
} # Test-TargetResource


# Helper functions
<#
.SYNOPSIS
    Returns the FQDN Member name based on the ComputerName and DomainName that are provided.
     
    If the ComputerName is already an FQDN but the domain in the FQDN does not match the
    value passed in DomainName then throw an exception.
     
    If the ComputerName is already an FQDN and the domain in the FQDN does match the value
    passed in DomainName then the existing ComputerName is returned.
     
    If the ComputerName is not already an FQDN and the DomainName passed is not empty then
    the ComputerName and DomainName are combined and returned.
     
    If the ComputerName is not already an FQDN and the DomainName passed is empty then
    the ComputerName is returned.
#>

function Get-FQDNMemberName
{
    [OutputType([System.String])]
    param
    (
        [parameter(Mandatory = $true)]
        [String]
        $GroupName,

        [parameter(Mandatory = $true)]
        [String]
        $ComputerName,

        [String]
        $DomainName
    )
    
    if ($ComputerName.Contains('.'))
    {
        if (($DomainName -ne $null) -and ($DomainName -ne ''))
        {
            if ($ComputerName -like "*.$DomainName")
            {
                return $ComputerName.ToLower()
            }
            else
            {
                $ExceptionParameters = @{
                    errorId = 'ReplicationGroupDomainMismatchError'
                    errorCategory = 'InvalidArgument'
                    errorMessage = $($LocalizedData.ReplicationGroupDomainMismatchError `
                        -f $GroupName,$ComputerName,$DomainName)
                }
                New-Exception @ExceptionParameters
            }
        }
        else
        {
            Return $ComputerName.ToLower()
        }
    }
    else
    {
        if (($DomainName -ne $null) -and ($DomainName -ne ''))
        {
            Return "$ComputerName.$DomainName".ToLower()
        }
        else
        {
            Return $ComputerName.ToLower()
        }
    }

} # Get-FQDNMemberName

<#
.SYNOPSIS
    Throw a custom exception.
#>

function New-Exception
{
    [CmdLetBinding()]
    param
    (
        [Parameter(Mandatory)]
        [String] $errorId,

        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorCategory] $errorCategory,

        [Parameter(Mandatory)]
        [String] $errorMessage
    )

    $exception = New-Object -TypeName System.Exception `
        -ArgumentList $errorMessage
    $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
        -ArgumentList $exception, $errorId, $errorCategory, $null

    throw $errorRecord
} # New-Exception

Export-ModuleMember -Function *-TargetResource