DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.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'

# This file is used to remember the number of times the node has been rebooted.
$script:restartLogFile = Join-Path $env:temp -ChildPath 'WaitForADDomain_Reboot.tmp'

# This scriptblock is ran inside the background job.
$script:waitForDomainControllerScriptBlock = {
    param
    (
        # Only used for unit tests, and debug purpose.
        [Parameter()]
        [System.Boolean]
        $RunOnce,

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

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

    $domainFound = $false

    do
    {
        Import-Module ActiveDirectoryDsc

        $findDomainControllerParameters = @{
            DomainName = $DomainName
        }

        if ($SiteName)
        {
            $findDomainControllerParameters['SiteName'] = $SiteName

        }

        if ($null -ne $Credential)
        {
            $findDomainControllerParameters['Credential'] = $Credential
        }

        if ($PSBoundParameters.ContainsKey('WaitForValidCredentials'))
        {
            $findDomainControllerParameters['WaitForValidCredentials'] = $WaitForValidCredentials
        }

        $currentDomainController = $null

        # Using verbose so that Receive-Job can output whats happened.
        $currentDomainController = Find-DomainController @findDomainControllerParameters -Verbose

        if ($currentDomainController)
        {
            $domainFound = $true
        }
        else
        {
            $domainFound = $false

            # Using verbose so that Receive-Job can output whats happened.
            Clear-DnsClientCache -Verbose

            Start-Sleep -Seconds 10
        }
    } until ($domainFound -or $RunOnce)
}

<#
    .SYNOPSIS
        Returns the current state of the specified Active Directory domain.

    .PARAMETER DomainName
        Specifies the fully qualified domain name to wait for.

    .PARAMETER SiteName
        Specifies the site in the domain where to look for a domain controller.

    .PARAMETER Credential
        Specifies the credentials that are used when accessing the domain,
        unless the built-in PsDscRunAsCredential is used.

    .PARAMETER WaitTimeout
        Specifies the timeout in seconds that the resource will wait for the
        domain to be accessible. Default value is 300 seconds.

    .PARAMETER RestartCount
        Specifies the number of times the node will be reboot in an effort to
        connect to the domain.

    .PARAMETER WaitForValidCredentials
        Specifies that the resource will not throw an error if authentication
        fails using the provided credentials and continue wait for the timeout.
        This can be used if the credentials are known to eventually exist but
        there are a potential timing issue before they are accessible.
#>

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.UInt64]
        $WaitTimeout = 300,

        [Parameter()]
        [System.UInt32]
        $RestartCount,

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

    $findDomainControllerParameters = @{
        DomainName = $DomainName
    }

    Write-Verbose -Message (
        $script:localizedData.SearchDomainController -f $DomainName
    )

    if ($PSBoundParameters.ContainsKey('SiteName'))
    {
        $findDomainControllerParameters['SiteName'] = $SiteName

        Write-Verbose -Message (
            $script:localizedData.SearchInSiteOnly -f $SiteName
        )
    }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
        $cimCredentialInstance = New-CimCredentialInstance -Credential $Credential

        $findDomainControllerParameters['Credential'] = $Credential

        Write-Verbose -Message (
            $script:localizedData.ImpersonatingCredentials -f $Credential.UserName
        )
    }
    else
    {
        if ($null -ne $global:PsDscContext.RunAsUser)
        {
            # Running using PsDscRunAsCredential
            Write-Verbose -Message (
                $script:localizedData.ImpersonatingCredentials -f $global:PsDscContext.RunAsUser
            )
        }
        else
        {
            # Running as SYSTEM or current user.
            Write-Verbose -Message (
                $script:localizedData.ImpersonatingCredentials -f (Get-CurrentUser).Name
            )
        }

        $cimCredentialInstance = $null
    }

    $currentDomainController = $null

    if ($PSBoundParameters.ContainsKey('WaitForValidCredentials'))
    {
        $findDomainControllerParameters['WaitForValidCredentials'] = $WaitForValidCredentials
    }

    $currentDomainController = Find-DomainController @findDomainControllerParameters

    if ($currentDomainController)
    {
        $domainFound = $true
        $domainControllerSiteName = $currentDomainController.SiteName

        Write-Verbose -Message $script:localizedData.FoundDomainController

    }
    else
    {
        $domainFound = $false
        $domainControllerSiteName = $null

        Write-Verbose -Message $script:localizedData.NoDomainController
    }

    return @{
        DomainName              = $DomainName
        SiteName                = $domainControllerSiteName
        Credential              = $cimCredentialInstance
        WaitTimeout             = $WaitTimeout
        RestartCount            = $RestartCount
        IsAvailable             = $domainFound
        WaitForValidCredentials = $WaitForValidCredentials
    }
}

