DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.psm1

$resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$modulesFolderPath = Join-Path -Path $resourceModulePath -ChildPath 'Modules'

$aDCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common'
Import-Module -Name $aDCommonModulePath

$dscResourceCommonModulePath = Join-Path -Path $modulesFolderPath -ChildPath 'DscResource.Common'
Import-Module -Name $dscResourceCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

<#
    .SYNOPSIS
        Gets the current configuration on an AD Replication Site Link.

    .PARAMETER Name
        Specifies the name of the AD Replication Site Link.

    .PARAMETER SitesExcluded
        Specifies the list of sites to remove from a site link.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [System.String[]]
        $SitesExcluded
    )

    try
    {
        $siteLink = Get-ADReplicationSiteLink -Identity $Name -Properties 'Description', 'Options'
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        Write-Verbose -Message ($script:localizedData.SiteLinkNotFound -f $Name)

        $siteLink = $null
    }
    catch
    {
        $errorMessage = $script:localizedData.GetSiteLinkUnexpectedError -f $Name
        New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
    }

    if ($null -ne $siteLink)
    {
        $siteCommonNames = @()

        if ($siteLink.SitesIncluded)
        {
            foreach ($siteDN in $siteLink.SitesIncluded)
            {
                $siteCommonNames += Resolve-SiteLinkName -SiteName $siteDn
            }
        }

        if ($null -eq $siteLink.Options)
        {
            $siteLinkOptions = Get-EnabledOptions -OptionValue 0
        }
        else
        {
            $siteLinkOptions = Get-EnabledOptions -OptionValue $siteLink.Options
        }

        $sitesExcludedEvaluated = $SitesExcluded |
            Where-Object -FilterScript { $_ -notin $siteCommonNames }

        $returnValue = @{
            Name                          = $Name
            Cost                          = $siteLink.Cost
            Description                   = $siteLink.Description
            ReplicationFrequencyInMinutes = $siteLink.ReplicationFrequencyInMinutes
            SitesIncluded                 = $siteCommonNames
            SitesExcluded                 = $sitesExcludedEvaluated
            OptionChangeNotification      = $siteLinkOptions.USE_NOTIFY
            OptionTwoWaySync              = $siteLinkOptions.TWOWAY_SYNC
            OptionDisableCompression      = $siteLinkOptions.DISABLE_COMPRESSION
            Ensure                        = 'Present'
        }
    }
    else
    {
        $returnValue = @{
            Name                          = $Name
            Cost                          = $null
            Description                   = $null
            ReplicationFrequencyInMinutes = $null
            SitesIncluded                 = $null
            SitesExcluded                 = $SitesExcluded
            OptionChangeNotification      = $false
            OptionTwoWaySync              = $false
            OptionDisableCompression      = $false
            Ensure                        = 'Absent'
        }
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Sets the desired configuration on an AD Replication Site Link.

    .PARAMETER Name
        Specifies the name of the AD Replication Site Link.

    .PARAMETER Cost
        Specifies the cost to be placed on the site link.

    .PARAMETER Description
        Specifies a description of the object.

    .PARAMETER ReplicationFrequencyInMinutes
        Specifies the frequency (in minutes) for which replication will occur where this site link is in use between sites.

    .PARAMETER SitesIncluded
        Specifies the list of sites included in the site link.

    .PARAMETER SitesExcluded
        Specifies the list of sites to remove from a site link.

    .PARAMETER OptionChangeNotification
        Enables or disables Change Notification Replication on a site link. Default value is $false.

    .PARAMETER OptionTwoWaySync
        Two Way Sync on a site link. Default value is $false.

    .PARAMETER OptionDisableCompression
        Enables or disables Compression on a site link. Default value is $false.

    .PARAMETER Ensure
        Specifies if the site link is created or deleted.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [System.Int32]
        $Cost,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter()]
        [System.Int32]
        $ReplicationFrequencyInMinutes,

        [Parameter()]
        [System.String[]]
        $SitesIncluded,

        [Parameter()]
        [System.String[]]
        $SitesExcluded,

        [Parameter()]
        [System.Boolean]
        $OptionChangeNotification,

        [Parameter()]
        [System.Boolean]
        $OptionTwoWaySync,

        [Parameter()]
        [System.Boolean]
        $OptionDisableCompression,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present'
    )

    if ($Ensure -eq 'Present')
    {
        # Resource should be Present
        $currentADSiteLink = Get-TargetResource -Name $Name

        <#
            Since Set and New have different parameters we have to test if the site link exists to determine what
            cmdlet we need to use.
        #>

        if ( $currentADSiteLink.Ensure -eq 'Absent' )
        {
            # Resource is Absent

            # Modify parameters for splatting to New-ADReplicationSiteLink.
            $newADReplicationSiteLinkParameters = @{} + $PSBoundParameters
            $newADReplicationSiteLinkParameters.Remove('Ensure')
            $newADReplicationSiteLinkParameters.Remove('SitesExcluded')
            $newADReplicationSiteLinkParameters.Remove('OptionChangeNotification')
            $newADReplicationSiteLinkParameters.Remove('OptionTwoWaySync')
            $newADReplicationSiteLinkParameters.Remove('OptionDisableCompression')
            $newADReplicationSiteLinkParameters.Remove('Verbose')

            $optionsValue = ConvertTo-EnabledOptions -OptionChangeNotification $optionChangeNotification `
                -OptionTwoWaySync $optionTwoWaySync -OptionDisableCompression $optionDisableCompression

            if ($optionsValue -gt 0)
            {
                $newADReplicationSiteLinkParameters['OtherAttributes'] = @{
                    options = $optionsValue
                }
            }

            Write-Verbose -Message ($script:localizedData.NewSiteLink -f $Name)
            New-ADReplicationSiteLink @newADReplicationSiteLinkParameters
        }
        else
        {
            # Resource is Present

            $setADReplicationSiteLinkParameters = @{}
            $setADReplicationSiteLinkParameters['Identity'] = $Name

            $replaceParameters = @{}

            # now we have to determine if we need to add or remove sites from SitesIncluded.
            if (-not (Test-Members -ExistingMembers $currentADSiteLink.SitesIncluded `
                        -MembersToInclude $SitesIncluded -MembersToExclude $SitesExcluded))
            {
                # build the SitesIncluded hashtable.
                $sitesIncludedParameters = @{}
                if ($SitesExcluded)
                {
                    Write-Verbose -Message ($script:localizedData.RemovingSites -f $($SitesExcluded -join ', '), $Name)

                    <#
                    Wrapped in $() as we were getting some weird results without it,
                    results were not being added into Hashtable as strings.
                #>

                    $sitesIncludedParameters.Add('Remove', $($SitesExcluded))
                }

                if ($SitesIncluded)
                {
                    Write-Verbose -Message ($script:localizedData.AddingSites -f $($SitesIncluded -join ', '), $Name)

                    <#
                    Wrapped in $() as we were getting some weird results without it,
                    results were not being added into Hashtable as strings.
                #>

                    $sitesIncludedParameters.Add('Add', $($SitesIncluded))
                }

                if ($null -ne $($sitesIncludedParameters.Keys))
                {
                    $setADReplicationSiteLinkParameters['SitesIncluded'] = $sitesIncludedParameters
                }
            }

            if ($PSBoundParameters.ContainsKey('Cost') -and $Cost -ne $currentADSiteLink.Cost)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'Cost', $Cost, $Name)
                $setADReplicationSiteLinkParameters['Cost'] = $Cost
            }

            if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $currentADSiteLink.Description)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'Description', $Description, $Name)
                $setADReplicationSiteLinkParameters['Description'] = $Description
            }

            if ($PSBoundParameters.ContainsKey('ReplicationFrequencyInMinutes') -and
                $ReplicationFrequencyInMinutes -ne $currentADSiteLink.ReplicationFrequencyInMinutes)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'ReplicationFrequencyInMinutes', $ReplicationFrequencyInMinutes, $Name)
                $setADReplicationSiteLinkParameters['ReplicationFrequencyInMinutes'] = $ReplicationFrequencyInMinutes
            }

            if ($PSBoundParameters.ContainsKey('ReplicationFrequencyInMinutes') -and
                $OptionChangeNotification -ne $currentADSiteLink.OptionChangeNotification)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'OptionChangeNotification', $OptionChangeNotification, $Name)
                $desiredChangeNotification = $OptionChangeNotification
            }
            else
            {
                $desiredChangeNotification = $currentADSiteLink.OptionChangeNotification
            }

            if ($PSBoundParameters.ContainsKey('ReplicationFrequencyInMinutes') -and
                $OptionTwoWaySync -ne $currentADSiteLink.OptionTwoWaySync)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'TwoWaySync', $OptionTwoWaySync, $Name)
                $desiredTwoWaySync = $OptionTwoWaySync
            }
            else
            {
                $desiredTwoWaySync = $currentADSiteLink.OptionTwoWaySync
            }

            if ($PSBoundParameters.ContainsKey('ReplicationFrequencyInMinutes') -and
                $OptionDisableCompression -ne $currentADSiteLink.OptionDisableCompression)
            {
                Write-Verbose -Message ($script:localizedData.SettingProperty -f
                    'OptionDisableCompression', $OptionDisableCompression, $Name)
                $desiredDisableCompression = $OptionDisableCompression
            }
            else
            {
                $desiredDisableCompression = $currentADSiteLink.OptionDisableCompression
            }

            $currentOptionsValue = ConvertTo-EnabledOptions `
                -OptionChangeNotification $currentADSiteLink.OptionChangeNotification `
                -OptionTwoWaySync $currentADSiteLink.OptionTwoWaySync `
                -OptionDisableCompression $currentADSiteLink.OptionDisableCompression
            $desiredOptionsValue = ConvertTo-EnabledOptions `
                -OptionChangeNotification $desiredChangeNotification `
                -OptionTwoWaySync $desiredTwoWaySync `
                -OptionDisableCompression $desiredDisableCompression

            if ($currentOptionsValue -ne $desiredOptionsValue)
            {
                if ($desiredoptionsValue -eq 0)
                {
                    $setADReplicationSiteLinkParameters.Add('Clear', 'Options')
                }
                else
                {
                    $replaceParameters.Add('Options', $desiredOptionsValue)
                }
            }

            if ($replaceParameters.Count -gt 0)
            {
                $setADReplicationSiteLinkParameters.Add('Replace', $replaceParameters)
            }

            Set-ADReplicationSiteLink @setADReplicationSiteLinkParameters
        }
    }
    else
    {
        # Resource should be absent

        Write-Verbose -Message ($script:localizedData.RemoveSiteLink -f $Name)

        Remove-ADReplicationSiteLink -Identity $Name
    }
}

