DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.psm1

$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common'
$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common'

Import-Module -Name $script:dscResourceCommonPath
Import-Module -Name $script:dnsServerDscCommonPath

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

<#
    .SYNOPSIS
        Get the state of a conditional forwarder.
 
    .DESCRIPTION
        DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder.
 
    .PARAMETER Ensure
        Ensure whether the zone is absent or present.
 
    .PARAMETER Name
        The name of the zone to manage.
 
    .PARAMETER MasterServers
        The IP addresses the forwarder should use. Mandatory if Ensure is present.
 
    .PARAMETER ReplicationScope
        Whether the conditional forwarder should be replicated in AD, and the scope of that replication.
 
        Valid values are:
 
            * None: (file based / not replicated)
            * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set.
            * Domain: DomainDnsZones
            * Forest: ForestDnsZones
            * Legacy: The domain partition (defaultNamingContext).
 
    .PARAMETER DirectoryPartitionName
        The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes.
 
#>

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

    $targetResource = @{
        Ensure                 = 'Absent'
        Name                   = $Name
        MasterServers          = $null
        ReplicationScope       = $null
        DirectoryPartitionName = $null
        ZoneType               = $null
    }

    $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue
    if ($zone)
    {
        Write-Verbose ($script:localizedData.FoundZone -f @(
            $zone.ZoneType
            $Name
        ))

        $targetResource.ZoneType = $zone.ZoneType
    }
    if ($zone -and $zone.ZoneType -eq 'Forwarder')
    {
        $targetResource.Ensure = 'Present'
        $targetResource.MasterServers = $zone.MasterServers

        if ($zone.IsDsIntegrated)
        {
            $targetResource.ReplicationScope = $zone.ReplicationScope
            $targetResource.DirectoryPartitionName = $zone.DirectoryPartitionName
        }
        else
        {
            $targetResource.ReplicationScope = 'None'
        }
    }
    else
    {
        Write-Verbose ($script:localizedData.CouldNotFindZone -f $Name)
    }

    $targetResource
}

<#
    .SYNOPSIS
        Set the state of a conditional forwarder.
 
    .DESCRIPTION
        DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder.
 
    .PARAMETER Ensure
        Ensure whether the zone is absent or present.
 
    .PARAMETER Name
        The name of the zone to manage.
 
    .PARAMETER MasterServers
        The IP addresses the forwarder should use. Mandatory if Ensure is present.
 
    .PARAMETER ReplicationScope
        Whether the conditional forwarder should be replicated in AD, and the scope of that replication.
 
        Valid values are:
 
            * None: (file based / not replicated)
            * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set.
            * Domain: DomainDnsZones
            * Forest: ForestDnsZones
            * Legacy: The domain partition (defaultNamingContext).
 
    .PARAMETER DirectoryPartitionName
        The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes.
 
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateSet('Absent', 'Present')]
        [System.String]
        $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [String[]]
        $MasterServers,

        [Parameter()]
        [ValidateSet('None', 'Custom', 'Domain', 'Forest', 'Legacy')]
        [System.String]
        $ReplicationScope = 'None',

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

    Test-DscDnsServerConditionalForwarderParameter

    $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue
    if ($Ensure -eq 'Present')
    {
        $params = @{
            Name          = $Name
            MasterServers = $MasterServers
        }

        if ($zone)
        {
            # File <--> DsIntegrated requires create and destroy
            if ($zone.ZoneType -ne 'Forwarder' -or
                ($zone.IsDsIntegrated -and $ReplicationScope -eq 'None') -or
                (-not $zone.IsDsIntegrated -and $ReplicationScope -ne 'None'))
            {
                Remove-DnsServerZone -Name $Name

                Write-Verbose ($script:localizedData.RecreateZone -f @(
                    $zone.ZoneType
                    $Name
                ))

                $zone = $null
            }
            else
            {
                if ("$($zone.MasterServers)" -ne "$MasterServers")
                {
                    Write-Verbose ($script:localizedData.UpdatingMasterServers -f @(
                        $Name
                        ($MasterServers -join ', ')
                    ))

                    $null = Set-DnsServerConditionalForwarderZone @params
                }
            }
        }

        $params = @{
            Name = $Name
        }
        if ($ReplicationScope -ne 'None')
        {
            $params.ReplicationScope = $ReplicationScope
        }
        if ($ReplicationScope -eq 'Custom' -and
            $DirectoryPartitionName -and
            $zone.DirectoryPartitionName -ne $DirectoryPartitionName)
        {
            $params.ReplicationScope = 'Custom'
            $params.DirectoryPartitionName = $DirectoryPartitionName
        }

        if ($zone)
        {
            if (($params.ReplicationScope -and $params.ReplicationScope -ne $zone.ReplicationScope) -or $params.DirectoryPartitionName)
            {
                Write-Verbose ($script:localizedData.MoveADZone -f @(
                    $Name
                    $ReplicationScope
                ))

                $null = Set-DnsServerConditionalForwarderZone @params
            }
        }
        else
        {
            Write-Verbose ($script:localizedData.NewZone -f $Name)

            $params.MasterServers = $MasterServers
            $null = Add-DnsServerConditionalForwarderZone @params
        }
    }
    elseif ($Ensure -eq 'Absent')
    {
        if ($zone -and $zone.ZoneType -eq 'Forwarder')
        {
            Write-Verbose ($script:localizedData.RemoveZone -f $Name)

            Remove-DnsServerZone -Name $Name
        }
    }
}

