#requires -Version 7.0 <# .SYNOPSIS Universal DHCP Scope Monitoring Utility .DESCRIPTION Cross-platform DHCP monitoring utility for: - Windows - macOS - Linux (including Debian) Execution modes: 1. LOCAL : On Windows with the DhcpServer module available. 2. REMOTE : On Windows/macOS/Linux using PowerShell Remoting over SSH to a Windows DHCP Server. Remote DHCP objects are flattened on the Windows server before being returned to non-Windows clients, avoiding CIM/MI serialization issues such as libmi errors on macOS/Linux. .EXAMPLE pwsh ./dhcp-monitor-universal.ps1 On macOS/Linux (or Windows without the local DhcpServer module), the script asks for the DHCP Server hostname/IP and SSH username. .EXAMPLE pwsh ./dhcp-monitor-universal.ps1 -DhcpServer 192.168.1.10 -UserName "CONTOSO\\administrator" Skips the interactive server/username questions. .EXAMPLE pwsh ./dhcp-monitor-universal.ps1 -Mode Local Forces Local mode. Requires Windows and the DhcpServer module. .NOTES In Remote mode, the script interactively asks for: - DHCP Server hostname or IP address - SSH username #> param ( [string]$DhcpServer, [string]$UserName, [ValidateSet("Auto", "Local", "Remote")] [string]$Mode = "Auto" ) $ErrorActionPreference = "Stop" $script:DhcpSession = $null $script:ExecutionMode = $null $script:DhcpServer = $DhcpServer $script:UserName = $UserName # ============================================================ # UI HELPERS # ============================================================ function Show-Title { Clear-Host Write-Host "===================================================" -ForegroundColor Cyan Write-Host " Universal DHCP Scope Monitoring Utility" -ForegroundColor Cyan Write-Host "===================================================" -ForegroundColor Cyan Write-Host Write-Host "Client OS : $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)" Write-Host "Mode : $script:ExecutionMode" if ($script:ExecutionMode -eq "Remote") { Write-Host "DHCP Server : $script:DhcpServer" Write-Host "SSH User : $script:UserName" } Write-Host } function Wait-Enter { param([string]$Message = "Tekan ENTER untuk melanjutkan") [void](Read-Host $Message) } function Convert-ToDisplayString { param($Value) if ($null -eq $Value) { return "" } return $Value.ToString() } # ============================================================ # CONNECTION / EXECUTION MODE # ============================================================ function Test-LocalDhcpModule { if (-not $IsWindows) { return $false } return [bool](Get-Module -ListAvailable -Name DhcpServer) } function Connect-DhcpBackend { if ($Mode -eq "Local") { if (-not (Test-LocalDhcpModule)) { throw "Mode Local dipilih, tetapi module DhcpServer tidak tersedia." } Import-Module DhcpServer -ErrorAction Stop $script:ExecutionMode = "Local" return } if ($Mode -eq "Auto" -and (Test-LocalDhcpModule)) { Import-Module DhcpServer -ErrorAction Stop $script:ExecutionMode = "Local" return } $script:ExecutionMode = "Remote" Write-Host Write-Host "Remote DHCP Server Configuration" -ForegroundColor Cyan Write-Host "---------------------------------------------------" while ([string]::IsNullOrWhiteSpace($script:DhcpServer)) { $script:DhcpServer = Read-Host "DHCP Server hostname / IP" if ([string]::IsNullOrWhiteSpace($script:DhcpServer)) { Write-Host "DHCP Server tidak boleh kosong." -ForegroundColor Red } } while ([string]::IsNullOrWhiteSpace($script:UserName)) { $script:UserName = Read-Host "SSH username (contoh: DOMAIN\administrator)" if ([string]::IsNullOrWhiteSpace($script:UserName)) { Write-Host "Username tidak boleh kosong." -ForegroundColor Red } } Write-Host Write-Host "Menghubungkan ke Windows DHCP Server..." -ForegroundColor Cyan Write-Host "Server : $script:DhcpServer" Write-Host "User : $script:UserName" Write-Host try { $script:DhcpSession = New-PSSession ` -HostName $script:DhcpServer ` -UserName $script:UserName ` -ErrorAction Stop Write-Host "Koneksi berhasil." -ForegroundColor Green Start-Sleep -Milliseconds 700 } catch { throw "Gagal membuat PowerShell SSH session ke $script:DhcpServer. $($_.Exception.Message)" } } function Disconnect-DhcpBackend { if ($null -ne $script:DhcpSession) { try { Remove-PSSession -Session $script:DhcpSession -ErrorAction SilentlyContinue } catch {} $script:DhcpSession = $null } } function Test-DhcpSession { if ($script:ExecutionMode -ne "Remote") { return $true } if ($null -eq $script:DhcpSession) { return $false } return ($script:DhcpSession.State -eq "Opened") } # ============================================================ # DATA ACCESS LAYER # ============================================================ function Get-DhcpScopeData { param ( [Parameter(Mandatory)] [string]$ScopeId ) if ($script:ExecutionMode -eq "Local") { $s = Get-DhcpServerv4Scope -ScopeId $ScopeId -ErrorAction Stop return [PSCustomObject]@{ ScopeId = Convert-ToDisplayString $s.ScopeId Name = $s.Name State = Convert-ToDisplayString $s.State StartRange = Convert-ToDisplayString $s.StartRange EndRange = Convert-ToDisplayString $s.EndRange SubnetMask = Convert-ToDisplayString $s.SubnetMask LeaseDuration = Convert-ToDisplayString $s.LeaseDuration } } return Invoke-Command -Session $script:DhcpSession -ArgumentList $ScopeId -ScriptBlock { param($RemoteScopeId) $s = Get-DhcpServerv4Scope -ScopeId $RemoteScopeId -ErrorAction Stop [PSCustomObject]@{ ScopeId = if ($null -eq $s.ScopeId) { "" } else { $s.ScopeId.ToString() } Name = $s.Name State = if ($null -eq $s.State) { "" } else { $s.State.ToString() } StartRange = if ($null -eq $s.StartRange) { "" } else { $s.StartRange.ToString() } EndRange = if ($null -eq $s.EndRange) { "" } else { $s.EndRange.ToString() } SubnetMask = if ($null -eq $s.SubnetMask) { "" } else { $s.SubnetMask.ToString() } LeaseDuration = if ($null -eq $s.LeaseDuration) { "" } else { $s.LeaseDuration.ToString() } } } -ErrorAction Stop } function Get-DhcpStatisticsData { param ( [Parameter(Mandatory)] [string]$ScopeId ) if ($script:ExecutionMode -eq "Local") { $s = Get-DhcpServerv4ScopeStatistics -ScopeId $ScopeId -ErrorAction Stop return [PSCustomObject]@{ Free = [long]$s.Free InUse = [long]$s.InUse PercentageInUse = [double]$s.PercentageInUse Reserved = [long]$s.Reserved Pending = [long]$s.Pending SuperscopeName = Convert-ToDisplayString $s.SuperscopeName } } return Invoke-Command -Session $script:DhcpSession -ArgumentList $ScopeId -ScriptBlock { param($RemoteScopeId) $s = Get-DhcpServerv4ScopeStatistics -ScopeId $RemoteScopeId -ErrorAction Stop [PSCustomObject]@{ Free = [long]$s.Free InUse = [long]$s.InUse PercentageInUse = [double]$s.PercentageInUse Reserved = [long]$s.Reserved Pending = [long]$s.Pending SuperscopeName = if ($null -eq $s.SuperscopeName) { "" } else { $s.SuperscopeName.ToString() } } } -ErrorAction Stop } function Get-DhcpLeaseData { param ( [Parameter(Mandatory)] [string]$ScopeId ) if ($script:ExecutionMode -eq "Local") { $raw = @(Get-DhcpServerv4Lease -ScopeId $ScopeId -ErrorAction Stop) return @( foreach ($lease in $raw) { [PSCustomObject]@{ IPAddress = Convert-ToDisplayString $lease.IPAddress HostName = Convert-ToDisplayString $lease.HostName ClientId = Convert-ToDisplayString $lease.ClientId AddressState = Convert-ToDisplayString $lease.AddressState LeaseExpiryTime = $lease.LeaseExpiryTime } } ) } # IMPORTANT: # Materialize the DHCP CIM result first, then flatten every property # while still executing on Windows. This prevents libmi/CIM objects # from crossing the SSH remoting boundary. return @( Invoke-Command -Session $script:DhcpSession -ArgumentList $ScopeId -ScriptBlock { param($RemoteScopeId) $rawLeases = @(Get-DhcpServerv4Lease -ScopeId $RemoteScopeId -ErrorAction Stop) foreach ($lease in $rawLeases) { $expiry = $null if ($null -ne $lease.LeaseExpiryTime) { try { $expiry = $lease.LeaseExpiryTime.ToString("o") } catch { $expiry = $lease.LeaseExpiryTime.ToString() } } [PSCustomObject]@{ IPAddress = if ($null -eq $lease.IPAddress) { "" } else { $lease.IPAddress.ToString() } HostName = if ($null -eq $lease.HostName) { "" } else { $lease.HostName.ToString() } ClientId = if ($null -eq $lease.ClientId) { "" } else { $lease.ClientId.ToString() } AddressState = if ($null -eq $lease.AddressState) { "" } else { $lease.AddressState.ToString() } LeaseExpiryTime = $expiry } } } -ErrorAction Stop ) } # ============================================================ # NORMALIZATION / SORTING # ============================================================ function Convert-LeaseExpiry { param($Value) if ($null -eq $Value -or [string]::IsNullOrWhiteSpace($Value.ToString())) { return $null } if ($Value -is [datetime]) { return $Value } $parsed = [datetime]::MinValue if ([datetime]::TryParse( $Value.ToString(), [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind, [ref]$parsed )) { return $parsed } if ([datetime]::TryParse($Value.ToString(), [ref]$parsed)) { return $parsed } return $null } function Get-IPv4SortKey { param([string]$IPAddress) try { $bytes = [System.Net.IPAddress]::Parse($IPAddress).GetAddressBytes() if ($bytes.Count -ne 4) { return [uint64]::MaxValue } return ( ([uint64]$bytes[0] -shl 24) -bor ([uint64]$bytes[1] -shl 16) -bor ([uint64]$bytes[2] -shl 8) -bor ([uint64]$bytes[3]) ) } catch { return [uint64]::MaxValue } } # ============================================================ # SCOPE FUNCTIONS # ============================================================ function Test-DhcpScope { param ( [Parameter(Mandatory)] [string]$ScopeId ) try { [void](Get-DhcpScopeData -ScopeId $ScopeId) return $true } catch { return $false } } function Show-ScopeSummary { param ( [Parameter(Mandatory)] [string]$ScopeId ) try { $scope = Get-DhcpScopeData -ScopeId $ScopeId $stats = Get-DhcpStatisticsData -ScopeId $ScopeId Write-Host Write-Host "Summary Scope" -ForegroundColor Yellow Write-Host "---------------------------------------------------" [PSCustomObject]@{ ScopeId = $scope.ScopeId Name = $scope.Name State = $scope.State StartRange = $scope.StartRange EndRange = $scope.EndRange SubnetMask = $scope.SubnetMask LeaseDuration = $scope.LeaseDuration Free = $stats.Free InUse = $stats.InUse PercentageInUse = ("{0:N2}%" -f [double]$stats.PercentageInUse) Reserved = $stats.Reserved Pending = $stats.Pending SuperscopeName = $stats.SuperscopeName } | Format-List | Out-Host Write-Host "---------------------------------------------------" if ([double]$stats.PercentageInUse -ge 95) { Write-Host "WARNING: Scope hampir penuh!" -ForegroundColor Red } elseif ([double]$stats.PercentageInUse -ge 80) { Write-Host "WARNING: Pemakaian scope sudah tinggi." -ForegroundColor Yellow } else { Write-Host "Status kapasitas scope masih aman." -ForegroundColor Green } Write-Host } catch { Write-Host Write-Host "Gagal membaca Scope $ScopeId." -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor DarkRed Write-Host } } # ============================================================ # DEVICE / LEASE LIST # ============================================================ function Show-ScopeClients { param ( [Parameter(Mandatory)] [string]$ScopeId ) Clear-Host Write-Host "===================================================" -ForegroundColor Cyan Write-Host " DHCP Lease / Device List" -ForegroundColor Cyan Write-Host "===================================================" -ForegroundColor Cyan Write-Host Write-Host "ScopeId : $ScopeId" -ForegroundColor Yellow Write-Host try { $leases = @(Get-DhcpLeaseData -ScopeId $ScopeId) if ($leases.Count -eq 0) { Write-Host "Tidak ada lease yang dikembalikan untuk scope ini." -ForegroundColor Yellow Write-Host return } $normalized = @( foreach ($lease in $leases) { [PSCustomObject]@{ IPAddress = $lease.IPAddress HostName = $lease.HostName ClientId = $lease.ClientId AddressState = $lease.AddressState LeaseExpiryTime = Convert-LeaseExpiry $lease.LeaseExpiryTime } } ) $normalized | Sort-Object @{ Expression = { Get-IPv4SortKey $_.IPAddress } } | Select-Object IPAddress, HostName, ClientId, AddressState, LeaseExpiryTime | Format-Table -AutoSize | Out-Host Write-Host Write-Host "---------------------------------------------------" Write-Host "Total lease : $($normalized.Count)" -ForegroundColor Cyan $activeCount = @( $normalized | Where-Object { $_.AddressState -match '^Active' } ).Count Write-Host "Active lease: $activeCount" -ForegroundColor Green Write-Host "---------------------------------------------------" Write-Host } catch { Write-Host Write-Host "Gagal membaca daftar lease." -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor DarkRed Write-Host } } # ============================================================ # LEASE ANALYSIS # ============================================================ function Show-LeaseAnalysis { param ( [Parameter(Mandatory)] [string]$ScopeId ) Clear-Host Write-Host "===================================================" -ForegroundColor Cyan Write-Host " DHCP Lease Analysis" -ForegroundColor Cyan Write-Host "===================================================" -ForegroundColor Cyan Write-Host Write-Host "ScopeId : $ScopeId" -ForegroundColor Yellow Write-Host try { $now = Get-Date $scope = Get-DhcpScopeData -ScopeId $ScopeId $stats = Get-DhcpStatisticsData -ScopeId $ScopeId $rawLeases = @(Get-DhcpLeaseData -ScopeId $ScopeId) $leases = @( foreach ($lease in $rawLeases) { [PSCustomObject]@{ IPAddress = $lease.IPAddress HostName = $lease.HostName ClientId = $lease.ClientId AddressState = $lease.AddressState LeaseExpiryTime = Convert-LeaseExpiry $lease.LeaseExpiryTime } } ) $leasesWithExpiry = @( $leases | Where-Object { $null -ne $_.LeaseExpiryTime } ) $expired = @( $leasesWithExpiry | Where-Object { $_.LeaseExpiryTime -lt $now } ).Count $expireWithin4Hour = @( $leasesWithExpiry | Where-Object { $_.LeaseExpiryTime -ge $now -and $_.LeaseExpiryTime -le $now.AddHours(4) } ).Count $beyond4Hour = @( $leasesWithExpiry | Where-Object { $_.LeaseExpiryTime -gt $now.AddHours(4) } ).Count Write-Host "Waktu Pemeriksaan : $now" Write-Host "Lease Duration : $($scope.LeaseDuration)" Write-Host Write-Host "Pool Statistics" -ForegroundColor Yellow Write-Host "---------------------------------------------------" [PSCustomObject]@{ Free = $stats.Free InUse = $stats.InUse PercentageInUse = ("{0:N2}%" -f [double]$stats.PercentageInUse) Reserved = $stats.Reserved Pending = $stats.Pending } | Format-List | Out-Host Write-Host Write-Host "Lease Expiration Analysis" -ForegroundColor Yellow Write-Host "---------------------------------------------------" [PSCustomObject]@{ TotalLease = $leases.Count WithExpiry = $leasesWithExpiry.Count Expired = $expired ExpireWithin4Hour = $expireWithin4Hour Beyond4Hour = $beyond4Hour } | Format-Table -AutoSize | Out-Host Write-Host Write-Host "Interpretasi" -ForegroundColor Yellow Write-Host "---------------------------------------------------" if ([double]$stats.PercentageInUse -ge 95) { Write-Host "Kapasitas scope : KRITIS (>=95%)." -ForegroundColor Red } elseif ([double]$stats.PercentageInUse -ge 80) { Write-Host "Kapasitas scope : TINGGI (>=80%)." -ForegroundColor Yellow } else { Write-Host "Kapasitas scope : AMAN (<80%)." -ForegroundColor Green } if ($leasesWithExpiry.Count -gt 0) { if ($beyond4Hour -gt ($leasesWithExpiry.Count / 2)) { Write-Host "Mayoritas lease masih memiliki expiry lebih dari 4 jam." -ForegroundColor Yellow Write-Host "Kemungkinan masih banyak lease lama dari konfigurasi sebelumnya." } else { Write-Host "Mayoritas lease sudah berada dalam window 4 jam." -ForegroundColor Green } } else { Write-Host "Tidak ada data expiry lease yang dapat dianalisis." -ForegroundColor Yellow } Write-Host } catch { Write-Host Write-Host "Gagal melakukan Lease Analysis." -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor DarkRed Write-Host } } # ============================================================ # MAIN PROGRAM # ============================================================ try { Connect-DhcpBackend while ($true) { Show-Title if (-not (Test-DhcpSession)) { throw "PowerShell remote session tidak lagi dalam state Opened." } Write-Host "Masukkan ScopeId yang ingin diperiksa." -ForegroundColor Green Write-Host "Contoh: 10.110.114.0" Write-Host Write-Host "Ketik 0 untuk keluar." Write-Host $ScopeId = Read-Host "ScopeId" if ($ScopeId -eq "0") { break } if ([string]::IsNullOrWhiteSpace($ScopeId)) { Write-Host Write-Host "ScopeId tidak boleh kosong." -ForegroundColor Red Write-Host Wait-Enter continue } if (-not (Test-DhcpScope -ScopeId $ScopeId)) { Write-Host Write-Host "ScopeId '$ScopeId' tidak ditemukan atau tidak dapat dibaca." -ForegroundColor Red Write-Host Wait-Enter "Tekan ENTER untuk mencoba lagi" continue } $changeScope = $false while (-not $changeScope) { Show-Title Show-ScopeSummary -ScopeId $ScopeId Write-Host "Apa yang ingin dilakukan?" -ForegroundColor Green Write-Host Write-Host "[1] Tampilkan daftar lease / perangkat" Write-Host "[2] Lease Analysis" Write-Host "[3] Cek ScopeId lain" Write-Host "[4] Refresh summary scope ini" Write-Host "[0] Keluar" Write-Host $choice = Read-Host "Pilihan" switch ($choice) { "1" { Show-ScopeClients -ScopeId $ScopeId $clientMenu = $true while ($clientMenu) { Write-Host "Apa yang ingin dilakukan selanjutnya?" -ForegroundColor Green Write-Host Write-Host "[1] Kembali ke summary scope ini" Write-Host "[2] Lease Analysis" Write-Host "[3] Cek ScopeId lain" Write-Host "[4] Refresh daftar lease" Write-Host "[0] Keluar" Write-Host $subChoice = Read-Host "Pilihan" switch ($subChoice) { "1" { $clientMenu = $false } "2" { Show-LeaseAnalysis -ScopeId $ScopeId Write-Host Wait-Enter "Tekan ENTER untuk kembali" $clientMenu = $false } "3" { $clientMenu = $false $changeScope = $true } "4" { Show-ScopeClients -ScopeId $ScopeId } "0" { return } default { Write-Host Write-Host "Pilihan tidak valid." -ForegroundColor Red Write-Host } } } } "2" { Show-LeaseAnalysis -ScopeId $ScopeId $analysisMenu = $true while ($analysisMenu) { Write-Host "Apa yang ingin dilakukan selanjutnya?" -ForegroundColor Green Write-Host Write-Host "[1] Kembali ke summary scope ini" Write-Host "[2] Tampilkan daftar lease / perangkat" Write-Host "[3] Cek ScopeId lain" Write-Host "[4] Refresh Lease Analysis" Write-Host "[0] Keluar" Write-Host $subChoice = Read-Host "Pilihan" switch ($subChoice) { "1" { $analysisMenu = $false } "2" { Show-ScopeClients -ScopeId $ScopeId Write-Host Wait-Enter "Tekan ENTER untuk kembali" $analysisMenu = $false } "3" { $analysisMenu = $false $changeScope = $true } "4" { Show-LeaseAnalysis -ScopeId $ScopeId } "0" { return } default { Write-Host Write-Host "Pilihan tidak valid." -ForegroundColor Red Write-Host } } } } "3" { $changeScope = $true } "4" { # Loop refreshes summary. } "0" { return } default { Write-Host Write-Host "Pilihan tidak valid." -ForegroundColor Red Start-Sleep -Seconds 1 } } } } } catch { Write-Host Write-Host "FATAL ERROR" -ForegroundColor Red Write-Host "---------------------------------------------------" Write-Host $_.Exception.Message -ForegroundColor Red Write-Host exit 1 } finally { Disconnect-DhcpBackend Write-Host Write-Host "Keluar dari Universal DHCP Scope Monitoring Utility." -ForegroundColor Cyan Write-Host }