<#
    .SYNOPSIS
        Tests if the AD Replication Site Link is in a desired state.

    .PARAMETER Name
        Specifies the name of the AD Replication Site Link.

    .PARAMETER Cost
        Specifies the cost to be placed on the site link.

    .PARAMETER Description
        Specifies a description of the object.

    .PARAMETER ReplicationFrequencyInMinutes
        Specifies the frequency (in minutes) for which replication will occur where this site link is in use between sites.

    .PARAMETER SitesIncluded
        Specifies the list of sites included in the site link.

    .PARAMETER SitesExcluded
        Specifies the list of sites to remove from a site link.

    .PARAMETER OptionChangeNotification
        Enables or disables Change Notification Replication on a site link. Default value is $false.

    .PARAMETER OptionTwoWaySync
        Two Way Sync on a site link. Default value is $false.

    .PARAMETER OptionDisableCompression
        Enables or disables Compression on a site link. Default value is $false.

    .PARAMETER Ensure
        Specifies if the site link is created or deleted.
#>

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

        [Parameter()]
        [System.Int32]
        $Cost,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter()]
        [System.Int32]
        $ReplicationFrequencyInMinutes,

        [Parameter()]
        [System.String[]]
        $SitesIncluded,

        [Parameter()]
        [System.String[]]
        $SitesExcluded,

        [Parameter()]
        [System.Boolean]
        $OptionChangeNotification,

        [Parameter()]
        [System.Boolean]
        $OptionTwoWaySync,

        [Parameter()]
        [System.Boolean]
        $OptionDisableCompression,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present'
    )

    $parameters = @{} + $PSBoundParameters
    $parameters.Remove('Ensure')
    $parameters.Remove('Verbose')
    $parameters.Remove('Debug')

    # Add parameters with default values as they may not be explicitly passed
    $parameters['OptionChangeNotification'] = $OptionChangeNotification
    $parameters['OptionTwoWaySync'] = $OptionTwoWaySync
    $parameters['OptionDisableCompression'] = $OptionDisableCompression

    $targetResource = Get-TargetResource -Name $Name

    $inDesiredState = $true

    if ($targetResource.Ensure -eq 'Present')
    {
        # Resource is Present
        if ($Ensure -eq 'Present')
        {
            # Resource Should be Present
            foreach ($parameter in $parameters.Keys)
            {
                if ($parameter -eq 'SitesIncluded')
                {
                    foreach ($desiredIncludedSite in $SitesIncluded)
                    {
                        if ($desiredIncludedSite -notin $targetResource.SitesIncluded)
                        {
                            Write-Verbose -Message ($script:localizedData.SiteNotFound -f
                                $desiredIncludedSite, $($targetResource.SitesIncluded -join ', '))
                            $inDesiredState = $false
                        }
                    }
                }
                elseif ($parameter -eq 'SitesExcluded')
                {
                    foreach ($desiredExcludedSite in $SitesExcluded)
                    {
                        if ($desiredExcludedSite -in $targetResource.SitesIncluded)
                        {
                            Write-Verbose -Message ($script:localizedData.SiteFoundInExcluded -f
                                $desiredExcludedSite, $($targetResource.SitesIncluded -join ', '))
                            $inDesiredState = $false
                        }
                    }
                }
                elseif ($parameters[$parameter] -ne $targetResource[$parameter])
                {
                    Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f
                        $parameter, $targetResource[$parameter], $parameters[$parameter])
                    $inDesiredState = $false
                }
            }

            if ($inDesiredState)
            {
                # Resource is in desired state
                Write-Verbose -Message ($script:localizedData.ADSiteInDesiredState -f $Name)
            }
            else
            {
                # Resource is not in the desired state
                Write-Verbose -Message ($script:localizedData.ADSiteNotInDesiredState -f $Name)
            }
        }
        else
        {
            # Resource Should be Absent
            Write-Verbose -Message ($script:localizedData.ADSiteIsPresentButShouldBeAbsent -f $Name)

            $inDesiredState = $false
        }
    }
    else
    {
        # Resource is Absent
        if ($Ensure -eq 'Present')
        {
            # Resource Should be Present
            Write-Verbose -Message ($script:localizedData.ADSiteIsAbsentButShouldBePresent -f $Name)

            $inDesiredState = $false
        }
        else
        {
            # Resource should be Absent
            Write-Verbose ($script:localizedData.ADSiteInDesiredState -f $Name)

            $inDesiredState = $true
        }
    }

    return $inDesiredState
}