<#
    .SYNOPSIS
        Test the state of a conditional forwarder.
 
    .DESCRIPTION
        DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder.
 
    .PARAMETER Ensure
        Ensure whether the zone is absent or present.
 
    .PARAMETER Name
        The name of the zone to manage.
 
    .PARAMETER MasterServers
        The IP addresses the forwarder should use. Mandatory if Ensure is present.
 
    .PARAMETER ReplicationScope
        Whether the conditional forwarder should be replicated in AD, and the scope of that replication.
 
        Valid values are:
 
            * None: (file based / not replicated)
            * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set.
            * Domain: DomainDnsZones
            * Forest: ForestDnsZones
            * Legacy: The domain partition (defaultNamingContext).
 
    .PARAMETER DirectoryPartitionName
        The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes.
 
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([Boolean])]
    param
    (
        [Parameter()]
        [ValidateSet('Absent', 'Present')]
        [System.String]
        $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [String[]]
        $MasterServers,

        [Parameter()]
        [ValidateSet('None', 'Custom', 'Domain', 'Forest', 'Legacy')]
        [System.String]
        $ReplicationScope = 'None',

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

    Test-DscDnsServerConditionalForwarderParameter

    $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue
    if ($Ensure -eq 'Present')
    {
        if (-not $zone)
        {
            Write-Verbose ($script:localizedData.ZoneDoesNotExist -f $Name)

            return $false
        }

        if ($zone.ZoneType -ne 'Forwarder')
        {
            Write-Verbose ($script:localizedData.IncorrectZoneType -f @(
                $Name
                $zone.ZoneType
            ))

            return $false
        }

        if ($zone.IsDsIntegrated -and $ReplicationScope -eq 'None')
        {
            Write-Verbose ($script:localizedData.ZoneIsDsIntegrated -f $Name)

            return $false
        }

        if (-not $zone.IsDsIntegrated -and $ReplicationScope -ne 'None')
        {
            Write-Verbose ($script:localizedData.ZoneIsFileBased -f $Name)

            return $false
        }

        if ($ReplicationScope -ne 'None' -and $zone.ReplicationScope -ne $ReplicationScope)
        {
            Write-Verbose ($script:localizedData.ReplicationScopeDoesNotMatch -f @(
                $Name
                $zone.ReplicationScope
                $ReplicationScope
            ))

            return $false
        }

        if ($ReplicationScope -eq 'Custom' -and $zone.DirectoryPartitionName -ne $DirectoryPartitionName)
        {
            Write-Verbose ($script:localizedData.DirectoryPartitionDoesNotMatch -f @(
                $Name
                $DirectoryPartitionName
            ))

            return $false
        }

        <#
            Compares two joined arrays. Arrays are joined using the output field separator.
 
            If the elements are not in the same order the configuration is considered to be different.
 
            Equivalent to Compare-Object -SyncWindow 0 without the need for null checking of reference or difference.
 
            Element order is considered important as it affects name resolution.
 
            https://support.microsoft.com/en-us/help/2834250/net-dns-forwarders-and-conditional-forwarders-resolution-timeouts
        #>

        if ("$($zone.MasterServers)" -ne "$MasterServers")
        {
            Write-Verbose ($script:localizedData.MasterServersDoNotMatch -f @(
                $Name
                ($MasterServers -join ', ')
                ($zone.MasterServers -join ', ')
            ))

            return $false
        }
    }
    elseif ($Ensure -eq 'Absent')
    {
        if ($zone -and $zone.ZoneType -eq 'Forwarder')
        {
            Write-Verbose ($script:localizedData.ZoneExists -f $Name)

            return $false
        }
    }

    return $true
}

function Test-DscDnsServerConditionalForwarderParameter
{
    <#
    .SYNOPSIS
        Tests the parameter combinations required by this resource.
    .DESCRIPTION
        Tests the parameter combinations required by this resource.
    #>


    [CmdletBinding()]
    param ()

    $invocationInfo = Get-Variable MyInvocation -Scope 1 -ValueOnly

    if (-not $invocationInfo.BoundParameters.ContainsKey('Ensure') -or $invocationInfo.BoundParameters['Ensure'] -eq 'Present')
    {
        if ($null -eq $invocationInfo.BoundParameters['MasterServers'] -or $invocationInfo.BoundParameters['MasterServers'].Count -eq 0)
        {
            $pscmdlet.ThrowTerminatingError((
                New-Object System.Management.Automation.ErrorRecord(
                    (New-Object System.ArgumentException($script:localizedData.MasterServersIsMandatory)),
                    'MasterServersIsMandatory',
                    'InvalidArgument',
                    $null
                )
            ))
        }

        if ($invocationInfo.BoundParameters['ReplicationScope'] -eq 'Custom' -and -not $invocationInfo.BoundParameters['DirectoryPartitionName'])
        {
            $pscmdlet.ThrowTerminatingError((
                New-Object System.Management.Automation.ErrorRecord(
                    (New-Object System.ArgumentException($script:localizedData.DirectoryPartitionNameIsMandatory)),
                    'DirectoryPartitionNameIsMandatory',
                    'InvalidArgument',
                    $null
                )
            ))
        }
    }
}