<#
    .SYNOPSIS
        Waits for the specified Active Directory domain to have a domain
        controller that can serve connections.

    .PARAMETER DomainName
        Specifies the fully qualified domain name to wait for.

    .PARAMETER SiteName
        Specifies the site in the domain where to look for a domain controller.

    .PARAMETER Credential
        Specifies the credentials that are used when accessing the domain,
        unless the built-in PsDscRunAsCredential is used.

    .PARAMETER WaitTimeout
        Specifies the timeout in seconds that the resource will wait for the
        domain to be accessible. Default value is 300 seconds.

    .PARAMETER RestartCount
        Specifies the number of times the node will be reboot in an effort to
        connect to the domain.

    .PARAMETER WaitForValidCredentials
        Specifies that the resource will not throw an error if authentication
        fails using the provided credentials and continue wait for the timeout.
        This can be used if the credentials are known to eventually exist but
        there are a potential timing issue before they are accessible.
#>

function Set-TargetResource
{
    <#
        Suppressing this rule because $global:DSCMachineStatus is used to trigger
        a reboot if the domain name cannot be found withing the timeout period.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DomainName,

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.UInt64]
        $WaitTimeout = 300,

        [Parameter()]
        [System.UInt32]
        $RestartCount,

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

    Write-Verbose -Message (
        $script:localizedData.WaitingForDomain -f $DomainName, $WaitTimeout
    )

    # Only pass properties that could be used when fetching the domain controller.
    $compareTargetResourceStateParameters = @{
        DomainName              = $DomainName
        SiteName                = $SiteName
        Credential              = $Credential
        WaitForValidCredentials = $WaitForValidCredentials
    }

    <#
        Removes any keys not bound to $PSBoundParameters.
        Need the @() around this to get a new array to enumerate.
    #>

    @($compareTargetResourceStateParameters.Keys) | ForEach-Object {
        if (-not $PSBoundParameters.ContainsKey($_))
        {
            $compareTargetResourceStateParameters.Remove($_)
        }
    }

    <#
        This returns array of hashtables which contain the properties ParameterName,
        Expected, Actual, and InDesiredState. In this case only the property
        'IsAvailable' will be returned.
    #>

    $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters

    $isInDesiredState = $compareTargetResourceStateResult.Where( { $_.ParameterName -eq 'IsAvailable' }).InDesiredState

    if (-not $isInDesiredState)
    {
        $startJobParameters = @{
            ScriptBlock  = $script:waitForDomainControllerScriptBlock
            ArgumentList = @(
                $false
                $DomainName
                $SiteName
                $Credential
                $WaitForValidCredentials
            )
        }

        Write-Verbose -Message $script:localizedData.StartBackgroundJob

        $jobSearchDomainController = Start-Job @startJobParameters

        Write-Verbose -Message $script:localizedData.WaitBackgroundJob

        $waitJobResult = Wait-Job -Job $jobSearchDomainController -Timeout $WaitTimeout

        # Wait-Job returns an object if the job completed or failed within the timeout.
        if ($waitJobResult)
        {
            Write-Verbose -Message $script:localizedData.BackgroundJobFinished
            switch ($waitJobResult.State)
            {
                'Failed'
                {
                    Write-Warning -Message $script:localizedData.BackgroundJobFailed

                    $foundDomainController = $false
                }

                'Completed'
                {
                    Write-Verbose -Message $script:localizedData.BackgroundJobSuccessful

                    if ($PSBoundParameters.ContainsKey('RestartCount'))
                    {
                        Remove-RestartLogFile
                    }

                    $foundDomainController = $true
                }
            }
        }
        else
        {
            Write-Warning -Message $script:localizedData.TimeoutReached

            if ($PSBoundParameters.ContainsKey('RestartCount'))
            {
                # if the file does not exist this will set $currentRestartCount to 0.
                [System.UInt32] $currentRestartCount = Get-Content $restartLogFile -ErrorAction SilentlyContinue

                if ($currentRestartCount -lt $RestartCount)
                {
                    $currentRestartCount += 1

                    Set-Content -Path $restartLogFile -Value $currentRestartCount

                    Write-Verbose -Message (
                        $script:localizedData.RestartWasRequested -f $currentRestartCount, $RestartCount
                    )

                    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '',
                        Justification = 'Set LCM DSCMachineStatus to indicate reboot required')]
                    $global:DSCMachineStatus = 1
                }
            }

            # The timeout was reached and no restarts was requested.
            $foundDomainController = $false
        }

        # Only output the result from the running job if Verbose was chosen.
        if ($PSBoundParameters.ContainsKey('Verbose') -or $waitJobResult.State -eq 'Failed')
        {
            Write-Verbose -Message $script:localizedData.StartOutputBackgroundJob

            Receive-Job -Job $jobSearchDomainController

            Write-Verbose -Message $script:localizedData.EndOutputBackgroundJob
        }

        Write-Verbose -Message $script:localizedData.RemoveBackgroundJob

        # Forcedly remove the job even if it was not completed.
        Remove-Job -Job $jobSearchDomainController -Force
    }
    else
    {
        $foundDomainController = $true
    }

    if ($foundDomainController)
    {
        Write-Verbose -Message ($script:localizedData.DomainInDesiredState -f $DomainName)
    }
    else
    {
        throw $script:localizedData.NoDomainController
    }
}

