DSCResources/MSFT_xExchWaitForADPrep/MSFT_xExchWaitForADPrep.psm1

function Get-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Identity,

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

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

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

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

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

        [Parameter()]
        [System.UInt32]
        $RetryIntervalSec = 60,

        [Parameter()]
        [System.UInt32]
        $RetryCount = 30
    )

    LogFunctionEntry -VerbosePreference $VerbosePreference

    $dse = GetADRootDSE -Credential $Credential

    if ($PSBoundParameters.ContainsKey('SchemaVersion'))
    {
        #Check for existence of schema object
        $schemaObj = GetADObject -Credential $credential -DistinguishedName "CN=ms-Exch-Schema-Version-Pt,$($dse.schemaNamingContext)" -Properties 'rangeUpper'

        if ($null -ne $schemaObj)
        {
            $currentSchemaVersion = $schemaObj.rangeUpper
        }
        else
        {
            Write-Warning "Unable to find schema object 'CN=ms-Exch-Schema-Version-Pt,$($dse.schemaNamingContext)'. This is either because Exchange /PrepareSchema has not been run, or because the configured account does not have permissions to access this object."
        }
    }

    if ($PSBoundParameters.ContainsKey('OrganizationVersion'))
    {
        $exchangeContainer = GetADObject -Credential $credential -DistinguishedName "CN=Microsoft Exchange,CN=Services,$($dse.configurationNamingContext)" -Properties 'rangeUpper'

        if ($null -ne $exchangeContainer)
        {
            $orgContainer = GetADObject -Credential $Credential -Searching $true -DistinguishedName "CN=Microsoft Exchange,CN=Services,$($dse.configurationNamingContext)" -Properties 'objectVersion' -Filter "objectClass -like 'msExchOrganizationContainer'" -SearchScope 'OneLevel'

            if ($null -ne $orgContainer)
            {
                $currentOrganizationVersion = $orgContainer.objectVersion
            }
            else
            {
                Write-Warning "Unable to find any objects of class msExchOrganizationContainer under 'CN=Microsoft Exchange,CN=Services,$($dse.configurationNamingContext)'. This is either because Exchange /PrepareAD has not been run, or because the configured account does not have permissions to access this object."
            }
        }
        else
        {
            Write-Warning "Unable to find Exchange Configuration Container at 'CN=Microsoft Exchange,CN=Services,$($dse.configurationNamingContext)'. This is either because Exchange /PrepareAD has not been run, or because the configured account does not have permissions to access this object."
        }  
    }

    if ($PSBoundParameters.ContainsKey('DomainVersion'))
    {
        #Get this server's domain
        [System.String]$machineDomain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain.ToLower()

        #Figure out all domains we need to inspect
        [System.String[]]$targetDomains = @()
        $targetDomains += $machineDomain

        if ($null -ne $ExchangeDomains)
        {
            foreach ($domain in $ExchangeDomains)
            {
                $domainLower = $domain.ToLower()

                if ($targetDomains.Contains($domainLower) -eq $false)
                {
                    $targetDomains += $domainLower
                }
            }
        }

        #Populate the return value in a hashtable of domains and versions
        [Hashtable]$currentDomainVersions = @{}

        foreach ($domain in $targetDomains)
        {
            $domainDn = DomainDNFromFQDN -Fqdn $domain

            $mesoContainer = GetADObject -Credential $Credential -DistinguishedName "CN=Microsoft Exchange System Objects,$($domainDn)" -Properties 'objectVersion'

            $mesoVersion = $null

            if ($null -ne $mesoContainer)
            {
                $mesoVersion = $mesoContainer.objectVersion
            }
            else
            {
                Write-Warning "Unable to find object with DN 'CN=Microsoft Exchange System Objects,$($domainDn)'. This is either because Exchange /PrepareDomain has not been run for this domain, or because the configured account does not have permissions to access this object."
            }

            if ($null -eq $currentDomainVersions)
            {
                $currentDomainVersions = @{$domain = $mesoVersion}
            }
            else
            {
                $currentDomainVersions.Add($domain, $mesoVersion)
            }
        }
    }

    $returnValue = @{
        SchemaVersion = $currentSchemaVersion
        OrganizationVersion = $currentOrganizationVersion
        DomainVersion = $currentDomainVersions
    }    

    $returnValue
}

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

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

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

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

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

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

        [Parameter()]
        [System.UInt32]
        $RetryIntervalSec = 60,

        [Parameter()]
        [System.UInt32]
        $RetryCount = 30
    )

    LogFunctionEntry -VerbosePreference $VerbosePreference

    $testResults = Test-TargetResource @PSBoundParameters

    for ($i = 0; $i -lt $RetryCount; $i++)
    {
        if ($testResults -eq $false)
        {
            Write-Verbose "AD has still not been fully prepped as of $([DateTime]::Now). Sleeping for $($RetryIntervalSec) seconds."
            Start-Sleep -Seconds $RetryIntervalSec

            $testResults = Test-TargetResource @PSBoundParameters
        }
        else
        {
            break
        }
    }
    
    if ($testResults -eq $false)
    {
        throw 'AD has still not been prepped after the maximum amount of retries.'
    }
}

