Public/healthcheck/Get-DhcpServerHealth.ps1

#Requires -Version 5.1
function Get-DhcpServerHealth {
    <#
        .SYNOPSIS
            Checks DHCP Server role health on Windows servers
 
        .DESCRIPTION
            Performs a comprehensive health check of the DHCP Server role on one or more Windows
            servers. Evaluates service status, scope statistics, address utilization, and failover
            relationships to produce a per-scope health assessment with an overall health rating.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not used for local queries.
 
        .EXAMPLE
            Get-DhcpServerHealth
 
            Checks DHCP Server health on the local machine.
 
        .EXAMPLE
            Get-DhcpServerHealth -ComputerName 'DHCP01'
 
            Checks DHCP Server health on the remote server DHCP01.
 
        .EXAMPLE
            'DHCP01', 'DHCP02' | Get-DhcpServerHealth -Credential (Get-Credential)
 
            Checks DHCP Server health on multiple servers using explicit credentials via pipeline.
 
        .OUTPUTS
            PSWinOps.DhcpServerHealth
            Returns one object per DHCP scope found, or one summary object if no scopes exist
            or the role is unavailable. Each object includes service status, scope details,
            address utilization, failover state, and an overall health rating.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-03-26
            Requires: PowerShell 5.1+ / Windows only
            Requires: DHCP Server role
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/powershell/module/dhcpserver/
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.DhcpServerHealth')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"
        $localNames = @($env:COMPUTERNAME, 'localhost', '.')

        $scriptBlock = {
            $scopeResults = [System.Collections.Generic.List[object]]::new()

            # Check DHCPServer service status
            $serviceStatus = 'NotFound'
            try {
                $dhcpService = Get-Service -Name 'DHCPServer' -ErrorAction Stop
                $serviceStatus = [string]$dhcpService.Status
            }
            catch {
                $serviceStatus = 'NotFound'
            }

            # Check DhcpServer module availability
            $moduleAvailable = $false
            $dhcpModule = Get-Module -Name 'DhcpServer' -ListAvailable -ErrorAction SilentlyContinue
            if ($dhcpModule) {
                $moduleAvailable = $true
            }

            # If module is not available, return a single summary hashtable
            if (-not $moduleAvailable) {
                $scopeResults.Add(@{
                    ServiceStatus   = $serviceStatus
                    ModuleAvailable = $false
                    ScopeId         = 'N/A'
                    ScopeName       = 'N/A'
                    ScopeState      = 'N/A'
                    AddressesTotal  = 0
                    AddressesInUse  = 0
                    AddressesFree   = 0
                    PercentInUse    = [decimal]0
                    FailoverPartner = 'None'
                    FailoverState   = 'None'
                })
                return $scopeResults
            }

            # If service is not running, return summary
            if ($serviceStatus -ne 'Running') {
                $scopeResults.Add(@{
                    ServiceStatus   = $serviceStatus
                    ModuleAvailable = $true
                    ScopeId         = 'N/A'
                    ScopeName       = 'N/A'
                    ScopeState      = 'N/A'
                    AddressesTotal  = 0
                    AddressesInUse  = 0
                    AddressesFree   = 0
                    PercentInUse    = [decimal]0
                    FailoverPartner = 'None'
                    FailoverState   = 'None'
                })
                return $scopeResults
            }

            # Get all failover relationships and index by ScopeId for O(1) lookup
            $failoverIndex = @{}
            try {
                $failoverRelationships = Get-DhcpServerv4Failover -ErrorAction SilentlyContinue
                if ($failoverRelationships) {
                    foreach ($failover in $failoverRelationships) {
                        foreach ($scopeId in $failover.ScopeId) {
                            $failoverIndex[[string]$scopeId] = @{
                                Partner = [string]$failover.PartnerServer
                                State   = [string]$failover.State
                            }
                        }
                    }
                }
            }
            catch {
                Write-Verbose -Message "DHCP failover query skipped: $_"
            }

            # Get all IPv4 scopes
            $dhcpScopes = $null
            try {
                $dhcpScopes = Get-DhcpServerv4Scope -ErrorAction Stop
            }
            catch {
                Write-Verbose -Message "DHCP scope retrieval failed: $_"
            }

            if (-not $dhcpScopes) {
                $scopeResults.Add(@{
                    ServiceStatus   = $serviceStatus
                    ModuleAvailable = $true
                    ScopeId         = 'N/A'
                    ScopeName       = 'No scopes configured'
                    ScopeState      = 'N/A'
                    AddressesTotal  = 0
                    AddressesInUse  = 0
                    AddressesFree   = 0
                    PercentInUse    = [decimal]0
                    FailoverPartner = 'None'
                    FailoverState   = 'None'
                })
                return $scopeResults
            }

            foreach ($scope in $dhcpScopes) {
                $scopeIdStr = [string]$scope.ScopeId

                # Get scope statistics
                $addressesFree  = 0
                $addressesInUse = 0
                $percentInUse   = [decimal]0
                try {
                    $scopeStats = Get-DhcpServerv4ScopeStatistics -ScopeId $scope.ScopeId -ErrorAction Stop
                    $addressesFree  = [int]$scopeStats.AddressesFree
                    $addressesInUse = [int]$scopeStats.AddressesInUse
                    $percentInUse   = [decimal]$scopeStats.PercentageInUse
                }
                catch {
                    Write-Verbose -Message "Statistics unavailable for scope $($scope.ScopeId): $_"
                }

                $addressesTotal = $addressesInUse + $addressesFree

                # Lookup failover info from pre-built index
                $failoverPartner = 'None'
                $failoverState   = 'None'
                if ($failoverIndex.ContainsKey($scopeIdStr)) {
                    $failoverPartner = $failoverIndex[$scopeIdStr].Partner
                    $failoverState   = $failoverIndex[$scopeIdStr].State
                }

                $scopeResults.Add(@{
                    ServiceStatus   = $serviceStatus
                    ModuleAvailable = $true
                    ScopeId         = $scopeIdStr
                    ScopeName       = [string]$scope.Name
                    ScopeState      = [string]$scope.State
                    AddressesTotal  = $addressesTotal
                    AddressesInUse  = $addressesInUse
                    AddressesFree   = $addressesFree
                    PercentInUse    = $percentInUse
                    FailoverPartner = $failoverPartner
                    FailoverState   = $failoverState
                })
            }

            $scopeResults
        }
    }

    process {
        foreach ($machine in $ComputerName) {
            $displayName = $machine.ToUpper()
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying '${machine}'"

            try {
                $isLocal = $localNames -contains $machine

                if ($isLocal) {
                    $rawResults = & $scriptBlock
                }
                else {
                    $invokeParams = @{
                        ComputerName = $machine
                        ScriptBlock  = $scriptBlock
                        ErrorAction  = 'Stop'
                    }
                    if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) {
                        $invokeParams['Credential'] = $Credential
                    }
                    $rawResults = Invoke-Command @invokeParams
                }

                if (-not $rawResults) {
                    Write-Warning -Message "[$($MyInvocation.MyCommand)] No results returned from '${machine}'"
                    continue
                }

                foreach ($scopeData in $rawResults) {
                    # Determine OverallHealth
                    if (-not $scopeData.ModuleAvailable) {
                        $healthStatus = 'RoleUnavailable'
                    }
                    elseif ($scopeData.ServiceStatus -ne 'Running') {
                        $healthStatus = 'Critical'
                    }
                    elseif ($scopeData.ScopeId -eq 'N/A') {
                        $healthStatus = 'Healthy'
                    }
                    elseif ($scopeData.ScopeState -eq 'Inactive' -and $scopeData.AddressesInUse -gt 0) {
                        $healthStatus = 'Critical'
                    }
                    elseif ($scopeData.PercentInUse -gt 90) {
                        $healthStatus = 'Degraded'
                    }
                    elseif ($scopeData.FailoverState -ne 'None' -and $scopeData.FailoverState -ne 'Normal') {
                        $healthStatus = 'Degraded'
                    }
                    else {
                        $healthStatus = 'Healthy'
                    }

                    [PSCustomObject]@{
                        PSTypeName      = 'PSWinOps.DhcpServerHealth'
                        ComputerName    = $displayName
                        ServiceName     = 'DHCPServer'
                        ServiceStatus   = [string]$scopeData.ServiceStatus
                        ScopeId         = [string]$scopeData.ScopeId
                        ScopeName       = [string]$scopeData.ScopeName
                        ScopeState      = [string]$scopeData.ScopeState
                        AddressesTotal  = [int]$scopeData.AddressesTotal
                        AddressesInUse  = [int]$scopeData.AddressesInUse
                        AddressesFree   = [int]$scopeData.AddressesFree
                        PercentInUse    = [decimal]$scopeData.PercentInUse
                        FailoverPartner = [string]$scopeData.FailoverPartner
                        FailoverState   = [string]$scopeData.FailoverState
                        OverallHealth   = $healthStatus
                        Timestamp       = Get-Date -Format 'o'
                    }
                }
            }
            catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}