<#
    .SYNOPSIS
        Determines if the specified Active Directory domain have a domain controller
        that can serve connections.

    .PARAMETER DomainName
        Specifies the fully qualified domain name to wait for.

    .PARAMETER SiteName
        Specifies the site in the domain where to look for a domain controller.

    .PARAMETER Credential
        Specifies the credentials that are used when accessing the domain,
        unless the built-in PsDscRunAsCredential is used.

    .PARAMETER WaitTimeout
        Specifies the timeout in seconds that the resource will wait for the
        domain to be accessible. Default value is 300 seconds.

    .PARAMETER RestartCount
        Specifies the number of times the node will be reboot in an effort to
        connect to the domain.

    .PARAMETER WaitForValidCredentials
        Specifies that the resource will not throw an error if authentication
        fails using the provided credentials and continue wait for the timeout.
        This can be used if the credentials are known to eventually exist but
        there are a potential timing issue before they are accessible.
#>

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.UInt64]
        $WaitTimeout = 300,

        [Parameter()]
        [System.UInt32]
        $RestartCount,

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

    Write-Verbose -Message (
        $script:localizedData.TestConfiguration -f $DomainName
    )

    # Only pass properties that could be used when fetching the domain controller.
    $compareTargetResourceStateParameters = @{
        DomainName              = $DomainName
        SiteName                = $SiteName
        Credential              = $Credential
        WaitForValidCredentials = $WaitForValidCredentials
    }

    <#
        Removes any keys not bound to $PSBoundParameters.
        Need the @() around this to get a new array to enumerate.
    #>

    @($compareTargetResourceStateParameters.Keys) | ForEach-Object {
        if (-not $PSBoundParameters.ContainsKey($_))
        {
            $compareTargetResourceStateParameters.Remove($_)
        }
    }

    <#
        This returns array of hashtables which contain the properties ParameterName,
        Expected, Actual, and InDesiredState. In this case only the property
        'IsAvailable' will be returned.
    #>

    $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters

    if ($false -in $compareTargetResourceStateResult.InDesiredState)
    {
        $testTargetResourceReturnValue = $false

        Write-Verbose -Message (
            $script:localizedData.DomainNotInDesiredState -f $DomainName
        )
    }
    else
    {
        $testTargetResourceReturnValue = $true

        if ($PSBoundParameters.ContainsKey('RestartCount') -and $RestartCount -gt 0 )
        {
            Remove-RestartLogFile
        }

        Write-Verbose -Message (
            $script:localizedData.DomainInDesiredState -f $DomainName
        )
    }

    return $testTargetResourceReturnValue
}

<#
    .SYNOPSIS
        Compares the properties in the current state with the properties of the
        desired state and returns a hashtable with the comparison result.

    .PARAMETER DomainName
        Specifies the fully qualified domain name to wait for.

    .PARAMETER SiteName
        Specifies the site in the domain where to look for a domain controller.

    .PARAMETER Credential
        Specifies the credentials that are used when accessing the domain,
        unless the built-in PsDscRunAsCredential is used.

    .PARAMETER WaitForValidCredentials
        Specifies that the resource will not throw an error if authentication
        fails using the provided credentials and continue wait for the timeout.
        This can be used if the credentials are known to eventually exist but
        there are a potential timing issue before they are accessible.
#>

function Compare-TargetResourceState
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DomainName,

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

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

    $getTargetResourceParameters = @{
        DomainName              = $DomainName
        SiteName                = $SiteName
        Credential              = $Credential
        WaitForValidCredentials = $WaitForValidCredentials
    }

    <#
        Removes any keys not bound to $PSBoundParameters.
        Need the @() around this to get a new array to enumerate.
    #>

    @($getTargetResourceParameters.Keys) | ForEach-Object {
        if (-not $PSBoundParameters.ContainsKey($_))
        {
            $getTargetResourceParameters.Remove($_)
        }
    }

    $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters

    <#
        Only interested in the read-only property IsAvailable, which
        should always be compared to the value $true.
    #>

    $compareResourcePropertyStateParameters = @{
        CurrentValues = $getTargetResourceResult
        DesiredValues = @{
            IsAvailable = $true
        }
        Properties    = 'IsAvailable'
    }

    return Compare-ResourcePropertyState @compareResourcePropertyStateParameters
}

function Remove-RestartLogFile
{
    [CmdletBinding()]
    param ()

    if (Test-Path -Path $script:restartLogFile)
    {
        Remove-Item $script:restartLogFile -Force -ErrorAction SilentlyContinue
    }
}