function Test-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Identity,

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

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

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

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

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

        [Parameter()]
        [System.UInt32]
        $RetryIntervalSec = 60,

        [Parameter()]
        [System.UInt32]
        $RetryCount = 30
    )

    LogFunctionEntry -VerbosePreference $VerbosePreference

    $adStatus = Get-TargetResource @PSBoundParameters

    $returnValue = $true

    if ($null -eq $adStatus)
    {
        $returnValue = $false
    }
    else
    {
        if (!(VerifySetting -Name 'SchemaVersion' -Type 'Int' -ExpectedValue $SchemaVersion -ActualValue $adStatus.SchemaVersion -PSBoundParametersIn $PSBoundParameters -VerbosePreference $VerbosePreference))
        {
            $returnValue = $false
        }

        if (!(VerifySetting -Name 'OrganizationVersion' -Type 'Int' -ExpectedValue $OrganizationVersion -ActualValue $adStatus.OrganizationVersion -PSBoundParametersIn $PSBoundParameters -VerbosePreference $VerbosePreference))
        {
            $returnValue = $false
        }

        if ($PSBoundParameters.ContainsKey('DomainVersion'))
        {
            #Get this server's domain
            [System.String]$machineDomain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain.ToLower()

            #Figure out all domains we need to inspect
            [System.String[]]$targetDomains = @()
            $targetDomains += $machineDomain

            if ($null -ne $ExchangeDomains)
            {
                foreach ($domain in $ExchangeDomains)
                {
                    $domainLower = $domain.ToLower()

                    if ($targetDomains.Contains($domainLower) -eq $false)
                    {
                        $targetDomains += $domainLower
                    }
                }
            }
            
            #Compare the desired DomainVersion with the actual version of each domain
            foreach ($domain in $targetDomains)
            {
                if (!(VerifySetting -Name 'DomainVersion' -Type 'Int' -ExpectedValue $DomainVersion -ActualValue $adStatus.DomainVersion[$domain] -PSBoundParametersIn $PSBoundParameters -VerbosePreference $VerbosePreference))
                {
                    $returnValue = $false
                }
            }       
        }
    }

    return $returnValue
}

function GetADRootDSE
{
    param
    (
        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    if ($null -eq $Credential)
    {
        $dse = Get-ADRootDSE -ErrorAction SilentlyContinue -ErrorVariable errVar
    }
    else
    {
        $dse = Get-ADRootDSE -Credential $Credential -ErrorAction SilentlyContinue -ErrorVariable errVar
    }

    return $dse
}

function GetADObject
{
    param
    (
        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter()]
        [System.Boolean]
        $Searching = $false,

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

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

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

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

    if ($Searching -eq $false)
    {
        $getAdObjParams = @{'Identity' = $DistinguishedName}
    }
    else
    {
        $getAdObjParams = @{'SearchBase' = $DistinguishedName}

        if ([System.String]::IsNullOrEmpty($Filter) -eq $false)
        {
            $getAdObjParams.Add('Filter', $Filter)
        }

        if ([System.String]::IsNullOrEmpty($SearchScope) -eq $false)
        {
            $getAdObjParams.Add('SearchScope', $SearchScope)
        }
    }

    if ($null -ne $Credential)
    {
        $getAdObjParams.Add('Credential', $Credential)
    }

    if ([System.String]::IsNullOrEmpty($Properties) -eq $false)
    {
        $getAdObjParams.Add('Properties', $Properties)
    }

    #ErrorAction SilentlyContinue doesn't seem to work with Get-ADObject. Doing in Try/Catch instead
    try
    {
        $object = Get-ADObject @getAdObjParams
    }
    catch
    {
        Write-Warning "Failed to find object at '$DistinguishedName' using Get-ADObject."
    }

    return $object
}

function DomainDNFromFQDN
{
    param
    (
        [Parameter()]
        [System.String]
        $Fqdn
    )

    if ($Fqdn.Contains('.'))
    {
        $domainParts = $Fqdn.Split('.')

        $domainDn = "DC=$($domainParts[0])"

        for ($i = 1; $i -lt $domainParts.Count; $i++)
        {
            $domainDn = "$($domainDn),DC=$($domainParts[$i])"
        }
    }
    elseif ($Fqdn.Length -gt 0)
    {
        $domainDn = "DC=$($Fqdn)"
    }
    else
    {
        throw 'Empty value specified for domain name'
    }

    return $domainDn
}

Export-ModuleMember -Function *-TargetResource