From 93c69c75ab145efe35a1f2ea510cc6aca99d16fd Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 27 Sep 2026 12:12:38 -0500 Subject: [PATCH 1/3] feat: add Client Management and Network Information, completing both API areas Adds Get/New/Update/Remove-PiHoleClient and Get-PiHoleClientSuggestion, covering Pi-hole's client management API (GET/POST/PUT + batch-delete, plus unconfigured-client suggestions). Single-item DELETE is intentionally skipped in favor of the batch endpoint, same precedent as Remove-PiHoleList/Remove-PiHoleDomain. Adds Get-PiHoleNetworkGateway/Route/Interface/Device and Remove-PiHoleNetworkDevice, covering the full Network Information API. Unlike Clients/Domains/Lists there's no batch-delete alternative for devices, so Remove-PiHoleNetworkDevice implements the single-item DELETE directly. Interface/route/gateway responses vary significantly by type and aren't fully enumerable from the spec, so they're returned via the existing generic PascalCase-conversion helper (already used by Get-PiHoleConfig) rather than a hand-picked set of fields - confirmed against the real server that this also correctly surfaces at least one live field not documented in the OpenAPI spec (interface stats.bits). Adds Client Management as its own README/EXAMPLES category; Network Information folds into "Configuration & Diagnostics" like the other read-only diagnostic areas (FTLInformation, Padd, Teleporter, History). Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 6 +- .../ClientManagement/Get-PiHoleClient.ps1 | 97 ++++ .../Get-PiHoleClientSuggestion.ps1 | 75 +++ .../ClientManagement/New-PiHoleClient.ps1 | 121 +++++ .../ClientManagement/Remove-PiHoleClient.ps1 | 90 +++ .../ClientManagement/Update-PiHoleClient.ps1 | 129 +++++ .../Get-PiHoleNetworkDevice.ps1 | 110 ++++ .../Get-PiHoleNetworkGateway.ps1 | 77 +++ .../Get-PiHoleNetworkInterface.ps1 | 77 +++ .../Get-PiHoleNetworkRoute.ps1 | 77 +++ .../Remove-PiHoleNetworkDevice.ps1 | 80 +++ README.md | 15 + docs/EXAMPLES.md | 511 ++++++++++++------ .../Get-PiHoleClient.Integration.Tests.ps1 | 62 +++ ...HoleClientSuggestion.Integration.Tests.ps1 | 38 ++ .../New-PiHoleClient.Integration.Tests.ps1 | 99 ++++ .../Remove-PiHoleClient.Integration.Tests.ps1 | 67 +++ .../Update-PiHoleClient.Integration.Tests.ps1 | 109 ++++ ...-PiHoleNetworkDevice.Integration.Tests.ps1 | 44 ++ ...PiHoleNetworkGateway.Integration.Tests.ps1 | 45 ++ ...HoleNetworkInterface.Integration.Tests.ps1 | 45 ++ ...t-PiHoleNetworkRoute.Integration.Tests.ps1 | 45 ++ ...-PiHoleNetworkDevice.Integration.Tests.ps1 | 38 ++ tools/Update-ExampleOutput.ps1 | 58 ++ tools/Update-ReadmeCommandReference.ps1 | 3 +- 25 files changed, 1949 insertions(+), 169 deletions(-) create mode 100644 PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1 create mode 100644 PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1 create mode 100644 PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1 create mode 100644 PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1 create mode 100644 PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1 create mode 100644 PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1 create mode 100644 PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1 create mode 100644 PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1 create mode 100644 PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1 create mode 100644 PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1 create mode 100644 tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1 create mode 100644 tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1 create mode 100644 tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1 create mode 100644 tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1 create mode 100644 tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 create mode 100644 tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1 create mode 100644 tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1 create mode 100644 tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1 create mode 100644 tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1 create mode 100644 tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 59967d5..da20726 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -40,5 +40,9 @@ Export-ModuleMember -Function @( #Teleporter 'Get-PiHoleTeleporterDownload', ` #DomainManagement - 'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain' + 'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain', ` + #ClientManagement + 'Get-PiHoleClient', 'New-PiHoleClient', 'Update-PiHoleClient', 'Remove-PiHoleClient', 'Get-PiHoleClientSuggestion', ` + #NetworkInformation + 'Get-PiHoleNetworkGateway', 'Get-PiHoleNetworkRoute', 'Get-PiHoleNetworkInterface', 'Get-PiHoleNetworkDevice', 'Remove-PiHoleNetworkDevice' ) \ No newline at end of file diff --git a/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1 new file mode 100644 index 0000000..799ff99 --- /dev/null +++ b/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1 @@ -0,0 +1,97 @@ +function Get-PiHoleClient { + <# +.SYNOPSIS +Get clients + +.DESCRIPTION +Request Pi-hole's configured clients (used to apply group-based rules to specific devices). +Omit -Client to get every configured client; specify it to get just that one. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Client +A specific client to return - an IP address, IP subnet (CIDR), MAC address, hostname, or +interface (prefixed with a colon, e.g. ":eth0"). Omit to return every configured client + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/-client-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [string]$Client, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Groups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/clients" + if ($Client) { + $Uri += "/$([System.Uri]::EscapeDataString($Client))" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + $ObjectFinal = foreach ($Item in $Response.clients) { + $GroupNames = [System.Collections.ArrayList]@() + foreach ($Group in $Item.groups) { + $GroupNames += ($Groups | Where-Object { $_.Id -eq $Group }).Name + } + + [PSCustomObject]@{ + Client = $Item.client + Name = $Item.name + Comment = $Item.comment + Groups = $GroupNames + Id = $Item.id + DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime + DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1 b/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1 new file mode 100644 index 0000000..e1ca8fc --- /dev/null +++ b/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1 @@ -0,0 +1,75 @@ +function Get-PiHoleClientSuggestion { + <# +.SYNOPSIS +Get client suggestions + +.DESCRIPTION +Returns a list of clients Pi-hole has seen making DNS queries but that aren't yet configured as +a client (see New-PiHoleClient to add one). + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleClientSuggestion -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/_suggestions')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/clients/_suggestions" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + $ObjectFinal = foreach ($Item in $Response.clients) { + [PSCustomObject]@{ + HwAddr = $Item.hwaddr + MacVendor = $Item.macVendor + LastQuery = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.lastQuery).LocalTime + Addresses = $Item.addresses + Names = $Item.names + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1 new file mode 100644 index 0000000..703d473 --- /dev/null +++ b/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1 @@ -0,0 +1,121 @@ +function New-PiHoleClient { + <# +.SYNOPSIS +Add a new client + +.DESCRIPTION +Adds a client to Pi-hole so group-based rules can be applied to it specifically. A client may +be identified by IP address, IP subnet (CIDR notation), MAC address, hostname, or the interface +it connects through (prefixed with a colon, e.g. ":eth0"). IP-based recognition is preferred - +MAC address, hostname, and interface recognition only work for devices Pi-hole has already seen. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Client +The client to add - an IP address, IP subnet (CIDR), MAC address, hostname, or interface +(prefixed with a colon, e.g. ":eth0") + +.PARAMETER Comment +An optional comment to store alongside the client + +.PARAMETER Group +The group(s) this client applies to. Defaults to "Default" + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +New-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" -Comment "Kid's tablet" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/clients')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Client, + [string]$Comment = $null, + [string[]]$Group = "Default", + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $FindMatchingClient = Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -Client $Client + + if ($FindMatchingClient) { + throw "Client $Client already exists on $PiHoleServer! Please use Update-PiHoleClient to update it" + } + + $AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $AllGroupsNames = @() + $AllGroupsIds = @() + foreach ($GroupItem in $Group) { + $FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem } + if ($FoundGroup) { + $AllGroupsNames += $FoundGroup.Name + $AllGroupsIds += $FoundGroup.Id + } + else { + throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups" + } + } + + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Body = @{ + client = $Client + comment = $Comment + groups = [Object[]]($AllGroupsIds) + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/clients" + Method = "Post" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + $ObjectFinal = foreach ($Item in $Response.clients) { + [PSCustomObject]@{ + Client = $Item.client + Name = $Item.name + Comment = $Item.comment + Groups = $AllGroupsNames + Id = $Item.id + DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime + DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1 new file mode 100644 index 0000000..0855c67 --- /dev/null +++ b/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1 @@ -0,0 +1,90 @@ +function Remove-PiHoleClient { + <# +.SYNOPSIS +Remove a client + +.DESCRIPTION +Removes a client from Pi-hole. The Pi-hole API deletes clients in a batch, so this sends a +single-item batch containing just the client you specify. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Client +The client to remove - an IP address, IP subnet (CIDR), MAC address, hostname, or interface +(prefixed with a colon, e.g. ":eth0") + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/clients-batchDelete')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Client, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Target = "Pi-Hole client $Client" + if ($PSCmdlet.ShouldProcess($Target, "Remove client")) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Body = @( + @{ + item = $Client + } + ) + + #For some reason this needs to be here to make it an array + $Body = , $Body + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/clients:batchDelete" + Method = "Post" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 -Compress + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + # A successful delete returns 204 No Content, so there's no response body to + # build a rich object from. + $Object = [PSCustomObject]@{ + Client = $Client + Status = "Removed" + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1 new file mode 100644 index 0000000..f7319af --- /dev/null +++ b/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1 @@ -0,0 +1,129 @@ +function Update-PiHoleClient { + <# +.SYNOPSIS +Update a client + +.DESCRIPTION +Updates an existing client's Comment and/or Group(s). The underlying Pi-hole API replaces the +entire client entry on update, so any property you don't pass here is preserved by first +reading the client's current value and resending it - nothing is silently cleared just because +you only meant to change one property. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Client +The client to update - an IP address, IP subnet (CIDR), MAC address, hostname, or interface +(prefixed with a colon, e.g. ":eth0") + +.PARAMETER Comment +The new comment for the client. Leave unset to keep the client's current comment + +.PARAMETER Group +The group(s) this client should apply to. Leave unset to keep the client's current group(s) + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Update-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" -Comment "Kid's tablet" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/clients/-client-')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Client, + [string]$Comment, + [string[]]$Group, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + + try { + if (-not $PSBoundParameters.ContainsKey('Comment') -and -not $PSBoundParameters.ContainsKey('Group')) { + throw "To update $Client, you must specify the Comment and/or Group parameter" + } + + $ExistingClient = Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -Client $Client + + if (-not $ExistingClient) { + throw "Cannot find $Client on $PiHoleServer! Please use New-PiHoleClient to create it" + } + + $AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $GroupNamesToResolve = if ($PSBoundParameters.ContainsKey('Group')) { $Group } else { $ExistingClient.Groups } + + $AllGroupsNames = @() + $AllGroupsIds = @() + foreach ($GroupItem in $GroupNamesToResolve) { + $FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem } + if ($FoundGroup) { + $AllGroupsNames += $FoundGroup.Name + $AllGroupsIds += $FoundGroup.Id + } + else { + throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups" + } + } + + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + # The API replaces the whole client entry on update, so any property not explicitly + # passed here is resent using the client's current value to avoid silently clearing it. + $Body = @{ + comment = if ($PSBoundParameters.ContainsKey('Comment')) { $Comment } else { $ExistingClient.Comment } + groups = [Object[]]($AllGroupsIds) + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/clients/$([System.Uri]::EscapeDataString($Client))" + Method = "Put" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $ObjectFinal = foreach ($Item in $Response.clients) { + [PSCustomObject]@{ + Client = $Item.client + Name = $Item.name + Comment = $Item.comment + Groups = $AllGroupsNames + Id = $Item.id + DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime + DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1 new file mode 100644 index 0000000..f47050b --- /dev/null +++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1 @@ -0,0 +1,110 @@ +function Get-PiHoleNetworkDevice { + <# +.SYNOPSIS +Get info about the devices in your local network as seen by your Pi-hole + +.DESCRIPTION +Returns the devices Pi-hole has seen on your network, ordered by most recent query first. Shown +devices default to 10; use -MaxDevices to change that. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER MaxDevices +Maximum number of devices to return. Defaults to 10 + +.PARAMETER MaxAddresses +Maximum number of addresses to return per device + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +Get-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxDevices 50 + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/devices')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[int]]$MaxDevices, + [Nullable[int]]$MaxAddresses, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $QueryParams = [System.Collections.ArrayList]@() + if ($PSBoundParameters.ContainsKey('MaxDevices')) { + $QueryParams.Add("max_devices=$MaxDevices") | Out-Null + } + if ($PSBoundParameters.ContainsKey('MaxAddresses')) { + $QueryParams.Add("max_addresses=$MaxAddresses") | Out-Null + } + + $Uri = "$($PiHoleServer.OriginalString)/api/network/devices" + if ($QueryParams.Count -gt 0) { + $Uri += "?" + ($QueryParams -join '&') + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $ObjectFinal = foreach ($Item in $Response.devices) { + $Ips = foreach ($IpItem in $Item.ips) { + [PSCustomObject]@{ + Ip = $IpItem.ip + Name = $IpItem.name + LastSeen = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $IpItem.lastSeen).LocalTime + NameUpdated = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $IpItem.nameUpdated).LocalTime + } + } + + [PSCustomObject]@{ + Id = $Item.id + HwAddr = $Item.hwaddr + Interface = $Item.interface + FirstSeen = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.firstSeen).LocalTime + LastQuery = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.lastQuery).LocalTime + NumQueries = $Item.numQueries + MacVendor = $Item.macVendor + Ips = $Ips + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1 new file mode 100644 index 0000000..ee6c1d3 --- /dev/null +++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1 @@ -0,0 +1,77 @@ +function Get-PiHoleNetworkGateway { + <# +.SYNOPSIS +Get info about the gateway of your Pi-hole + +.DESCRIPTION +Returns information about your Pi-hole's network gateway(s). The response shape varies by +address family and interface type, so it's returned as nested objects with PascalCase property +names rather than a hand-picked subset - use -RawOutput to see the untouched API response. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Detailed +Include detailed information about the individual interfaces and routes. Available fields +depend on the interface type and state + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleNetworkGateway -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/gateway')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[bool]]$Detailed, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/network/gateway" + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.gateway + Write-Output $Object + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1 new file mode 100644 index 0000000..8efbc42 --- /dev/null +++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1 @@ -0,0 +1,77 @@ +function Get-PiHoleNetworkInterface { + <# +.SYNOPSIS +Get info about the interfaces of your Pi-hole + +.DESCRIPTION +Returns information about your Pi-hole's networking interfaces. Not every field applies to +every interface type, so the response is returned as nested objects with PascalCase property +names rather than a hand-picked subset - use -RawOutput to see the untouched API response. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Detailed +Include more detailed information about the individual interfaces. Available fields depend on +the interface type and state + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleNetworkInterface -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/interfaces')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[bool]]$Detailed, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/network/interfaces" + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.interfaces + Write-Output $Object + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1 new file mode 100644 index 0000000..e6f3740 --- /dev/null +++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1 @@ -0,0 +1,77 @@ +function Get-PiHoleNetworkRoute { + <# +.SYNOPSIS +Get info about the routes of your Pi-hole + +.DESCRIPTION +Returns information about your Pi-hole's networking routes. Not every field applies to every +route type, so the response is returned as nested objects with PascalCase property names +rather than a hand-picked subset - use -RawOutput to see the untouched API response. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Detailed +Include more detailed information about the individual routes. Available fields depend on the +route type and state + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleNetworkRoute -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/routes')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[bool]]$Detailed, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/network/routes" + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.routes + Write-Output $Object + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1 b/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1 new file mode 100644 index 0000000..aac1ee4 --- /dev/null +++ b/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1 @@ -0,0 +1,80 @@ +function Remove-PiHoleNetworkDevice { + <# +.SYNOPSIS +Delete a device from the network table + +.DESCRIPTION +Deletes a device from Pi-hole's network table, along with all its associated IP addresses and +hostnames. Use Get-PiHoleNetworkDevice to find the device's Id first. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER DeviceId +The Id of the device to remove, as shown by Get-PiHoleNetworkDevice + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -DeviceId 5 + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/network/devices/-id-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [int]$DeviceId, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Target = "Pi-Hole network device $DeviceId" + if ($PSCmdlet.ShouldProcess($Target, "Remove device")) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/network/devices/$DeviceId" + Method = "Delete" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + # A successful delete returns 204 No Content, so there's no response body to + # build a rich object from. + $Object = [PSCustomObject]@{ + DeviceId = $DeviceId + Status = "Removed" + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/README.md b/README.md index 37671ed..d1cf8e7 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,16 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Remove-PiHoleDomain` | Remove a domain | | `Update-PiHoleDomain` | Update a domain | +### Client Management + +| Function | Description | +|---|---| +| `Get-PiHoleClient` | Get clients | +| `Get-PiHoleClientSuggestion` | Get client suggestions | +| `New-PiHoleClient` | Add a new client | +| `Remove-PiHoleClient` | Remove a client | +| `Update-PiHoleClient` | Update a client | + ### Metrics | Function | Description | @@ -168,9 +178,14 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleInfoSystem` | Get info about various system parameters | | `Get-PiHoleInfoVersion` | Get Pi-hole version | | `Get-PiHoleLogWebserver` | Get webserver log content | +| `Get-PiHoleNetworkDevice` | Get info about the devices in your local network as seen by your Pi-hole | +| `Get-PiHoleNetworkGateway` | Get info about the gateway of your Pi-hole | +| `Get-PiHoleNetworkInterface` | Get info about the interfaces of your Pi-hole | +| `Get-PiHoleNetworkRoute` | Get info about the routes of your Pi-hole | | `Get-PiHolePadd` | Get summarized data for PADD | | `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | +| `Remove-PiHoleNetworkDevice` | Delete a device from the network table | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index a0deb07..1cc2273 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -100,8 +100,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:33 AM +DateAdded : 9/27/2026 11:54:34 AM +DateModified : 9/27/2026 11:54:34 AM ``` ### Get-PiHoleGroup @@ -137,8 +137,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:33 AM +DateAdded : 9/27/2026 11:54:34 AM +DateModified : 9/27/2026 11:54:34 AM ``` ### Update-PiHoleGroup @@ -153,8 +153,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:51 AM +DateAdded : 9/27/2026 11:54:34 AM +DateModified : 9/27/2026 11:55:06 AM ``` ### Remove-PiHoleGroup @@ -184,8 +184,8 @@ Comment : Example list Groups : {Default} Enabled : True Id : 65 -DateAdded : 9/27/2026 7:46:28 AM -DateModified : 9/27/2026 7:46:28 AM +DateAdded : 9/27/2026 11:56:17 AM +DateModified : 9/27/2026 11:56:17 AM Type : Block DateUpdated : Number : 0 @@ -214,7 +214,7 @@ DateUpdated : 7/6/2025 2:20:13 AM Number : 74761 InvalidDomains : 1 AbpEntries : 0 -Status : 1 +Status : 2 Address : https://adaway.org/hosts.txt Comment : @@ -288,8 +288,8 @@ Comment : Example list Groups : {Default} Enabled : False Id : 65 -DateAdded : 9/27/2026 7:46:28 AM -DateModified : 9/27/2026 7:47:01 AM +DateAdded : 9/27/2026 11:56:17 AM +DateModified : 9/27/2026 11:57:19 AM Type : Block DateUpdated : Number : 0 @@ -319,7 +319,7 @@ DateUpdated : 9/26/2026 11:28:08 PM Number : 74761 InvalidDomains : 1 AbpEntries : 0 -Status : 1 +Status : 2 Domain : doubleclick.net Address : https://adaway.org/hosts.txt @@ -414,9 +414,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:47:39 AM +Id : 12 +DateAdded : 9/27/2026 11:58:32 AM +DateModified : 9/27/2026 11:58:32 AM ``` ### Get-PiHoleDomain @@ -434,9 +434,20 @@ Kind : exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:47:39 AM +Id : 12 +DateAdded : 9/27/2026 11:58:32 AM +DateModified : 9/27/2026 11:58:32 AM + +Domain : piholeshell-test-domain.example.com +Unicode : piholeshell-test-domain.example.com +Type : allow +Kind : exact +Comment : +Groups : {Default} +Enabled : True +Id : 13 +DateAdded : 9/27/2026 11:58:40 AM +DateModified : 9/27/2026 11:58:40 AM ``` ### Update-PiHoleDomain @@ -454,9 +465,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : False -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:48:12 AM +Id : 12 +DateAdded : 9/27/2026 11:58:32 AM +DateModified : 9/27/2026 11:59:34 AM ``` ### Remove-PiHoleDomain @@ -473,6 +484,81 @@ Kind : Exact Status : Removed ``` +## Client Management + +### New-PiHoleClient + +```powershell +New-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Example client" +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Example client +Groups : {Default} +Id : 1 +DateAdded : 9/27/2026 12:00:33 PM +DateModified : 9/27/2026 12:00:33 PM +``` + +### Get-PiHoleClient + +```powershell +Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Example client +Groups : {Default} +Id : 1 +DateAdded : 9/27/2026 12:00:33 PM +DateModified : 9/27/2026 12:00:33 PM +``` + +### Get-PiHoleClientSuggestion + +```powershell +Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Update-PiHoleClient + +```powershell +Update-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Updated comment" +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Updated comment +Groups : {Default} +Id : 1 +DateAdded : 9/27/2026 12:00:33 PM +DateModified : 9/27/2026 12:01:45 PM +``` + +### Remove-PiHoleClient + +```powershell +Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" +``` + +``` + +Client : 192.168.99.99 +Status : Removed +``` + ## Metrics ### Get-PiHoleStatsSummary @@ -483,18 +569,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 281 -Blocked : 84 -PercentBlocked : 29.893238067627 -UniqueDomains : 21 -Forwarded : 37 -Cached : 160 +Total : 0 +Blocked : 0 +PercentBlocked : 0 +UniqueDomains : 0 +Forwarded : 0 +Cached : 0 Frequency : 0 -Types : @{A=130; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=21; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=84; Forwarded=37; Cache=139; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=21; ExternalBlockedEde15=0} -Replies : @{Unknown=12; Nodata=17; Nxdomain=3; Cname=0; Ip=231; Domain=18; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} -Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=499226; LastUpdate=1790483337} +Types : @{A=0; AAAA=0; ANY=0; SRV=0; SOA=0; PTR=0; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=0; Forwarded=0; Cache=0; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=0; ExternalBlockedEde15=0} +Replies : @{Unknown=0; Nodata=0; Nxdomain=0; Cname=0; Ip=0; Domain=0; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Clients : @{Active=0; Total=0} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790527593} ``` ### Get-PiHoleStatsRecentBlocked @@ -504,8 +590,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -Blocked : googleadservices.com +(no output) ``` ### Get-PiHoleStatsQueryType @@ -517,10 +602,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 130 +Count : 0 Type : AAAA -Count : 130 +Count : 0 Type : ANY Count : 0 @@ -541,23 +626,7 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -Domain : wikipedia.org -Count : 20 - -Domain : bbc.com -Count : 18 - -Domain : python.org -Count : 18 - -Domain : example.com -Count : 18 - -Domain : mozilla.org -Count : 16 - -_(showing 5 of 10 results)_ +(no output) ``` ### Get-PiHoleStatsTopClient @@ -567,14 +636,7 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -IP : 192.168.1.162 -Name : -Count : 260 - -IP : 127.0.0.1 -Name : localhost -Count : 21 +(no output) ``` ### Get-PiHoleStatsUpstream @@ -585,9 +647,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 281 -ForwardedQueries : 37 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=84; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=160; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=37; ResponseTime=0.024111141761144; Variance=0.00213313327521311}} +TotalQueries : 0 +ForwardedQueries : 0 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -598,10 +660,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {wikipedia.org, bbc.com, python.org, example.com…} -ClientIp : {192.168.1.162, 127.0.0.1} -ClientName : {localhost} -Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted} +Domain : {} +ClientIp : {} +ClientName : {} +Upstream : {blocklist, cache, permitted} Type : {A, AAAA, ANY, SRV…} Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} @@ -618,10 +680,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 8 +SumQueries : 0 SumBlocked : 0 PercentBlocked : 0 -TotalClients : 1 +TotalClients : 0 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -647,7 +709,7 @@ Type : SRV Count : 0 Type : SOA -Count : 8 +Count : 0 _(showing 5 of 16 results)_ ``` @@ -661,9 +723,7 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -Domain : 1.0.0.127.in-addr.arpa -Count : 8 +(no output) ``` ### Get-PiHoleStatsDatabaseTopClient @@ -675,10 +735,7 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -IP : 127.0.0.1 -Name : localhost -Count : 8 +(no output) ``` ### Get-PiHoleStatsDatabaseUpstream @@ -691,9 +748,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 8 +TotalQueries : 0 ForwardedQueries : 0 -Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}} +Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}} ``` ## Configuration & Diagnostics @@ -727,22 +784,22 @@ Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password CpuPercent : 0 MemoryPercent : 0 -ActiveClients : 2 +ActiveClients : 0 Blocking : enabled -Cache : @{Size=10000; Inserted=96; Evicted=0} +Cache : @{Size=10000; Inserted=0; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} -GravitySize : 499226 +GravitySize : 496171 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 389 -Queries : @{Total=281; Blocked=84; PercentBlocked=29.893238067627; QueryFrequency=0} -RecentBlocked : googleadservices.com -Sensors : @{CpuTemp=36.318; HotLimit=60; Unit=C} -System : @{Uptime=629874; Memory=; Procs=70; Cpu=; Ftl=} -TopBlocked : googleadservices.com -TopClient : 192.168.1.162 -TopDomain : wikipedia.org +Queries : @{Total=0; Blocked=0; PercentBlocked=0; QueryFrequency=0} +RecentBlocked : +Sensors : @{CpuTemp=40.084; HotLimit=60; Unit=C} +System : @{Uptime=644738; Memory=; Procs=71; Cpu=; Ftl=} +TopBlocked : +TopClient : +TopDomain : Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -779,11 +836,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 629884 +Uptime : 644757 Memory : @{Ram=; Swap=} -Procs : 70 -Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=} -Ftl : @{PercentMemory=2.40430068969727; PercentCpu=96.3000030517578} +Procs : 71 +Cpu : @{NumProcessors=1; PercentCpu=100; Load=} +Ftl : @{PercentMemory=2.33196616172791; PercentCpu=99.0999984741211} ``` ### Get-PiHoleInfoFtl @@ -794,16 +851,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Database : @{Gravity=499226; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} +Database : @{Gravity=496171; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 -Clients : @{Total=2; Active=2} +Clients : @{Total=0; Active=0} Pid : 389 -Uptime : 51790377.711423 -PercentMemory : 2.40430068969727 -PercentCpu : 96.3000030517578 +Uptime : 495351.278596 +PercentMemory : 2.32703423500061 +PercentCpu : 99.3000030517578 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=96; DnsCacheLiveFreed=0; DnsQueriesForwarded=58; DnsAuthAnswered=0; DnsLocalAnswered=223; DnsStaleAnswered=21; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} +Dnsmasq : @{DnsCacheInserted=0; DnsCacheLiveFreed=0; DnsQueriesForwarded=0; DnsAuthAnswered=0; DnsLocalAnswered=0; DnsStaleAnswered=0; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} ``` ### Get-PiHoleInfoSensors @@ -815,7 +872,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password ``` List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}} -CpuTemp : 36.856 +CpuTemp : 40.622 HotLimit : 60 Unit : C ``` @@ -832,12 +889,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/27/2026 4:04:00 AM -ChangeTime : 9/27/2026 4:04:00 AM +ModifiedTime : 9/27/2026 11:44:09 AM +ChangeTime : 9/27/2026 11:44:09 AM Owner : @{User=; Group=} -Queries : 281 -EarliestTimestamp : 9/26/2026 7:40:00 AM -QueriesDisk : 281 +Queries : 0 +EarliestTimestamp : 9/26/2026 11:50:00 AM +QueriesDisk : 0 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -905,7 +962,12 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Id : 1 +Timestamp : 9/27/2026 11:49:11 AM +Type : LOAD +Plain : Long-term load (15min avg) larger than number of processors: 1.3 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.3 > 1
This may slow down DNS resolution and can cause bottlenecks. ``` ### Get-PiHoleInfoMessageCount @@ -916,7 +978,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password ``` -Count : 0 +Count : 1 ``` ### Remove-PiHoleInfoMessage @@ -940,7 +1002,7 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : @{Timestamp=9/26/2026 5:21:18 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} +Log : @{Timestamp=9/27/2026 11:44:11 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} NextID : 1 Pid : 389 File : /var/log/pihole/webserver.log @@ -970,31 +1032,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM +Timestamp : 9/26/2026 12:05:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:05:00 AM +Timestamp : 9/26/2026 12:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:15:00 AM +Timestamp : 9/26/2026 12:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:25:00 AM +Timestamp : 9/26/2026 12:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:35:00 AM +Timestamp : 9/26/2026 12:45:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1011,20 +1073,20 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 9/26/2026 12:05:00 PM +Clients : @{IP=others; Name=; Count=0} -Timestamp : 9/26/2026 8:05:00 AM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 9/26/2026 12:15:00 PM +Clients : @{IP=others; Name=; Count=0} -Timestamp : 9/26/2026 8:15:00 AM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 9/26/2026 12:25:00 PM +Clients : @{IP=others; Name=; Count=0} -Timestamp : 9/26/2026 8:25:00 AM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 9/26/2026 12:35:00 PM +Clients : @{IP=others; Name=; Count=0} -Timestamp : 9/26/2026 8:35:00 AM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 9/26/2026 12:45:00 PM +Clients : @{IP=others; Name=; Count=0} _(showing 5 of 145 results)_ ``` @@ -1037,67 +1099,154 @@ _Defaults to the last 8 hours; pass -From/-Until for a different window._ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` +``` +(no output) ``` -Timestamp : 9/27/2026 12:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +### Get-PiHoleHistoryDatabaseClient -Timestamp : 9/27/2026 1:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +_Defaults to the last 8 hours; pass -From/-Until for a different window._ -Timestamp : 9/27/2026 2:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +```powershell +Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password +``` -Timestamp : 9/27/2026 3:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +``` +(no output) +``` -Timestamp : 9/27/2026 4:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +### Get-PiHoleNetworkGateway -_(showing 5 of 8 results)_ +```powershell +Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password ``` -### Get-PiHoleHistoryDatabaseClient +``` -_Defaults to the last 8 hours; pass -From/-Until for a different window._ +Family : inet +Interface : wlan0 +Address : 192.168.1.1 +Local : 192.168.1.248 +``` + +### Get-PiHoleNetworkRoute ```powershell -Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password +Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Table : 254 +Family : inet +Protocol : boot +Scope : universe +Type : unicast +Flags : +Gateway : 192.168.1.1 +Oif : wlan0 +Dst : default + +Table : 254 +Family : inet +Protocol : kernel +Scope : link +Type : unicast +Flags : +Dst : 192.168.1.0 +Prefsrc : 192.168.1.248 +Oif : wlan0 + +Table : 255 +Family : inet +Protocol : kernel +Scope : host +Type : local +Flags : +Dst : 127.0.0.0 +Prefsrc : 127.0.0.1 +Oif : lo + +Table : 255 +Family : inet +Protocol : kernel +Scope : host +Type : local +Flags : +Dst : 127.0.0.1 +Prefsrc : 127.0.0.1 +Oif : lo + +Table : 255 +Family : inet +Protocol : kernel +Scope : link +Type : broadcast +Flags : +Dst : 127.255.255.255 +Prefsrc : 127.0.0.1 +Oif : lo + +_(showing 5 of 11 results)_ +``` + +### Get-PiHoleNetworkInterface + +```powershell +Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $Password ``` ``` -Timestamp : 9/27/2026 7:00:00 AM -Clients : @{ClientId=3; Count=1} +Name : lo +Speed : +Type : loopback +Flags : {up, loopback, running, lower_up} +State : unknown +Carrier : True +ProtoDown : False +Address : 00:00:00:00:00:00 +Broadcast : 00:00:00:00:00:00 +Stats : @{RxBytes=; TxBytes=; Bits=64} +Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1789883183.02; Tstamp=1789883183.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883183.02; Tstamp=1789883183.02}} -Timestamp : 9/27/2026 6:00:00 AM -Clients : @{ClientId=3; Count=1} +Name : wlan0 +Speed : +Type : ether +Flags : {up, broadcast, running, multicast…} +State : up +Carrier : True +ProtoDown : False +Address : b8:27:eb:11:a4:d5 +Broadcast : ff:ff:ff:ff:ff:ff +PermAddress : b8:27:eb:11:a4:d5 +Stats : @{RxBytes=; TxBytes=; Bits=64} +Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=61931; Valid=61931; Cstamp=1789883203.32; Tstamp=1790074734.89}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883202.81; Tstamp=1789883202.81}} +``` -Timestamp : 9/27/2026 5:00:00 AM -Clients : @{ClientId=3; Count=1} +### Get-PiHoleNetworkDevice -Timestamp : 9/27/2026 4:00:00 AM -Clients : @{ClientId=3; Count=1} +_Shows up to 10 devices by default; pass -MaxDevices for more._ -Timestamp : 9/27/2026 3:00:00 AM -Clients : @{ClientId=3; Count=1} +```powershell +Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password +``` -_(showing 5 of 8 results)_ +``` +(no output) +``` + +### Remove-PiHoleNetworkDevice + +_Network devices arise from Pi-hole having genuinely seen a device and cannot be manufactured on demand, so this example shows the error for a device ID that does not exist rather than a fabricated success._ + +```powershell +Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password -DeviceId 5 +``` + +``` + +Error : Response status code does not indicate success: 404 (Not Found). ``` ## Authentication @@ -1111,13 +1260,41 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/27/2026 11:50:34 AM +LastActive : 9/27/2026 11:50:34 AM +ValidUntil : 9/27/2026 12:20:34 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 +XForwardedFor : +App : True +Cli : False + +Id : 1 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/27/2026 7:43:32 AM -LastActive : 9/27/2026 7:43:36 AM -ValidUntil : 9/27/2026 8:13:36 AM +LoginAt : 9/27/2026 11:50:38 AM +LastActive : 9/27/2026 11:50:47 AM +ValidUntil : 9/27/2026 12:20:47 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 +XForwardedFor : +App : True +Cli : False + +Id : 2 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/27/2026 11:50:39 AM +LastActive : 9/27/2026 11:50:39 AM +ValidUntil : 9/27/2026 12:20:39 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : diff --git a/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1 new file mode 100644 index 0000000..ef4ce5f --- /dev/null +++ b/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1 @@ -0,0 +1,62 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleClient (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestClient = '192.168.99.99' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test client behind + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client' | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'returns clients as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'filters by Client' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Client | Should -Be $script:TestClient + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1 b/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1 new file mode 100644 index 0000000..17d94ef --- /dev/null +++ b/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1 @@ -0,0 +1,38 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleClientSuggestion (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'succeeds and returns without throwing, even with no suggestions available' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl } | Should -Not -Throw + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1 new file mode 100644 index 0000000..652b554 --- /dev/null +++ b/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1 @@ -0,0 +1,99 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'New-PiHoleClient (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestClient = '192.168.99.99' + $script:TestGroupName = 'PesterClientGroup' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test client/group behind + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + + New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + # Ensures the test client/group are never left behind for other test files to trip over + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds a new client and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Client | Should -Be $script:TestClient + $result.Groups | Should -Contain 'Default' + + # Clean up immediately so the next test starts from a known (client-absent) state + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'applies a non-default group passed via -Group' -Skip:(-not $script:ConfigAvailable) { + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group $script:TestGroupName + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Groups | Should -Be @($script:TestGroupName) + $result.Groups | Should -Not -Contain 'Default' + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when -Group names a group that does not exist, without creating the client' -Skip:(-not $script:ConfigAvailable) { + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group 'DefinitelyNotARealGroup' -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient + $remaining | Should -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.clients[0].client | Should -Be $script:TestClient + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when the client already exists' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1 new file mode 100644 index 0000000..a30919d --- /dev/null +++ b/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1 @@ -0,0 +1,67 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleClient (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestClient = '192.168.99.99' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test client behind + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + # Defensive cleanup in case a test left the client behind + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an existing client and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client' | Out-Null + + $result = Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Client | Should -Be $script:TestClient + $result.Status | Should -Be 'Removed' + + $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient + $remaining | Should -BeNullOrEmpty + } + + It 'removes the client when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here - + # the client actually being gone afterward is the real signal of success. + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -RawOutput $true -Confirm:$false + + $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient + $remaining | Should -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 new file mode 100644 index 0000000..d3461de --- /dev/null +++ b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 @@ -0,0 +1,109 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestClient = '192.168.99.99' + $script:TestGroupName = 'PesterClientGroup' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test client/group behind + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + + New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'updates only the comment, preserving Group' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'original comment' | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'updated comment' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Comment | Should -Be 'updated comment' + $result.Groups | Should -Contain 'Default' + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'updates only Group, preserving the current comment' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'keep this comment' | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group $script:TestGroupName + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Comment | Should -Be 'keep this comment' + $result.Groups | Should -Be @($script:TestGroupName) + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when -Group names a group that does not exist' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group 'DefinitelyNotARealGroup' -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'raw output test' -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.clients[0].comment | Should -Be 'raw output test' + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when neither Comment nor Group is specified' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null + } + + It 'errors when the client does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client '10.10.10.10' -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1 new file mode 100644 index 0000000..3b96ca1 --- /dev/null +++ b/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1 @@ -0,0 +1,44 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleNetworkDevice (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + # The test server may not have tracked any devices recently, so this only confirms the call + # succeeds rather than asserting specific devices are present. + It 'succeeds and returns without throwing, even with no devices tracked' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl } | Should -Not -Throw + } + + It 'accepts -MaxDevices and -MaxAddresses without error' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxDevices 5 -MaxAddresses 2 } | Should -Not -Throw + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1 new file mode 100644 index 0000000..e5118e6 --- /dev/null +++ b/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1 @@ -0,0 +1,45 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleNetworkGateway (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns gateway info as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1 new file mode 100644 index 0000000..65f66e6 --- /dev/null +++ b/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1 @@ -0,0 +1,45 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleNetworkInterface (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns interfaces as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1 new file mode 100644 index 0000000..9e7465a --- /dev/null +++ b/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1 @@ -0,0 +1,45 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleNetworkRoute (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns routes as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) { + { Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1 b/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1 new file mode 100644 index 0000000..0bae3d4 --- /dev/null +++ b/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1 @@ -0,0 +1,38 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: Network devices arise from Pi-hole having genuinely seen a device on the network and +# can't be manufactured on demand, so there's no reliable way to test a genuine successful +# delete here. These tests instead verify the error paths, which still exercise the real +# request/auth flow. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleNetworkDevice (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'errors when the device does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -DeviceId 999999 -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -DeviceId 1 -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index ad26ca4..0f55591 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -52,6 +52,7 @@ $categoryOrder = [ordered]@{ GroupManagement = 'Group Management' ListManagement = 'List Management' DomainManagement = 'Domain Management' + ClientManagement = 'Client Management' Metrics = 'Metrics' Config = 'Configuration & Diagnostics' Authentication = 'Authentication' @@ -284,6 +285,35 @@ Add-Example -Category DomainManagement -FunctionName 'Remove-PiHoleDomain' ` -Result (Remove-PiHoleDomain -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Domain $docsDomain -Type Allow -Kind Exact -Confirm:$false) #endregion +#region ClientManagement +$docsClient = '192.168.99.99' +Invoke-Quietly { Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Confirm:$false } + +Add-Example -Category ClientManagement -FunctionName 'New-PiHoleClient' ` + -Invocation "New-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`" -Comment `"Example client`"" ` + -Result (New-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Comment 'Example client') + +# Same settle-time reasoning as ListManagement/DomainManagement above - give the test server a +# moment before relying on the client just being created. +Start-Sleep -Seconds 3 + +Add-Example -Category ClientManagement -FunctionName 'Get-PiHoleClient' ` + -Invocation 'Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category ClientManagement -FunctionName 'Get-PiHoleClientSuggestion' ` + -Invocation 'Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category ClientManagement -FunctionName 'Update-PiHoleClient' ` + -Invocation "Update-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`" -Comment `"Updated comment`"" ` + -Result (Update-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Comment 'Updated comment') + +Add-Example -Category ClientManagement -FunctionName 'Remove-PiHoleClient' ` + -Invocation "Remove-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`"" ` + -Result (Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Confirm:$false) +#endregion + #region Metrics foreach ($fn in 'Get-PiHoleStatsSummary', 'Get-PiHoleStatsRecentBlocked', 'Get-PiHoleStatsQueryType', 'Get-PiHoleStatsTopDomain', 'Get-PiHoleStatsTopClient', 'Get-PiHoleStatsUpstream', 'Get-PiHoleStatsQuerySuggestions') { Add-Example -Category Metrics -FunctionName $fn ` @@ -315,6 +345,34 @@ foreach ($fn in 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryDatabaseClient') } #endregion +#region NetworkInformation +# The NetworkInformation folder isn't its own README category either - same fallback-folding +# reasoning as History above. +Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkGateway' ` + -Invocation 'Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkRoute' ` + -Invocation 'Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkInterface' ` + -Invocation 'Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkDevice' ` + -Invocation 'Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password' ` + -Note 'Shows up to 10 devices by default; pass -MaxDevices for more.' ` + -Result (Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +$dummyDeviceId = 999999 +Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -DeviceId $dummyDeviceId -Confirm:$false -ErrorVariable removeDeviceError -ErrorAction SilentlyContinue | Out-Null +Add-Example -Category Config -FunctionName 'Remove-PiHoleNetworkDevice' ` + -Invocation 'Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password -DeviceId 5' ` + -Note 'Network devices arise from Pi-hole having genuinely seen a device and cannot be manufactured on demand, so this example shows the error for a device ID that does not exist rather than a fabricated success.' ` + -Result $(if ($removeDeviceError) { [PSCustomObject]@{ Error = $removeDeviceError[-1].Exception.Message } }) +#endregion + #region Actions if ($IncludeDisruptive) { Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' ` diff --git a/tools/Update-ReadmeCommandReference.ps1 b/tools/Update-ReadmeCommandReference.ps1 index b31b5c0..9ea8337 100644 --- a/tools/Update-ReadmeCommandReference.ps1 +++ b/tools/Update-ReadmeCommandReference.ps1 @@ -24,6 +24,7 @@ $displayNames = [ordered]@{ GroupManagement = 'Group Management' ListManagement = 'List Management' DomainManagement = 'Domain Management' + ClientManagement = 'Client Management' Metrics = 'Metrics' Config = 'Configuration & Diagnostics' Padd = 'Configuration & Diagnostics' @@ -31,7 +32,7 @@ $displayNames = [ordered]@{ Teleporter = 'Configuration & Diagnostics' Authentication = 'Authentication' } -$categoryOrder = @('Actions', 'DnsControl', 'GroupManagement', 'ListManagement', 'DomainManagement', 'Metrics', 'Config', 'Authentication') +$categoryOrder = @('Actions', 'DnsControl', 'GroupManagement', 'ListManagement', 'DomainManagement', 'ClientManagement', 'Metrics', 'Config', 'Authentication') $categoryIntros = @{ Authentication = 'Session handling is automatic for every command above, but these are available for managing sessions directly:' } From be8eccd08f0a296a0cd5ca53d34345bc6a4f3599 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 27 Sep 2026 14:31:12 -0500 Subject: [PATCH 2/3] feat: add Config write/property functions, completing the Config API area Extends Get-PiHoleConfig with -Element (request one subset of the config tree, e.g. "dns/upstreams") and -Detailed. Adds Set-PiHoleConfig (PATCH /config), Add/Remove-PiHoleConfigArrayItem (PUT/DELETE /config/{element}/{value}, for array-type settings like dns/hosts and dns/cnameRecords), and Get-PiHoleConfigProperty (the list of settings that can never be changed via the API). All three write functions require the app password's "app_sudo" flag to be enabled in Pi-hole (Settings > All Settings) - Pi-hole blocks config changes from app passwords by default, and there's no way to enable it via the API itself (avoiding that would defeat the point). Verified against a real server with app_sudo both disabled and enabled: a real config value can be changed and reverted, an array item can be added and removed, and a genuinely read-only property (misc.readOnly) is correctly rejected. Adds ConvertTo-PiHoleFriendlyErrorMessage (Private/Misc.ps1), used by the three write functions' error handling: Pi-hole's own error responses carry a clearer message/hint than the generic HTTP exception text, and for the app_sudo case specifically, a concrete pointer to where to enable it - previously the module surfaced only "403 (Forbidden)" or "400 (Bad Request)" with no explanation. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 2 +- PiHoleShell/Private/Misc.ps1 | 38 ++ .../Config/Add-PiHoleConfigArrayItem.ps1 | 101 +++++ .../Public/Config/Get-PiHoleConfig.ps1 | 26 +- .../Config/Get-PiHoleConfigProperty.ps1 | 73 ++++ .../Config/Remove-PiHoleConfigArrayItem.ps1 | 103 +++++ .../Public/Config/Set-PiHoleConfig.ps1 | 97 +++++ README.md | 4 + docs/EXAMPLES.md | 365 +++++++++++------- ...iHoleConfigArrayItem.Integration.Tests.ps1 | 68 ++++ .../Get-PiHoleConfig.Integration.Tests.ps1 | 9 + ...PiHoleConfigProperty.Integration.Tests.ps1 | 43 +++ ...iHoleConfigArrayItem.Integration.Tests.ps1 | 76 ++++ .../Set-PiHoleConfig.Integration.Tests.ps1 | 69 ++++ tools/Update-ExampleOutput.ps1 | 42 ++ 15 files changed, 969 insertions(+), 147 deletions(-) create mode 100644 PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 create mode 100644 PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 create mode 100644 PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 create mode 100644 PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 create mode 100644 tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 create mode 100644 tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 create mode 100644 tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 create mode 100644 tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 59967d5..9524952 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -24,7 +24,7 @@ Export-ModuleMember -Function @( #DnsControl 'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', ` #Config - 'Get-PiHoleConfig', ` + 'Get-PiHoleConfig', 'Set-PiHoleConfig', 'Add-PiHoleConfigArrayItem', 'Remove-PiHoleConfigArrayItem', 'Get-PiHoleConfigProperty', ` #Padd 'Get-PiHolePadd', ` #Metrics diff --git a/PiHoleShell/Private/Misc.ps1 b/PiHoleShell/Private/Misc.ps1 index a537078..3b0c02e 100644 --- a/PiHoleShell/Private/Misc.ps1 +++ b/PiHoleShell/Private/Misc.ps1 @@ -98,6 +98,44 @@ function ConvertTo-PiHolePascalCaseObject { } } +function ConvertTo-PiHoleFriendlyErrorMessage { + #INTERNAL FUNCTION + # + # Pi-hole's own error responses often carry a clearer message/hint than the generic HTTP + # exception text (e.g. "Unable to change configuration (read-only): ...app_sudo is false" + # vs just "403 Forbidden"). This extracts and combines them when present, and adds a + # concrete pointer to fix the most common cause of a blocked config write - the app + # password's app_sudo setting - since Pi-hole's own hint says what's wrong but not how to + # fix it. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Falls back to the raw exception message when the response body isn't valid JSON - nothing to report.")] + param ( + [Parameter(Mandatory = $true)] + $ErrorRecord + ) + + $Message = $ErrorRecord.Exception.Message + + if ($ErrorRecord.ErrorDetails.Message) { + try { + $ApiError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).error + if ($ApiError.message) { + $Message = $ApiError.message + if ($ApiError.hint) { + $Message += ": $($ApiError.hint)" + } + if ($ApiError.hint -like '*app_sudo*') { + $Message += " Enable it in your Pi-hole admin UI under Settings > All Settings by searching for 'app_sudo' and setting webserver.api.app_sudo to true." + } + } + } + catch { + # ErrorDetails.Message wasn't valid JSON - fall back to the raw exception message + } + } + + return $Message +} + function Remove-PiHoleCurrentAuthSession { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")] [CmdletBinding()] diff --git a/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..2d31bc9 --- /dev/null +++ b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,101 @@ +function Add-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Add config array item + +.DESCRIPTION +Adds one item to an array-type Pi-hole configuration setting - for example, an upstream DNS +server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record (dns/cnameRecords). +Use Set-PiHoleConfig instead for non-array settings. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Element +The array-type configuration setting to add to, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The item to add. For dns/hosts this is " "; for dns/cnameRecords this is +",[,]" + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when adding several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Add-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/config/-element-/-value-')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Put" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + # A successful add returns 201 Created with no body, so there's no response to + # build a rich object from. + $Object = [PSCustomObject]@{ + Element = $Element + Value = $Value + Status = "Added" + } + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 index 12cb171..f6fe735 100644 --- a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 +++ b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 @@ -7,7 +7,8 @@ Get current configuration of Pi-hole Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver, files, misc, and debug settings). The formatted output mirrors the API response as nested objects with PascalCase property names, so the entire configuration is available for -inspection rather than a hand-picked subset. +inspection rather than a hand-picked subset. Pass -Element to request just one subset of +the tree instead of everything. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -15,6 +16,13 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h .PARAMETER Password The API Password you generated from your PiHole server +.PARAMETER Element +Only return this part of the configuration tree, as a slash-separated path (e.g. +"dns/upstreams" or "dns/hosts"). Omit to return the entire configuration + +.PARAMETER Detailed +Include detailed information about the configuration (e.g. value types and validation info) + .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,6 +34,9 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a .EXAMPLE (Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams + +.EXAMPLE +Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/upstreams" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -34,15 +45,26 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, + [string]$Element, + [Nullable[bool]]$Detailed, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($Element) { + $Uri += "/$($Element.Trim('/'))" + } + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/config" + Uri = $Uri Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 new file mode 100644 index 0000000..f9f6d50 --- /dev/null +++ b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 @@ -0,0 +1,73 @@ +function Get-PiHoleConfigProperty { + <# +.SYNOPSIS +Get special properties of your Pi-hole configuration + +.DESCRIPTION +Returns the configuration properties that cannot be changed through the API (e.g. because +they're only settable in pihole.toml, or are controlled by an environment variable), along with +why each one is restricted. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleConfigProperty -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config/_properties')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/config/_properties" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $ObjectFinal = foreach ($Item in $Response.config.read_only) { + [PSCustomObject]@{ + Key = $Item.key + Reason = $Item.reason + Description = $Item.description + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..f197ea8 --- /dev/null +++ b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,103 @@ +function Remove-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Delete config array item + +.DESCRIPTION +Removes one item from an array-type Pi-hole configuration setting - for example, an upstream +DNS server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record +(dns/cnameRecords). + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Element +The array-type configuration setting to remove from, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The exact item to remove, as it appears in the array (see Get-PiHoleConfig) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when removing several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/config/-element-/-value-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Target = "Pi-Hole config item $Value in $Element" + if ($PSCmdlet.ShouldProcess($Target, "Remove config array item")) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Delete" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + # A successful delete returns 204 No Content, so there's no response body to + # build a rich object from. + $Object = [PSCustomObject]@{ + Element = $Element + Value = $Value + Status = "Removed" + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 new file mode 100644 index 0000000..2c4eaf0 --- /dev/null +++ b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 @@ -0,0 +1,97 @@ +function Set-PiHoleConfig { + <# +.SYNOPSIS +Change configuration of your Pi-hole + +.DESCRIPTION +Updates one or more Pi-hole configuration settings in a single call. -Settings takes a +hashtable shaped like the configuration tree returned by Get-PiHoleConfig - only include the +properties you want to change; everything else is left untouched. Some changes require FTL to +restart to take effect; this happens automatically unless -Restart is set to $false. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. Some settings can never be +changed via the API at all (only in pihole.toml, or via an environment variable) - see +Get-PiHoleConfigProperty for the current list. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Settings +A hashtable of the configuration properties to change, shaped like the tree returned by +Get-PiHoleConfig (e.g. @{ dns = @{ CNAMEdeepInspect = $true } }) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when making several changes independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Set-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Settings @{ dns = @{ CNAMEdeepInspect = $true } } + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#patch-/config')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [hashtable]$Settings, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Body = @{ + config = $Settings + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Patch" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.config + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/README.md b/README.md index 37671ed..ad4c0a5 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,9 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | Function | Description | |---|---| +| `Add-PiHoleConfigArrayItem` | Add config array item | | `Get-PiHoleConfig` | Get current configuration of Pi-hole | +| `Get-PiHoleConfigProperty` | Get special properties of your Pi-hole configuration | | `Get-PiHoleHistory` | Get activity graph data | | `Get-PiHoleHistoryClient` | Get per-client activity graph data | | `Get-PiHoleHistoryDatabase` | Get activity graph data (long-term data) | @@ -170,7 +172,9 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleLogWebserver` | Get webserver log content | | `Get-PiHolePadd` | Get summarized data for PADD | | `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | +| `Remove-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | +| `Set-PiHoleConfig` | Change configuration of your Pi-hole | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index a0deb07..9f97dcf 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -99,9 +99,9 @@ New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHo Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:33 AM +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:02 PM ``` ### Get-PiHoleGroup @@ -136,9 +136,9 @@ DateModified : 7/1/2025 10:13:40 PM Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:33 AM +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:02 PM ``` ### Update-PiHoleGroup @@ -152,9 +152,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False -Id : 4 -DateAdded : 9/27/2026 7:45:33 AM -DateModified : 9/27/2026 7:45:51 AM +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:21 PM ``` ### Remove-PiHoleGroup @@ -183,9 +183,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : True -Id : 65 -DateAdded : 9/27/2026 7:46:28 AM -DateModified : 9/27/2026 7:46:28 AM +Id : 79 +DateAdded : 9/27/2026 2:18:57 PM +DateModified : 9/27/2026 2:18:57 PM Type : Block DateUpdated : Number : 0 @@ -214,7 +214,7 @@ DateUpdated : 7/6/2025 2:20:13 AM Number : 74761 InvalidDomains : 1 AbpEntries : 0 -Status : 1 +Status : 2 Address : https://adaway.org/hosts.txt Comment : @@ -287,9 +287,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : False -Id : 65 -DateAdded : 9/27/2026 7:46:28 AM -DateModified : 9/27/2026 7:47:01 AM +Id : 79 +DateAdded : 9/27/2026 2:18:57 PM +DateModified : 9/27/2026 2:19:31 PM Type : Block DateUpdated : Number : 0 @@ -319,7 +319,7 @@ DateUpdated : 9/26/2026 11:28:08 PM Number : 74761 InvalidDomains : 1 AbpEntries : 0 -Status : 1 +Status : 2 Domain : doubleclick.net Address : https://adaway.org/hosts.txt @@ -414,9 +414,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:47:39 AM +Id : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:09 PM ``` ### Get-PiHoleDomain @@ -434,9 +434,9 @@ Kind : exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:47:39 AM +Id : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:09 PM ``` ### Update-PiHoleDomain @@ -454,9 +454,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : False -Id : 22 -DateAdded : 9/27/2026 7:47:39 AM -DateModified : 9/27/2026 7:48:12 AM +Id : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:42 PM ``` ### Remove-PiHoleDomain @@ -483,18 +483,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 281 -Blocked : 84 -PercentBlocked : 29.893238067627 +Total : 265 +Blocked : 82 +PercentBlocked : 30.943395614624 UniqueDomains : 21 -Forwarded : 37 -Cached : 160 +Forwarded : 33 +Cached : 150 Frequency : 0 -Types : @{A=130; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=21; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=84; Forwarded=37; Cache=139; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=21; ExternalBlockedEde15=0} -Replies : @{Unknown=12; Nodata=17; Nxdomain=3; Cname=0; Ip=231; Domain=18; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=129; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=6; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=82; Forwarded=33; Cache=123; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=27; ExternalBlockedEde15=0} +Replies : @{Unknown=8; Nodata=20; Nxdomain=2; Cname=0; Ip=231; Domain=4; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=499226; LastUpdate=1790483337} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790530224} ``` ### Get-PiHoleStatsRecentBlocked @@ -505,7 +505,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : googleadservices.com +Blocked : adservice.google.com ``` ### Get-PiHoleStatsQueryType @@ -517,7 +517,7 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 130 +Count : 129 Type : AAAA Count : 130 @@ -542,19 +542,19 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : wikipedia.org -Count : 20 +Domain : python.org +Count : 22 -Domain : bbc.com -Count : 18 +Domain : microsoft.com +Count : 20 -Domain : python.org +Domain : nytimes.com Count : 18 Domain : example.com Count : 18 -Domain : mozilla.org +Domain : wikipedia.org Count : 16 _(showing 5 of 10 results)_ @@ -570,11 +570,11 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 260 +Count : 259 IP : 127.0.0.1 Name : localhost -Count : 21 +Count : 6 ``` ### Get-PiHoleStatsUpstream @@ -585,9 +585,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 281 -ForwardedQueries : 37 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=84; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=160; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=37; ResponseTime=0.024111141761144; Variance=0.00213313327521311}} +TotalQueries : 265 +ForwardedQueries : 33 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=82; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=150; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=33; ResponseTime=0.0300731555275295; Variance=0.0024513289105294}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -598,7 +598,7 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {wikipedia.org, bbc.com, python.org, example.com…} +Domain : {python.org, microsoft.com, nytimes.com, example.com…} ClientIp : {192.168.1.162, 127.0.0.1} ClientName : {localhost} Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted} @@ -618,10 +618,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 8 -SumBlocked : 0 -PercentBlocked : 0 -TotalClients : 1 +SumQueries : 265 +SumBlocked : 82 +PercentBlocked : 30.943395614624 +TotalClients : 3 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -635,7 +635,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 0 +Count : 130 Type : AAAA Count : 0 @@ -647,7 +647,7 @@ Type : SRV Count : 0 Type : SOA -Count : 8 +Count : 6 _(showing 5 of 16 results)_ ``` @@ -662,8 +662,22 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : 1.0.0.127.in-addr.arpa -Count : 8 +Domain : python.org +Count : 22 + +Domain : microsoft.com +Count : 20 + +Domain : nytimes.com +Count : 18 + +Domain : example.com +Count : 18 + +Domain : wikipedia.org +Count : 16 + +_(showing 5 of 10 results)_ ``` ### Get-PiHoleStatsDatabaseTopClient @@ -676,9 +690,17 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` +IP : 192.168.1.162 +Name : +Count : 177 + IP : 127.0.0.1 Name : localhost -Count : 8 +Count : 4 + +IP : 127.0.0.1 +Name : +Count : 2 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -691,9 +713,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 8 -ForwardedQueries : 0 -Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}} +TotalQueries : 265 +ForwardedQueries : 33 +Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=1; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=32; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=123; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=109; ResponseTime=0; Variance=0}} ``` ## Configuration & Diagnostics @@ -717,6 +739,64 @@ Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsma Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=False; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} ``` +### Get-PiHoleConfigProperty + +```powershell +Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Key : misc.readOnly +Reason : read_only +Description : Variable can only be set in pihole.toml, not via API +``` + +### Set-PiHoleConfig + +```powershell +Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } } +``` + +``` + +Dns : @{Upstreams=System.Object[]; CNAMEdeepInspect=True; BlockESNI=True; EDNS0ECS=True; IgnoreLocalhost=False; ShowDNSSEC=True; AnalyzeOnlyAandAAAA=False; PiholePTR=PI.HOLE; ReplyWhenBusy=ALLOW; BlockTTL=2; Hosts=System.Object[]; DomainNeeded=False; ExpandHosts=False; BogusPriv=True; Dnssec=False; Interface=wlan0; HostRecord=; ListeningMode=LOCAL; QueryLogging=False; CnameRecords=; Port=53; Localise=True; RevServers=; Domain=; Cache=; Blocking=; SpecialDomains=; Reply=; RateLimit=} +Dhcp : @{Active=False; Start=; End=; Router=; Netmask=; LeaseTime=; Ipv6=False; RapidCommit=False; MultiDNS=False; Logging=False; IgnoreUnknownClients=False; Hosts=} +Ntp : @{Ipv4=; Ipv6=; Sync=} +Resolver : @{ResolveIPv4=True; ResolveIPv6=True; MacNames=True; NetworkNames=True; RefreshNames=IPV4_ONLY} +Database : @{DBimport=True; MaxDBdays=2; DBinterval=60; UseWAL=True; ForceDisk=False; Network=} +Webserver : @{Domain=pi.hole; Acl=; Port=8089,8489s; Threads=50; Headers=System.Object[]; ServeAll=False; AdvancedOpts=; Session=; Tls=; Paths=; Interface=; Api=} +Files : @{Database=/etc/pihole/pihole-FTL.db; TmpDb=/etc/pihole/pihole-tmp.db; Gravity=/etc/pihole/gravity.db; GravityTmp=/tmp; Macvendor=/etc/pihole/macvendor.db; Pcap=; Log=} +Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsmasqD=False; DnsmasqLines=; ExtraLogging=False; ReadOnly=False; NormalizeCPU=True; HideDnsmasqWarn=False; HideConnectionError=False; Check=} +Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=True; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} +``` + +### Add-PiHoleConfigArrayItem + +```powershell +Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Added +``` + +### Remove-PiHoleConfigArrayItem + +```powershell +Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Removed +``` + ### Get-PiHolePadd ```powershell @@ -729,20 +809,20 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=96; Evicted=0} +Cache : @{Size=10000; Inserted=127; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} -GravitySize : 499226 +GravitySize : 496171 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 389 -Queries : @{Total=281; Blocked=84; PercentBlocked=29.893238067627; QueryFrequency=0} -RecentBlocked : googleadservices.com -Sensors : @{CpuTemp=36.318; HotLimit=60; Unit=C} -System : @{Uptime=629874; Memory=; Procs=70; Cpu=; Ftl=} -TopBlocked : googleadservices.com +Queries : @{Total=265; Blocked=82; PercentBlocked=30.943395614624; QueryFrequency=0} +RecentBlocked : adservice.google.com +Sensors : @{CpuTemp=37.932; HotLimit=60; Unit=C} +System : @{Uptime=653422; Memory=; Procs=80; Cpu=; Ftl=} +TopBlocked : analytics.google.com TopClient : 192.168.1.162 -TopDomain : wikipedia.org +TopDomain : python.org Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -779,11 +859,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 629884 +Uptime : 653433 Memory : @{Ram=; Swap=} -Procs : 70 -Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=} -Ftl : @{PercentMemory=2.40430068969727; PercentCpu=96.3000030517578} +Procs : 81 +Cpu : @{NumProcessors=1; PercentCpu=96.1999969482422; Load=} +Ftl : @{PercentMemory=3.29122042655945; PercentCpu=95.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -794,16 +874,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Database : @{Gravity=499226; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} +Database : @{Gravity=496171; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} Pid : 389 -Uptime : 51790377.711423 -PercentMemory : 2.40430068969727 -PercentCpu : 96.3000030517578 +Uptime : 6534699.497781 +PercentMemory : 3.29122042655945 +PercentCpu : 96.1999969482422 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=96; DnsCacheLiveFreed=0; DnsQueriesForwarded=58; DnsAuthAnswered=0; DnsLocalAnswered=223; DnsStaleAnswered=21; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} +Dnsmasq : @{DnsCacheInserted=127; DnsCacheLiveFreed=0; DnsQueriesForwarded=60; DnsAuthAnswered=0; DnsLocalAnswered=205; DnsStaleAnswered=27; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} ``` ### Get-PiHoleInfoSensors @@ -815,7 +895,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password ``` List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}} -CpuTemp : 36.856 +CpuTemp : 37.932 HotLimit : 60 Unit : C ``` @@ -832,12 +912,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/27/2026 4:04:00 AM -ChangeTime : 9/27/2026 4:04:00 AM +ModifiedTime : 9/27/2026 12:28:00 PM +ChangeTime : 9/27/2026 12:28:00 PM Owner : @{User=; Group=} -Queries : 281 -EarliestTimestamp : 9/26/2026 7:40:00 AM -QueriesDisk : 281 +Queries : 265 +EarliestTimestamp : 9/26/2026 2:10:00 PM +QueriesDisk : 265 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -905,7 +985,12 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Id : 1 +Timestamp : 9/27/2026 1:13:02 PM +Type : LOAD +Plain : Long-term load (15min avg) larger than number of processors: 1.1 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.1 > 1
This may slow down DNS resolution and can cause bottlenecks. ``` ### Get-PiHoleInfoMessageCount @@ -916,7 +1001,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password ``` -Count : 0 +Count : 1 ``` ### Remove-PiHoleInfoMessage @@ -940,8 +1025,8 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : @{Timestamp=9/26/2026 5:21:18 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} -NextID : 1 +Log : {@{Timestamp=9/27/2026 12:28:02 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/27/2026 1:54:43 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:42 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:44 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:43 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:48 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:47 +0000] "GET /api/dns/blocking HTTP/1.1" 200 692 https://dns3.localdomain:8489/admin/settings/all Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36...; Priority=}…} +NextID : 14 Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -959,7 +1044,7 @@ Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -Fo FileName : pihole-backup.zip FilePath : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample\pihole-backup.zip RootFolder : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample -FileSizeKB : 24 +FileSizeKB : 25 ``` ### Get-PiHoleHistory @@ -970,31 +1055,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM +Timestamp : 9/26/2026 2:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:05:00 AM +Timestamp : 9/26/2026 2:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:15:00 AM +Timestamp : 9/26/2026 2:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:25:00 AM +Timestamp : 9/26/2026 2:55:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:35:00 AM +Timestamp : 9/26/2026 3:05:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1011,19 +1096,19 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM +Timestamp : 9/26/2026 2:25:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:05:00 AM +Timestamp : 9/26/2026 2:35:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:15:00 AM +Timestamp : 9/26/2026 2:45:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:25:00 AM +Timestamp : 9/26/2026 2:55:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:35:00 AM +Timestamp : 9/26/2026 3:05:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ @@ -1039,37 +1124,23 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 12:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +Timestamp : 9/27/2026 1:00:00 PM +Total : 61 +Cached : 13 +Blocked : 24 +Forwarded : 24 -Timestamp : 9/27/2026 1:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 9/27/2026 2:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +Timestamp : 9/27/2026 1:10:00 PM +Total : 201 +Cached : 134 +Blocked : 58 +Forwarded : 9 -Timestamp : 9/27/2026 3:00:00 AM -Total : 1 -Cached : 1 +Timestamp : 9/27/2026 2:00:00 PM +Total : 3 +Cached : 3 Blocked : 0 Forwarded : 0 - -Timestamp : 9/27/2026 4:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -_(showing 5 of 8 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1082,22 +1153,14 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 7:00:00 AM +Timestamp : 9/27/2026 2:00:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 9/27/2026 6:00:00 AM -Clients : @{ClientId=3; Count=1} +Timestamp : 9/27/2026 1:10:00 PM +Clients : @{ClientId=5; Count=1} -Timestamp : 9/27/2026 5:00:00 AM -Clients : @{ClientId=3; Count=1} - -Timestamp : 9/27/2026 4:00:00 AM -Clients : @{ClientId=3; Count=1} - -Timestamp : 9/27/2026 3:00:00 AM -Clients : @{ClientId=3; Count=1} - -_(showing 5 of 8 results)_ +Timestamp : 9/27/2026 1:00:00 PM +Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; Count=1}} ``` ## Authentication @@ -1111,13 +1174,27 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/27/2026 1:51:01 PM +LastActive : 9/27/2026 2:03:07 PM +ValidUntil : 9/27/2026 2:33:07 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0 +XForwardedFor : +App : False +Cli : False + +Id : 1 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/27/2026 7:43:32 AM -LastActive : 9/27/2026 7:43:36 AM -ValidUntil : 9/27/2026 8:13:36 AM +LoginAt : 9/27/2026 2:15:19 PM +LastActive : 9/27/2026 2:15:24 PM +ValidUntil : 9/27/2026 2:45:24 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : @@ -1130,13 +1207,13 @@ Cli : False _Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._ ```powershell -Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 0 +Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 1 ``` ``` -Id : 0 +Id : 1 Status : Removed ``` diff --git a/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..046d999 --- /dev/null +++ b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,68 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Add-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds an item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain $script:TestValue + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when the item already exists' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 index 4461c04..8ef8b5d 100644 --- a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 +++ b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 @@ -28,6 +28,15 @@ Describe 'Get-PiHoleConfig (Integration)' -Tag 'Integration' { $result.Dhcp | Should -Not -BeNullOrEmpty } + It 'filters to just one element when -Element is specified' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/upstreams' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Dns.Upstreams | Should -Not -BeNullOrEmpty + $result.Dhcp | Should -BeNullOrEmpty + } + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true $result | Format-List | Out-String | Write-Host diff --git a/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 new file mode 100644 index 0000000..2522f3b --- /dev/null +++ b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 @@ -0,0 +1,43 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleConfigProperty (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns read-only properties as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + # misc.readOnly is documented as always read-only, so it's a stable thing to assert on. + $result | Should -Not -BeNullOrEmpty + $result.Key | Should -Contain 'misc.readOnly' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..5f9c24e --- /dev/null +++ b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,76 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an existing item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Removed' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'removes the item when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here - + # the item actually being gone afterward is the real signal of success. + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -RawOutput $true -Confirm:$false + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'errors when the item does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 new file mode 100644 index 0000000..2e14527 --- /dev/null +++ b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 @@ -0,0 +1,69 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests change and restore a genuinely harmless debug flag (debug.api) rather than +# anything that affects DNS resolution or other real behavior. They require the app password +# used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same as the +# functions themselves - see their .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Set-PiHoleConfig (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'changes a setting and restores it, without restarting FTL' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Debug.Api | Should -Be (-not $original) + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.config | Should -Not -BeNullOrEmpty + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'errors and gives a clear reason when the property can only be set in pihole.toml' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ misc = @{ readOnly = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + $errOut[-1] | Should -Match 'pihole.toml' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index ad26ca4..21206c7 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -155,6 +155,48 @@ Add-Example -Category Config -FunctionName 'Get-PiHoleConfig' ` -Invocation 'Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +Add-Example -Category Config -FunctionName 'Get-PiHoleConfigProperty' ` + -Invocation 'Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +# Set-PiHoleConfig/Add-PiHoleConfigArrayItem/Remove-PiHoleConfigArrayItem all require the app +# password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings) - Pi-hole +# blocks config changes from app passwords by default. Skip these three with a placeholder if +# that isn't the case, rather than failing the whole run. +$appSudoEnabled = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Webserver.Api.AppSudo +if ($appSudoEnabled) { + $originalDebugApi = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Debug.Api + $toggledDebugApi = -not $originalDebugApi + + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation "Set-PiHoleConfig -PiHoleServer `$PiHoleServer -Password `$Password -Settings @{ debug = @{ api = `$$($toggledDebugApi.ToString().ToLower()) } }" ` + -Result (Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $toggledDebugApi } } -Restart $false) + Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $originalDebugApi } } -Restart $false | Out-Null + + $docsHostEntry = '192.0.2.1 piholeshell-docs-example.com' + Invoke-Quietly { Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false } + + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation "Add-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false) + + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation "Remove-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false) +} +else { + $appSudoNote = 'Requires "app_sudo" enabled for this app password (Settings > All Settings in your Pi-hole admin UI) - not captured this run since it is disabled here.' + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation 'Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } }' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation 'Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation 'Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped +} + Add-Example -Category Config -FunctionName 'Get-PiHolePadd' ` -Invocation 'Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) From 7caed4a25afa31e404579298b958d5dde767745e Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Mon, 28 Sep 2026 15:17:18 -0500 Subject: [PATCH 3/3] docs: regenerate README and EXAMPLES after merging develop Resolves the README.md/docs/EXAMPLES.md conflicts from merging develop (which now includes PR #122's Client Management and Network Information) by regenerating both from the merged source instead of hand-resolving generated-file diffs. Co-Authored-By: Claude Sonnet 5 --- README.md | 15 ++ docs/EXAMPLES.md | 523 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 388 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index ad4c0a5..798ebf2 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,16 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Remove-PiHoleDomain` | Remove a domain | | `Update-PiHoleDomain` | Update a domain | +### Client Management + +| Function | Description | +|---|---| +| `Get-PiHoleClient` | Get clients | +| `Get-PiHoleClientSuggestion` | Get client suggestions | +| `New-PiHoleClient` | Add a new client | +| `Remove-PiHoleClient` | Remove a client | +| `Update-PiHoleClient` | Update a client | + ### Metrics | Function | Description | @@ -170,10 +180,15 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleInfoSystem` | Get info about various system parameters | | `Get-PiHoleInfoVersion` | Get Pi-hole version | | `Get-PiHoleLogWebserver` | Get webserver log content | +| `Get-PiHoleNetworkDevice` | Get info about the devices in your local network as seen by your Pi-hole | +| `Get-PiHoleNetworkGateway` | Get info about the gateway of your Pi-hole | +| `Get-PiHoleNetworkInterface` | Get info about the interfaces of your Pi-hole | +| `Get-PiHoleNetworkRoute` | Get info about the routes of your Pi-hole | | `Get-PiHolePadd` | Get summarized data for PADD | | `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | | `Remove-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | +| `Remove-PiHoleNetworkDevice` | Delete a device from the network table | | `Set-PiHoleConfig` | Change configuration of your Pi-hole | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 9f97dcf..7bb7f63 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -100,8 +100,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:02 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Get-PiHoleGroup @@ -137,8 +137,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:02 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Update-PiHoleGroup @@ -153,8 +153,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:21 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:06:06 PM ``` ### Remove-PiHoleGroup @@ -184,8 +184,8 @@ Comment : Example list Groups : {Default} Enabled : True Id : 79 -DateAdded : 9/27/2026 2:18:57 PM -DateModified : 9/27/2026 2:18:57 PM +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:06:43 PM Type : Block DateUpdated : Number : 0 @@ -288,8 +288,8 @@ Comment : Example list Groups : {Default} Enabled : False Id : 79 -DateAdded : 9/27/2026 2:18:57 PM -DateModified : 9/27/2026 2:19:31 PM +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:07:17 PM Type : Block DateUpdated : Number : 0 @@ -415,8 +415,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:09 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Get-PiHoleDomain @@ -435,8 +435,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:09 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Update-PiHoleDomain @@ -455,8 +455,8 @@ Comment : Example domain Groups : {Default} Enabled : False Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:42 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:08:28 PM ``` ### Remove-PiHoleDomain @@ -473,6 +473,92 @@ Kind : Exact Status : Removed ``` +## Client Management + +### New-PiHoleClient + +```powershell +New-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Example client" +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Example client +Groups : {Default} +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM +``` + +### Get-PiHoleClient + +```powershell +Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Example client +Groups : {Default} +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM +``` + +### Get-PiHoleClientSuggestion + +```powershell +Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +HwAddr : ip-127.0.0.1 +MacVendor : +LastQuery : 9/28/2026 3:00:00 PM +Addresses : 127.0.0.1 +Names : localhost + +HwAddr : 74:56:3c:bb:f4:0b +MacVendor : Giga-Byte Technology Co.,Ltd. +LastQuery : 9/27/2026 5:34:26 PM +Addresses : 192.168.1.162 +Names : +``` + +### Update-PiHoleClient + +```powershell +Update-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Updated comment" +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Updated comment +Groups : {Default} +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:09:37 PM +``` + +### Remove-PiHoleClient + +```powershell +Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" +``` + +``` + +Client : 192.168.99.99 +Status : Removed +``` + ## Metrics ### Get-PiHoleStatsSummary @@ -483,18 +569,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 265 -Blocked : 82 -PercentBlocked : 30.943395614624 -UniqueDomains : 21 -Forwarded : 33 -Cached : 150 +Total : 299 +Blocked : 103 +PercentBlocked : 34.4481620788574 +UniqueDomains : 22 +Forwarded : 39 +Cached : 157 Frequency : 0 -Types : @{A=129; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=6; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=82; Forwarded=33; Cache=123; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=27; ExternalBlockedEde15=0} -Replies : @{Unknown=8; Nodata=20; Nxdomain=2; Cname=0; Ip=231; Domain=4; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=134; AAAA=133; ANY=0; SRV=0; SOA=0; PTR=32; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=103; Forwarded=39; Cache=127; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=30; ExternalBlockedEde15=0} +Replies : @{Unknown=13; Nodata=24; Nxdomain=3; Cname=0; Ip=230; Domain=29; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790530224} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790545835} ``` ### Get-PiHoleStatsRecentBlocked @@ -505,7 +591,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : adservice.google.com +Blocked : googlesyndication.com ``` ### Get-PiHoleStatsQueryType @@ -517,10 +603,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 129 +Count : 134 Type : AAAA -Count : 130 +Count : 133 Type : ANY Count : 0 @@ -542,21 +628,21 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : python.org -Count : 22 +Domain : bbc.com +Count : 23 -Domain : microsoft.com -Count : 20 +Domain : 1.0.0.127.in-addr.arpa +Count : 23 Domain : nytimes.com -Count : 18 - -Domain : example.com -Count : 18 +Count : 20 -Domain : wikipedia.org +Domain : cloudflare.com Count : 16 +Domain : github.com +Count : 15 + _(showing 5 of 10 results)_ ``` @@ -570,11 +656,11 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 259 +Count : 267 IP : 127.0.0.1 Name : localhost -Count : 6 +Count : 32 ``` ### Get-PiHoleStatsUpstream @@ -585,9 +671,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 265 -ForwardedQueries : 33 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=82; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=150; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=33; ResponseTime=0.0300731555275295; Variance=0.0024513289105294}} +TotalQueries : 299 +ForwardedQueries : 39 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=103; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=157; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=21; ResponseTime=0.0285110175609589; Variance=0.00743893922252483}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=18; ResponseTime=0.0329826772212982; Variance=0.00822325505214882}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -598,10 +684,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {python.org, microsoft.com, nytimes.com, example.com…} +Domain : {bbc.com, 1.0.0.127.in-addr.arpa, nytimes.com, cloudflare.com…} ClientIp : {192.168.1.162, 127.0.0.1} ClientName : {localhost} -Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted} +Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), 8.8.4.4#53 (dns.google)…} Type : {A, AAAA, ANY, SRV…} Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} @@ -618,10 +704,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 265 -SumBlocked : 82 -PercentBlocked : 30.943395614624 -TotalClients : 3 +SumQueries : 8 +SumBlocked : 0 +PercentBlocked : 0 +TotalClients : 1 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -635,7 +721,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 130 +Count : 0 Type : AAAA Count : 0 @@ -647,7 +733,7 @@ Type : SRV Count : 0 Type : SOA -Count : 6 +Count : 8 _(showing 5 of 16 results)_ ``` @@ -662,22 +748,8 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : python.org -Count : 22 - -Domain : microsoft.com -Count : 20 - -Domain : nytimes.com -Count : 18 - -Domain : example.com -Count : 18 - -Domain : wikipedia.org -Count : 16 - -_(showing 5 of 10 results)_ +Domain : 1.0.0.127.in-addr.arpa +Count : 8 ``` ### Get-PiHoleStatsDatabaseTopClient @@ -690,17 +762,9 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` -IP : 192.168.1.162 -Name : -Count : 177 - IP : 127.0.0.1 Name : localhost -Count : 4 - -IP : 127.0.0.1 -Name : -Count : 2 +Count : 8 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -713,9 +777,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 265 -ForwardedQueries : 33 -Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=1; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=32; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=123; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=109; ResponseTime=0; Variance=0}} +TotalQueries : 8 +ForwardedQueries : 0 +Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}} ``` ## Configuration & Diagnostics @@ -809,20 +873,20 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=127; Evicted=0} +Cache : @{Size=10000; Inserted=131; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} GravitySize : 496171 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 389 -Queries : @{Total=265; Blocked=82; PercentBlocked=30.943395614624; QueryFrequency=0} -RecentBlocked : adservice.google.com -Sensors : @{CpuTemp=37.932; HotLimit=60; Unit=C} -System : @{Uptime=653422; Memory=; Procs=80; Cpu=; Ftl=} -TopBlocked : analytics.google.com +Queries : @{Total=299; Blocked=103; PercentBlocked=34.4481620788574; QueryFrequency=0} +RecentBlocked : googlesyndication.com +Sensors : @{CpuTemp=39.008; HotLimit=60; Unit=C} +System : @{Uptime=742734; Memory=; Procs=72; Cpu=; Ftl=} +TopBlocked : googleadservices.com TopClient : 192.168.1.162 -TopDomain : python.org +TopDomain : bbc.com Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -859,11 +923,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 653433 +Uptime : 742744 Memory : @{Ram=; Swap=} -Procs : 81 -Cpu : @{NumProcessors=1; PercentCpu=96.1999969482422; Load=} -Ftl : @{PercentMemory=3.29122042655945; PercentCpu=95.3000030517578} +Procs : 72 +Cpu : @{NumProcessors=1; PercentCpu=96.9000015258789; Load=} +Ftl : @{PercentMemory=3.11284995079041; PercentCpu=96.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -879,11 +943,11 @@ PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} Pid : 389 -Uptime : 6534699.497781 -PercentMemory : 3.29122042655945 -PercentCpu : 96.1999969482422 +Uptime : 80190954.666427 +PercentMemory : 3.11284995079041 +PercentCpu : 96.3000030517578 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=127; DnsCacheLiveFreed=0; DnsQueriesForwarded=60; DnsAuthAnswered=0; DnsLocalAnswered=205; DnsStaleAnswered=27; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} +Dnsmasq : @{DnsCacheInserted=131; DnsCacheLiveFreed=0; DnsQueriesForwarded=69; DnsAuthAnswered=0; DnsLocalAnswered=230; DnsStaleAnswered=30; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} ``` ### Get-PiHoleInfoSensors @@ -895,7 +959,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password ``` List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}} -CpuTemp : 37.932 +CpuTemp : 40.084 HotLimit : 60 Unit : C ``` @@ -912,12 +976,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/27/2026 12:28:00 PM -ChangeTime : 9/27/2026 12:28:00 PM +ModifiedTime : 9/28/2026 2:23:00 PM +ChangeTime : 9/28/2026 2:23:00 PM Owner : @{User=; Group=} -Queries : 265 -EarliestTimestamp : 9/26/2026 2:10:00 PM -QueriesDisk : 265 +Queries : 299 +EarliestTimestamp : 9/27/2026 3:00:00 PM +QueriesDisk : 299 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -987,7 +1051,7 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` Id : 1 -Timestamp : 9/27/2026 1:13:02 PM +Timestamp : 9/27/2026 5:33:13 PM Type : LOAD Plain : Long-term load (15min avg) larger than number of processors: 1.1 > 1 Html : Long-term load (15min avg) larger than number of processors: 1.1 > 1
This may slow down DNS resolution and can cause bottlenecks. @@ -1025,8 +1089,8 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=9/27/2026 12:28:02 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/27/2026 1:54:43 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:42 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:44 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:43 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:48 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:47 +0000] "GET /api/dns/blocking HTTP/1.1" 200 692 https://dns3.localdomain:8489/admin/settings/all Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36...; Priority=}…} -NextID : 14 +Log : {@{Timestamp=9/27/2026 4:48:13 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/28/2026 3:04:02 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:01 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:03 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:07 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}} +NextID : 4 Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -1055,31 +1119,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 2:25:00 PM +Timestamp : 9/27/2026 3:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:35:00 PM +Timestamp : 9/27/2026 3:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:45:00 PM +Timestamp : 9/27/2026 3:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:55:00 PM +Timestamp : 9/27/2026 3:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 3:05:00 PM +Timestamp : 9/27/2026 3:55:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1096,19 +1160,19 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 2:25:00 PM +Timestamp : 9/27/2026 3:15:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:35:00 PM +Timestamp : 9/27/2026 3:25:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:45:00 PM +Timestamp : 9/27/2026 3:35:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:55:00 PM +Timestamp : 9/27/2026 3:45:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 3:05:00 PM +Timestamp : 9/27/2026 3:55:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ @@ -1124,23 +1188,37 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 1:00:00 PM -Total : 61 -Cached : 13 -Blocked : 24 -Forwarded : 24 +Timestamp : 9/28/2026 8:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 -Timestamp : 9/27/2026 1:10:00 PM -Total : 201 -Cached : 134 -Blocked : 58 -Forwarded : 9 +Timestamp : 9/28/2026 9:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 10:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 -Timestamp : 9/27/2026 2:00:00 PM -Total : 3 -Cached : 3 +Timestamp : 9/28/2026 11:00:00 AM +Total : 1 +Cached : 1 Blocked : 0 Forwarded : 0 + +Timestamp : 9/28/2026 12:00:00 PM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +_(showing 5 of 8 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1153,14 +1231,173 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 2:00:00 PM +Timestamp : 9/28/2026 3:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 2:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 1:00:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 9/27/2026 1:10:00 PM -Clients : @{ClientId=5; Count=1} +Timestamp : 9/28/2026 12:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 11:00:00 AM +Clients : @{ClientId=3; Count=1} + +_(showing 5 of 8 results)_ +``` + +### Get-PiHoleNetworkGateway + +```powershell +Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Family : inet +Interface : wlan0 +Address : 192.168.1.1 +Local : 192.168.1.248 +``` + +### Get-PiHoleNetworkRoute + +```powershell +Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Table : 254 +Family : inet +Protocol : boot +Scope : universe +Type : unicast +Flags : +Gateway : 192.168.1.1 +Oif : wlan0 +Dst : default + +Table : 254 +Family : inet +Protocol : kernel +Scope : link +Type : unicast +Flags : +Dst : 192.168.1.0 +Prefsrc : 192.168.1.248 +Oif : wlan0 + +Table : 255 +Family : inet +Protocol : kernel +Scope : host +Type : local +Flags : +Dst : 127.0.0.0 +Prefsrc : 127.0.0.1 +Oif : lo + +Table : 255 +Family : inet +Protocol : kernel +Scope : host +Type : local +Flags : +Dst : 127.0.0.1 +Prefsrc : 127.0.0.1 +Oif : lo + +Table : 255 +Family : inet +Protocol : kernel +Scope : link +Type : broadcast +Flags : +Dst : 127.255.255.255 +Prefsrc : 127.0.0.1 +Oif : lo + +_(showing 5 of 11 results)_ +``` + +### Get-PiHoleNetworkInterface + +```powershell +Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $Password +``` -Timestamp : 9/27/2026 1:00:00 PM -Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; Count=1}} +``` + +Name : lo +Speed : +Type : loopback +Flags : {up, loopback, running, lower_up} +State : unknown +Carrier : True +ProtoDown : False +Address : 00:00:00:00:00:00 +Broadcast : 00:00:00:00:00:00 +Stats : @{RxBytes=; TxBytes=; Bits=64} +Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1789883140.02; Tstamp=1789883140.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883140.02; Tstamp=1789883140.02}} + +Name : wlan0 +Speed : +Type : ether +Flags : {up, broadcast, running, multicast…} +State : up +Carrier : True +ProtoDown : False +Address : b8:27:eb:11:a4:d5 +Broadcast : ff:ff:ff:ff:ff:ff +PermAddress : b8:27:eb:11:a4:d5 +Stats : @{RxBytes=; TxBytes=; Bits=64} +Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=65600; Valid=65600; Cstamp=1789883160.32; Tstamp=1790175981.82}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883159.81; Tstamp=1789883159.81}} +``` + +### Get-PiHoleNetworkDevice + +_Shows up to 10 devices by default; pass -MaxDevices for more._ + +```powershell +Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Id : 2 +HwAddr : ip-127.0.0.1 +Interface : lo +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/28/2026 3:00:00 PM +NumQueries : 32 +MacVendor : +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=9/28/2026 3:11:00 PM} + +Id : 1 +HwAddr : 74:56:3c:bb:f4:0b +Interface : wlan0 +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/27/2026 5:34:26 PM +NumQueries : 267 +MacVendor : Giga-Byte Technology Co.,Ltd. +Ips : @{Ip=192.168.1.162; Name=; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=12/31/1969 6:00:00 PM} +``` + +### Remove-PiHoleNetworkDevice + +_Network devices arise from Pi-hole having genuinely seen a device and cannot be manufactured on demand, so this example shows the error for a device ID that does not exist rather than a fabricated success._ + +```powershell +Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password -DeviceId 5 +``` + +``` + +Error : Response status code does not indicate success: 404 (Not Found). ``` ## Authentication @@ -1174,27 +1411,13 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 -CurrentSession : False -Valid : True -TlsLogin : True -TlsMixed : False -LoginAt : 9/27/2026 1:51:01 PM -LastActive : 9/27/2026 2:03:07 PM -ValidUntil : 9/27/2026 2:33:07 PM -RemoteAddress : 192.168.1.162 -UserAgent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0 -XForwardedFor : -App : False -Cli : False - -Id : 1 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/27/2026 2:15:19 PM -LastActive : 9/27/2026 2:15:24 PM -ValidUntil : 9/27/2026 2:45:24 PM +LoginAt : 9/28/2026 3:03:07 PM +LastActive : 9/28/2026 3:03:11 PM +ValidUntil : 9/28/2026 3:33:11 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : @@ -1207,13 +1430,13 @@ Cli : False _Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._ ```powershell -Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 1 +Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 0 ``` ``` -Id : 1 +Id : 0 Status : Removed ```