Public/Get-ADLocalDC.ps1

#Requires -Modules ps_Module_ActiveDirectoryTools

Function Get-ADLocalDC() {
    <#
        .SYNOPSIS
        This function will attempt to return an available domain controller in the same Active Directory site as the computer calling the function.
 
        It provides functionality to use either .net calls (default) or the ActiveDirectory module for Powershell cmdlet to validate the availability of the Domain Controller
 
        IMPORTANT: This function relies on the function Get-ADComputerSite which is available in this module
 
        .PARAMETER UsePsAdCommand
        Optional. Default = no. This will use the PowerShell module for ActiveDirectory command "Get-ADDomain" to validate the domain controller availability. This is useful for testing AD WebServices availability. If this parameter is not specified the domain controller will be validated using the .net System.DirectoryServices.DirectoryEntry class.
 
        .PARAMETER Verbose
        Optional. This will print the steps taken to discover the DC in the local site
 
        .DESCRIPTION
        This function uses the .net class System.DirectoryServices.ActiveDirectory.DomainController to discover the domain controllers in the AD site. Each DC is tested in turn until a valid Domain Controller has been discovered.
 
        .INPUTS
        See parameters.
 
        .OUTPUTS
        This function will return a string containing an available domain controller located in the same AD site as the computer calling the function.
 
        .EXAMPLE
        Import-Module ps_Module_ActiveDirectoryTools
 
        $sDomainController = Get-ADLocalDC
 
        .NOTES
        MVogwell - October 2022 - Version 1.0
 
        Version history:
            1.0 - Initial tested release
            1.1 - Updated to remove .net calls to enable pester testing
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory=$false)][switch]$UsePsAdCommand
    )

    $ErrorActionPreference = "Stop"

    # This function attempts to discover an available domain controller in the local AD site
    # The UsePsADCommand

    $sRtnDcName = ""

    # Get local AD site name - call function to get this. This could trigger an error
    $sAdSite = Get-ADComputerSite

    # Confirm the local AD site name could be discovered
    if ($null -eq $sAdSite) {
        throw "Failed to discover local AD site name"
    }

    # Call Get-ADDomainControllersBySite function to return the Domain Controllers within a specific site
    # The function will be loaded by importing the base module.
    $arrDomainControllers = Get-ADDomainControllersBySite -sAdSite $sAdSite

    # Confirm at least one DC has been returned
    if ($null -eq $arrDomainControllers) {
        throw "Failed to discover any domain controllers"
    }

    # Test the availability of the domain controllers
    foreach ($objDc in $arrDomainControllers) {
        Write-Verbose "`t=== Testing DC: $($objDc.Name)"

        $objAd = $null

        try {
            if ($UsePsAdCommand -eq $true) {
                $objAd = Get-ADDomain -Server $objDc.Name -ErrorAction "Stop"

                # If the search was successful
                if (!($null -eq $objAD)) {
                    $sRtnDcName = $objDc.name

                    Write-Verbose "`t`t+++ Successfully tested $($objDc.Name)"

                    break   # escape the AD loop
                }
            }
            else {
                # Validate the domain controller available - calls function Get-ADDomainControllerRootEntry which
                # loaded during the module import
                $objAD = Get-ADDomainControllerRootEntry -sDCName $objDc.Name

                # Confirm the returned object isn't null then confirm if the returned data contains a property called defaultNamingContext
                # if it does then the .net function from Get-ADDomainControllerRootEntry has discovered an available active Domain Controller
                if (!($null -eq $objAd)) {
                    if ((($objAd | Get-Member -MemberType Properties | Select-Object -ExpandProperty name) -contains "defaultNamingContext") -eq $true) {
                        $sRtnDcName = $objDc.Name

                        # Function Test-QuickTcpConnection is available in the module and loaded when the module is imported
                        $bADWebServicesPortAvailable = Test-QuickTcpConnection -sComputerName $objDc.Name -iPort 9389

                        if ($bADWebServicesPortAvailable -eq $true) {
                            Write-Verbose "`t`t+++ SUCCESS: DC $($objDC.Name) has been validated"

                            break   # escape the AD loop - a DC has been found so no need to keep searching
                        }
                        else { # AD Web Services (reqd for PowerShell modules) is not available on the destination DC
                            Write-Verbose "`t`t--- DC $($objDC.Name) is up but AD Web Services is not available"
                        }
                    }
                    else {
                        Write-Verbose "`t`t--- DC $($objDC.Name) did not respond with a valid defaultNamingContext value"
                    }
                }
                else {
                    Write-Verbose "`t`t--- DC $($objDC.Name) did not response with any valid data"
                }
            }
        }
        catch {
            $objAd = $null

            $sErrMsg = ("Domain Controller $($objDC.Name) not available. Error: " + (($Global:Error[0].exception).toString()).replace("`r"," ").replace("`n"," "))
            Write-Verbose "`t`t--- $sErrMsg"
        }
    }

    # Check that a DC was discovered - throw error if not found
    if ([string]::IsNullOrEmpty($sRtnDcName) -eq $true) {
        throw "Failed to discover an available Domain Controller in the local site"
    }

    return $sRtnDcName
}