<#
    .SYNOPSIS
        Resolves the AD replication site link distinguished names to short names

    .PARAMETER SiteName
        Specifies the distinguished name of a AD replication site link

    .EXAMPLE
        PS C:\> Resolve-SiteLinkName -SiteName 'CN=Site1,CN=Sites,CN=Configuration,DC=contoso,DC=com'
        Site1
#>

function Resolve-SiteLinkName
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")]
    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SiteName
    )

    $adSite = Get-ADReplicationSite -Identity $SiteName

    return $adSite.Name
}

<#
    .SYNOPSIS
        Calculates the options enabled on a Site Link

    .PARAMETER OptionValue
        The value of currently enabled options
#>

function Get-EnabledOptions
{
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]

    param
    (
        [Parameter(Mandatory = $true)]
        [System.Int32]
        $OptionValue
    )

    $returnValue = @{
        USE_NOTIFY          = $false
        TWOWAY_SYNC         = $false
        DISABLE_COMPRESSION = $false
    }

    if (1 -band $optionValue)
    {
        $returnValue.USE_NOTIFY = $true
    }

    if (2 -band $optionValue)
    {
        $returnValue.TWOWAY_SYNC = $true
    }

    if (4 -band $optionValue)
    {
        $returnValue.DISABLE_COMPRESSION = $true
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Calculates the options value for the given choices

    .PARAMETER OptionChangeNotification
        Enable/Disable Change notification replication

    .PARAMETER OptionTwoWaySync
        Enable/Disable Two Way sync

    .PARAMETER OptionDisableCompression
        Enable/Disable Compression
#>

function ConvertTo-EnabledOptions
{
    [OutputType([System.Int32])]
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Boolean]
        $OptionChangeNotification,

        [Parameter()]
        [System.Boolean]
        $OptionTwoWaySync,

        [Parameter()]
        [System.Boolean]
        $OptionDisableCompression
    )

    $returnValue = 0

    if ($OptionChangeNotification)
    {
        $returnValue = $returnValue + 1
    }

    if ($OptionTwoWaySync)
    {
        $returnValue = $returnValue + 2
    }

    if ($OptionDisableCompression)
    {
        $returnValue = $returnValue + 4
    }

    return $returnValue
}

Export-ModuleMember -Function *-TargetResource