From 9d1b1efbc8f55a1dedd1dc2a122fc488ad0c0f64 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Wed, 30 Sep 2026 09:18:40 -0500 Subject: [PATCH] feat: add DNS/FTL log endpoints and group batch-delete Adds Get-PiHoleLogDnsmasq and Get-PiHoleLogFtl (GET /logs/dnsmasq, GET /logs/ftl), completing the Logs API area. Reworks Remove-PiHoleGroup to delete multiple groups in a single POST /groups:batchDelete call while still validating every name exists first, since the batch API silently no-ops for names that don't exist. Fixes a latent bug in the batch body construction: a single-name foreach result unwraps to a bare hashtable instead of a one-element array, which ConvertTo-Json then serializes as a JSON object instead of an array - causing the API to reject single-group deletes with 400. Building the body with @() and passing it via ConvertTo-Json -InputObject (instead of piping) fixes both the single- and multi-name cases. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 2 +- .../FTLInformation/Get-PiHoleLogDnsmasq.ps1 | 91 ++++ .../FTLInformation/Get-PiHoleLogFtl.ps1 | 91 ++++ .../GroupManagement/Remove-PiHoleGroup.ps1 | 82 ++-- README.md | 4 +- docs/EXAMPLES.md | 422 ++++++++++-------- ...Get-PiHoleLogDnsmasq.Integration.Tests.ps1 | 55 +++ .../Get-PiHoleLogFtl.Integration.Tests.ps1 | 55 +++ .../Remove-PiHoleGroup.Integration.Tests.ps1 | 38 +- tools/Update-ExampleOutput.ps1 | 8 + 10 files changed, 627 insertions(+), 221 deletions(-) create mode 100644 PiHoleShell/Public/FTLInformation/Get-PiHoleLogDnsmasq.ps1 create mode 100644 PiHoleShell/Public/FTLInformation/Get-PiHoleLogFtl.ps1 create mode 100644 tests/FTLInformation/Get-PiHoleLogDnsmasq.Integration.Tests.ps1 create mode 100644 tests/FTLInformation/Get-PiHoleLogFtl.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index efe4904..6482163 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -34,7 +34,7 @@ Export-ModuleMember -Function @( 'Get-PiHoleList', 'Search-PiHoleListDomain', 'Add-PiHoleList', 'Remove-PiHoleList', 'Update-PiHoleList', ` #FTLInformation 'Get-PiHoleInfoMessage', 'Get-PiHoleInfoHost', 'Get-PiHoleInfoClient', 'Get-PiHoleInfoLogin', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', ` - 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', 'Get-PiHoleLogWebserver', ` + 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', 'Get-PiHoleLogWebserver', 'Get-PiHoleLogDnsmasq', 'Get-PiHoleLogFtl', ` #History 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient', ` #Teleporter diff --git a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogDnsmasq.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogDnsmasq.ps1 new file mode 100644 index 0000000..274fec4 --- /dev/null +++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogDnsmasq.ps1 @@ -0,0 +1,91 @@ +function Get-PiHoleLogDnsmasq { + <# +.SYNOPSIS +Get DNS log content + +.DESCRIPTION +Request content from the log of the embedded DNS resolver dnsmasq. Every response includes a +NextID; pass it back as -NextID on your next call to only get lines added since then, making +periodic polling for new log lines easy without checking for duplicates. + +.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 NextID +Only return log lines added after this ID (returned as NextID on a previous call). Omit to +get the full available log + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleLogDnsmasq -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/logs/dnsmasq')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[int]]$NextID, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/logs/dnsmasq" + if ($PSBoundParameters.ContainsKey('NextID')) { + $Uri += "?nextID=$NextID" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + $Log = foreach ($Item in $Response.log) { + [PSCustomObject]@{ + Timestamp = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.timestamp).LocalTime + Message = $Item.message + Priority = $Item.prio + } + } + + $Object = [PSCustomObject]@{ + Log = $Log + NextID = $Response.nextID + Pid = $Response.pid + File = $Response.file + } + 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/FTLInformation/Get-PiHoleLogFtl.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogFtl.ps1 new file mode 100644 index 0000000..9f5dbcb --- /dev/null +++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogFtl.ps1 @@ -0,0 +1,91 @@ +function Get-PiHoleLogFtl { + <# +.SYNOPSIS +Get FTL log content + +.DESCRIPTION +Request content from FTL's own log. Every response includes a NextID; pass it back as -NextID +on your next call to only get lines added since then, making periodic polling for new log +lines easy without checking for duplicates. + +.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 NextID +Only return log lines added after this ID (returned as NextID on a previous call). Omit to +get the full available log + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleLogFtl -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/logs/ftl')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Nullable[int]]$NextID, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/logs/ftl" + if ($PSBoundParameters.ContainsKey('NextID')) { + $Uri += "?nextID=$NextID" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + $Log = foreach ($Item in $Response.log) { + [PSCustomObject]@{ + Timestamp = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.timestamp).LocalTime + Message = $Item.message + Priority = $Item.prio + } + } + + $Object = [PSCustomObject]@{ + Log = $Log + NextID = $Response.nextID + Pid = $Response.pid + File = $Response.file + } + 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/GroupManagement/Remove-PiHoleGroup.ps1 b/PiHoleShell/Public/GroupManagement/Remove-PiHoleGroup.ps1 index 6f85970..fdac7a1 100644 --- a/PiHoleShell/Public/GroupManagement/Remove-PiHoleGroup.ps1 +++ b/PiHoleShell/Public/GroupManagement/Remove-PiHoleGroup.ps1 @@ -1,10 +1,11 @@ function Remove-PiHoleGroup { <# .SYNOPSIS -Delete a group +Delete one or more groups .DESCRIPTION -Deletes a group from Pi-hole. Any lists or clients assigned to it are unassigned, not deleted. +Deletes one or more groups from Pi-hole in a single batch call. Any lists or clients assigned +to a deleted group are unassigned, not deleted. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -13,7 +14,7 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER GroupName -The name of the group to delete +The name(s) of the group(s) to delete .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -23,9 +24,11 @@ This will dump the response instead of the formatted object .EXAMPLE Remove-PiHoleGroup -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -GroupName "Kids" + +.EXAMPLE +Remove-PiHoleGroup -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -GroupName "Kids", "Guests" #> - [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/groups/-name-')] - [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/groups-batchDelete')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( [Parameter(Mandatory = $true)] @@ -33,39 +36,60 @@ Remove-PiHoleGroup -PiHoleServer "http://pihole.domain.com:8080" -Password "your [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [string]$GroupName, + [string[]]$GroupName, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false - ) try { - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $Target = "Pi-Hole group(s) $($GroupName -join ', ')" + if ($PSCmdlet.ShouldProcess($Target, "Remove group(s)")) { + # The batch delete API silently succeeds even for a group name that doesn't exist, + # rather than reporting it as an error - check every name exists first so this + # function never falsely reports a nonexistent group as "Deleted". + $AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + foreach ($Name in $GroupName) { + if (-not ($AllGroups | Where-Object { $_.Name -eq $Name })) { + throw "Cannot find $Name on $PiHoleServer! Please use Get-PiHoleGroup to list all groups" + } + } - $GetGroupName = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -GroupName $GroupName + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - if (-not $GetGroupName) { - throw "Cannot find $GroupName on $PiHoleServer! Please use Get-PiHoleGroup to list all groups" - } + # @() forces this to stay an array even for a single name - without it, PowerShell + # unwraps a one-item foreach result to a bare hashtable, which ConvertTo-Json then + # serializes as a JSON object instead of an array, and the API rejects it with 400. + $Body = @(foreach ($Name in $GroupName) { + @{ item = $Name } + }) - $Params = @{ - Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/groups/$GroupName" - Method = "Delete" - SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" - } + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/groups:batchDelete" + Method = "Post" + SkipCertificateCheck = $IgnoreSsl + # -InputObject (not piped) so ConvertTo-Json serializes the array as-is instead of + # unwrapping it into individual pipeline objects first. + Body = ConvertTo-Json -InputObject $Body -Depth 10 -Compress + ContentType = "application/json" + } - $Response = Invoke-RestMethod @Params + $Response = Invoke-RestMethod @Params - if ($RawOutput) { - Write-Output $Response - } - else { - $Object = [PSCustomObject]@{ - Name = $GroupName - Status = "Deleted" + 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. + $ObjectFinal = foreach ($Name in $GroupName) { + [PSCustomObject]@{ + Name = $Name + Status = "Deleted" + } + } + Write-Output $ObjectFinal } - Write-Output $Object } } @@ -78,4 +102,4 @@ Remove-PiHoleGroup -PiHoleServer "http://pihole.domain.com:8080" -Password "your Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index e8e9732..1a29f29 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu |---|---| | `Get-PiHoleGroup` | Get groups | | `New-PiHoleGroup` | Creates a new group | -| `Remove-PiHoleGroup` | Delete a group | +| `Remove-PiHoleGroup` | Delete one or more groups | | `Update-PiHoleGroup` | Update a group | ### List Management @@ -180,6 +180,8 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleInfoSensors` | Get info about various sensors | | `Get-PiHoleInfoSystem` | Get info about various system parameters | | `Get-PiHoleInfoVersion` | Get Pi-hole version | +| `Get-PiHoleLogDnsmasq` | Get DNS log content | +| `Get-PiHoleLogFtl` | Get FTL log content | | `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 | diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 03ed530..95fd82e 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 : 17 -DateAdded : 9/29/2026 9:44:50 AM -DateModified : 9/29/2026 9:44:50 AM +Id : 53 +DateAdded : 9/30/2026 9:12:05 AM +DateModified : 9/30/2026 9:12:05 AM ``` ### Get-PiHoleGroup @@ -133,12 +133,19 @@ Id : 3 DateAdded : 7/1/2025 10:13:40 PM DateModified : 7/1/2025 10:13:40 PM +Name : RealFunctionTest +Comment : +Enabled : True +Id : 44 +DateAdded : 9/30/2026 8:48:26 AM +DateModified : 9/30/2026 8:48:26 AM + Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 17 -DateAdded : 9/29/2026 9:44:50 AM -DateModified : 9/29/2026 9:44:50 AM +Id : 53 +DateAdded : 9/30/2026 9:12:05 AM +DateModified : 9/30/2026 9:12:05 AM ``` ### Update-PiHoleGroup @@ -152,9 +159,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False -Id : 17 -DateAdded : 9/29/2026 9:44:50 AM -DateModified : 9/29/2026 9:45:09 AM +Id : 53 +DateAdded : 9/30/2026 9:12:05 AM +DateModified : 9/30/2026 9:12:24 AM ``` ### Remove-PiHoleGroup @@ -183,9 +190,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : True -Id : 79 -DateAdded : 9/29/2026 9:45:45 AM -DateModified : 9/29/2026 9:45:45 AM +Id : 80 +DateAdded : 9/30/2026 9:13:02 AM +DateModified : 9/30/2026 9:13:02 AM Type : Block DateUpdated : Number : 0 @@ -287,9 +294,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : False -Id : 79 -DateAdded : 9/29/2026 9:45:45 AM -DateModified : 9/29/2026 9:46:19 AM +Id : 80 +DateAdded : 9/30/2026 9:13:02 AM +DateModified : 9/30/2026 9:13:36 AM Type : Block DateUpdated : Number : 0 @@ -414,9 +421,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : True -Id : 16 -DateAdded : 9/29/2026 9:46:56 AM -DateModified : 9/29/2026 9:46:56 AM +Id : 18 +DateAdded : 9/30/2026 9:14:17 AM +DateModified : 9/30/2026 9:14:17 AM ``` ### Get-PiHoleDomain @@ -434,9 +441,9 @@ Kind : exact Comment : Example domain Groups : {Default} Enabled : True -Id : 16 -DateAdded : 9/29/2026 9:46:56 AM -DateModified : 9/29/2026 9:46:56 AM +Id : 18 +DateAdded : 9/30/2026 9:14:17 AM +DateModified : 9/30/2026 9:14:17 AM ``` ### Update-PiHoleDomain @@ -454,9 +461,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : False -Id : 16 -DateAdded : 9/29/2026 9:46:56 AM -DateModified : 9/29/2026 9:47:30 AM +Id : 18 +DateAdded : 9/30/2026 9:14:17 AM +DateModified : 9/30/2026 9:14:50 AM ``` ### Remove-PiHoleDomain @@ -487,9 +494,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 18 -DateAdded : 9/29/2026 9:48:01 AM -DateModified : 9/29/2026 9:48:01 AM +Id : 20 +DateAdded : 9/30/2026 9:15:21 AM +DateModified : 9/30/2026 9:15:21 AM ``` ### Get-PiHoleClient @@ -504,9 +511,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 18 -DateAdded : 9/29/2026 9:48:01 AM -DateModified : 9/29/2026 9:48:01 AM +Id : 20 +DateAdded : 9/30/2026 9:15:21 AM +DateModified : 9/30/2026 9:15:21 AM ``` ### Get-PiHoleClientSuggestion @@ -517,17 +524,17 @@ Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password ``` -HwAddr : 74:56:3c:bb:f4:0b -MacVendor : Giga-Byte Technology Co.,Ltd. -LastQuery : 9/29/2026 9:37:20 AM -Addresses : 192.168.1.162 -Names : - HwAddr : ip-127.0.0.1 MacVendor : -LastQuery : 9/29/2026 9:14:51 AM +LastQuery : 9/30/2026 9:00:01 AM Addresses : 127.0.0.1 Names : localhost + +HwAddr : 74:56:3c:bb:f4:0b +MacVendor : Giga-Byte Technology Co.,Ltd. +LastQuery : 9/30/2026 7:49:13 AM +Addresses : 192.168.1.162 +Names : ``` ### Update-PiHoleClient @@ -542,9 +549,9 @@ Client : 192.168.99.99 Name : Comment : Updated comment Groups : {Default} -Id : 18 -DateAdded : 9/29/2026 9:48:01 AM -DateModified : 9/29/2026 9:48:39 AM +Id : 20 +DateAdded : 9/30/2026 9:15:21 AM +DateModified : 9/30/2026 9:16:00 AM ``` ### Remove-PiHoleClient @@ -569,18 +576,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 283 -Blocked : 74 -PercentBlocked : 26.1484107971191 -UniqueDomains : 21 -Forwarded : 36 -Cached : 173 +Total : 287 +Blocked : 93 +PercentBlocked : 32.404182434082 +UniqueDomains : 22 +Forwarded : 33 +Cached : 161 Frequency : 0 -Types : @{A=139; AAAA=141; ANY=0; SRV=0; SOA=0; PTR=3; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=74; Forwarded=36; Cache=126; 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=47; ExternalBlockedEde15=0} -Replies : @{Unknown=11; Nodata=24; Nxdomain=1; Cname=0; Ip=245; Domain=2; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=136; AAAA=139; ANY=0; SRV=0; SOA=0; PTR=12; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=93; Forwarded=33; Cache=122; 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=39; ExternalBlockedEde15=0} +Replies : @{Unknown=7; Nodata=17; Nxdomain=3; Cname=0; Ip=251; Domain=9; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=494009; LastUpdate=1790689320} +Gravity : @{DomainsBeingBlocked=494568; LastUpdate=1790769668} ``` ### Get-PiHoleStatsRecentBlocked @@ -603,10 +610,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 139 +Count : 136 Type : AAAA -Count : 141 +Count : 139 Type : ANY Count : 0 @@ -629,19 +636,19 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` Domain : bbc.com -Count : 24 - -Domain : cloudflare.com Count : 22 -Domain : nytimes.com +Domain : microsoft.com Count : 22 Domain : example.com -Count : 18 +Count : 20 -Domain : reddit.com -Count : 18 +Domain : apple.com +Count : 20 + +Domain : cloudflare.com +Count : 16 _(showing 5 of 10 results)_ ``` @@ -656,11 +663,11 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 280 +Count : 275 IP : 127.0.0.1 Name : localhost -Count : 3 +Count : 12 ``` ### Get-PiHoleStatsUpstream @@ -671,9 +678,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 283 -ForwardedQueries : 36 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=74; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=173; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=36; ResponseTime=0.0256568037945291; Variance=0.00185644959596056}} +TotalQueries : 287 +ForwardedQueries : 33 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=93; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=161; ResponseTime=0; Variance=0}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=18; ResponseTime=0.0253777056932449; Variance=0.00323175294284653}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=15; ResponseTime=0.0273200273513794; Variance=0.00394722979890426}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -684,10 +691,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {bbc.com, cloudflare.com, nytimes.com, example.com…} +Domain : {bbc.com, microsoft.com, example.com, apple.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.4.4#53 (dns.google), 8.8.8.8#53 (dns.google)…} Type : {A, AAAA, ANY, SRV…} Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} @@ -704,9 +711,9 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 283 -SumBlocked : 74 -PercentBlocked : 26.1484107971191 +SumQueries : 287 +SumBlocked : 93 +PercentBlocked : 32.404182434082 TotalClients : 3 ``` @@ -721,7 +728,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 141 +Count : 139 Type : AAAA Count : 0 @@ -733,7 +740,7 @@ Type : SRV Count : 0 Type : SOA -Count : 3 +Count : 12 _(showing 5 of 16 results)_ ``` @@ -749,19 +756,19 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` Domain : bbc.com -Count : 24 - -Domain : nytimes.com Count : 22 -Domain : cloudflare.com +Domain : microsoft.com Count : 22 Domain : example.com -Count : 18 +Count : 20 -Domain : reddit.com -Count : 18 +Domain : apple.com +Count : 20 + +Domain : cloudflare.com +Count : 16 _(showing 5 of 10 results)_ ``` @@ -778,15 +785,15 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 206 +Count : 182 IP : 127.0.0.1 -Name : -Count : 2 +Name : localhost +Count : 9 IP : 127.0.0.1 -Name : localhost -Count : 1 +Name : +Count : 3 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -799,9 +806,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 283 -ForwardedQueries : 36 -Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=1; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=35; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=126; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=121; ResponseTime=0; Variance=0}} +TotalQueries : 287 +ForwardedQueries : 33 +Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=22; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=11; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=122; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=132; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleQuery @@ -814,67 +821,67 @@ Get-PiHoleQuery -PiHoleServer $PiHoleServer -Password $Password -Length 5 ``` -Id : 282 -Time : 9/29/2026 9:37:20 AM -Type : AAAA -Domain : apple.com +Id : 286 +Time : 9/30/2026 9:00:01 AM +Type : PTR +Domain : 4.4.8.8.in-addr.arpa Cname : -Status : CACHE_STALE -Client : @{Ip=192.168.1.162; Name=} +Status : CACHE +Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=IP; Time=9.89437103271484E-05} +Reply : @{Type=DOMAIN; Time=0.0011289119720459} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 281 -Time : 9/29/2026 9:37:20 AM -Type : A -Domain : apple.com +Id : 285 +Time : 9/30/2026 9:00:01 AM +Type : PTR +Domain : 8.8.8.8.in-addr.arpa Cname : Status : CACHE -Client : @{Ip=192.168.1.162; Name=} +Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=IP; Time=0.000111103057861328} +Reply : @{Type=DOMAIN; Time=0.00119900703430176} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 280 -Time : 9/29/2026 9:37:20 AM -Type : AAAA -Domain : bbc.com +Id : 284 +Time : 9/30/2026 9:00:01 AM +Type : PTR +Domain : 1.0.0.127.in-addr.arpa Cname : -Status : CACHE_STALE -Client : @{Ip=192.168.1.162; Name=} +Status : CACHE +Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=IP; Time=0.000116109848022461} +Reply : @{Type=DOMAIN; Time=0.0010991096496582} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 279 -Time : 9/29/2026 9:37:20 AM -Type : A -Domain : bbc.com +Id : 283 +Time : 9/30/2026 9:00:01 AM +Type : PTR +Domain : 162.1.168.192.in-addr.arpa Cname : -Status : CACHE_STALE -Client : @{Ip=192.168.1.162; Name=} +Status : CACHE +Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=IP; Time=0.000133991241455078} +Reply : @{Type=NXDOMAIN; Time=0.0204389095306396} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 278 -Time : 9/29/2026 9:37:20 AM -Type : AAAA -Domain : nytimes.com +Id : 282 +Time : 9/30/2026 8:00:00 AM +Type : PTR +Domain : 4.4.8.8.in-addr.arpa Cname : -Status : CACHE_STALE -Client : @{Ip=192.168.1.162; Name=} +Status : CACHE +Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=NODATA; Time=0.000113964080810547} +Reply : @{Type=DOMAIN; Time=0.00113701820373535} ListId : Upstream : Ede : @{Code=-1; Text=} @@ -971,17 +978,17 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=160; Evicted=0} +Cache : @{Size=10000; Inserted=157; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} -GravitySize : 494009 +GravitySize : 494568 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 388 -Queries : @{Total=283; Blocked=74; PercentBlocked=26.1484107971191; QueryFrequency=0} +Queries : @{Total=287; Blocked=93; PercentBlocked=32.404182434082; QueryFrequency=0} RecentBlocked : googlesyndication.com -Sensors : @{CpuTemp=37.394; HotLimit=60; Unit=C} -System : @{Uptime=43352; Memory=; Procs=72; Cpu=; Ftl=} +Sensors : @{CpuTemp=40.622; HotLimit=60; Unit=C} +System : @{Uptime=127777; Memory=; Procs=73; Cpu=; Ftl=} TopBlocked : ads.pubmatic.com TopClient : 192.168.1.162 TopDomain : bbc.com @@ -1021,11 +1028,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 43362 +Uptime : 127787 Memory : @{Ram=; Swap=} -Procs : 72 -Cpu : @{NumProcessors=1; PercentCpu=94.9000015258789; Load=} -Ftl : @{PercentMemory=3.0199658870697; PercentCpu=94.3000030517578} +Procs : 73 +Cpu : @{NumProcessors=1; PercentCpu=95.8000030517578; Load=} +Ftl : @{PercentMemory=2.42485022544861; PercentCpu=95.5} ``` ### Get-PiHoleInfoFtl @@ -1036,16 +1043,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Database : @{Gravity=494009; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} +Database : @{Gravity=494568; Antigravity=0; Groups=4; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} Pid : 388 -Uptime : 3854351.673993 -PercentMemory : 3.0199658870697 -PercentCpu : 96.0999984741211 +Uptime : 7963466.771567 +PercentMemory : 2.42485022544861 +PercentCpu : 96.5999984741211 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=160; DnsCacheLiveFreed=0; DnsQueriesForwarded=83; DnsAuthAnswered=0; DnsLocalAnswered=200; DnsStaleAnswered=47; 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=157; DnsCacheLiveFreed=0; DnsQueriesForwarded=72; DnsAuthAnswered=0; DnsLocalAnswered=215; DnsStaleAnswered=39; 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 @@ -1057,7 +1064,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.622 HotLimit : 60 Unit : C ``` @@ -1074,12 +1081,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/29/2026 8:39:30 AM -ChangeTime : 9/29/2026 8:39:30 AM +ModifiedTime : 9/30/2026 6:58:06 AM +ChangeTime : 9/30/2026 6:58:06 AM Owner : @{User=; Group=} -Queries : 283 -EarliestTimestamp : 9/28/2026 9:40:00 AM -QueriesDisk : 283 +Queries : 287 +EarliestTimestamp : 9/29/2026 9:10:00 AM +QueriesDisk : 287 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -1149,7 +1156,7 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` Id : 1 -Timestamp : 9/29/2026 9:29:31 AM +Timestamp : 9/30/2026 7:38:07 AM Type : LOAD Plain : Long-term load (15min avg) larger than number of processors: 1.0 > 1 Html : Long-term load (15min avg) larger than number of processors: 1.0 > 1
This may slow down DNS resolution and can cause bottlenecks. @@ -1187,12 +1194,40 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=9/29/2026 8:39:32 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/29/2026 8:56:23 AM; Message=ACCESS: 192.168.1.162 - - [29/Sep/2026:13:56:22 +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/29/2026 8:56:23 AM; Message=ACCESS: 192.168.1.162 - - [29/Sep/2026:13:56:22 +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/29/2026 8:56:27 AM; Message=ACCESS: 192.168.1.162 - - [29/Sep/2026:13:56:22 +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 : 10 +Log : {@{Timestamp=9/30/2026 6:58:08 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/30/2026 7:16:07 AM; Message=ACCESS: 192.168.1.162 - - [30/Sep/2026:12:16:06 +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/30/2026 7:16:08 AM; Message=ACCESS: 192.168.1.162 - - [30/Sep/2026:12:16:07 +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/30/2026 7:16:12 AM; Message=ACCESS: 192.168.1.162 - - [30/Sep/2026:12:16:07 +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 : 13 Pid : 388 File : /var/log/pihole/webserver.log ``` +### Get-PiHoleLogDnsmasq + +```powershell +Get-PiHoleLogDnsmasq -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Log : {@{Timestamp=9/30/2026 6:58:08 AM; Message=started, version pi-hole-v2.93 cachesize 10000; Priority=}, @{Timestamp=9/30/2026 6:58:08 AM; Message=DNS service limited to local subnets; Priority=}, @{Timestamp=9/30/2026 6:58:08 AM; Message=compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n IDN2 DHCP DHCPv6 Lua TFTP no-conntrack ipset no-nftset auth DNSSEC loop-detect inotify dumpfile; Priority=}, @{Timestamp=9/30/2026 6:58:08 AM; Message=using nameserver 8.8.8.8#53; Priority=}…} +NextID : 57 +Pid : 388 +File : /var/log/pihole/pihole.log +``` + +### Get-PiHoleLogFtl + +```powershell +Get-PiHoleLogFtl -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Log : {@{Timestamp=9/30/2026 6:58:08 AM; Message= -> Unknown DNS queries: 0; Priority=INFO}, @{Timestamp=9/30/2026 6:58:08 AM; Message= -> Unique domains: 0; Priority=INFO}, @{Timestamp=9/30/2026 6:58:08 AM; Message= -> Unique clients: 0; Priority=INFO}, @{Timestamp=9/30/2026 6:58:08 AM; Message= -> DNS cache records: 0; Priority=INFO}…} +NextID : 535 +Pid : 388 +File : /var/log/pihole/FTL.log +``` + ### Get-PiHoleTeleporterDownload _FolderPath/FileName are yours to choose; the captured output below used a scratch temp folder for this run instead of C:\Backups._ @@ -1217,31 +1252,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/28/2026 9:55:00 AM +Timestamp : 9/29/2026 9:15:00 AM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/28/2026 10:05:00 AM +Timestamp : 9/29/2026 9:25:00 AM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/28/2026 10:15:00 AM +Timestamp : 9/29/2026 9:35:00 AM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/28/2026 10:25:00 AM +Timestamp : 9/29/2026 9:45:00 AM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/28/2026 10:35:00 AM +Timestamp : 9/29/2026 9:55:00 AM Total : 0 Cached : 0 Blocked : 0 @@ -1258,19 +1293,19 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/28/2026 9:55:00 AM +Timestamp : 9/29/2026 9:15:00 AM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/28/2026 10:05:00 AM +Timestamp : 9/29/2026 9:25:00 AM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/28/2026 10:15:00 AM +Timestamp : 9/29/2026 9:35:00 AM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/28/2026 10:25:00 AM +Timestamp : 9/29/2026 9:45:00 AM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/28/2026 10:35:00 AM +Timestamp : 9/29/2026 9:55:00 AM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ @@ -1286,22 +1321,28 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/29/2026 9:10:00 AM -Total : 50 +Timestamp : 9/30/2026 7:30:00 AM +Total : 47 Cached : 12 -Blocked : 8 -Forwarded : 30 - -Timestamp : 9/29/2026 9:20:00 AM -Total : 215 -Cached : 151 -Blocked : 58 -Forwarded : 6 - -Timestamp : 9/29/2026 9:30:00 AM -Total : 18 -Cached : 10 -Blocked : 8 +Blocked : 9 +Forwarded : 26 + +Timestamp : 9/30/2026 7:40:00 AM +Total : 232 +Cached : 141 +Blocked : 84 +Forwarded : 7 + +Timestamp : 9/30/2026 8:00:00 AM +Total : 4 +Cached : 4 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/30/2026 9:00:00 AM +Total : 4 +Cached : 4 +Blocked : 0 Forwarded : 0 ``` @@ -1315,13 +1356,16 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/29/2026 9:30:00 AM -Clients : @{ClientId=5; Count=1} +Timestamp : 9/30/2026 9:00:00 AM +Clients : @{ClientId=3; Count=1} -Timestamp : 9/29/2026 9:20:00 AM +Timestamp : 9/30/2026 8:00:00 AM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/30/2026 7:40:00 AM Clients : @{ClientId=5; Count=1} -Timestamp : 9/29/2026 9:10:00 AM +Timestamp : 9/30/2026 7:30:00 AM Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; Count=1}} ``` @@ -1431,7 +1475,7 @@ 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=79695; Valid=79695; Cstamp=1790649687.41; Tstamp=1790686720.37}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1790649682.83; Tstamp=1790649682.83}} +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=71284; Valid=71284; Cstamp=1790649687.41; Tstamp=1790762750.49}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1790649682.83; Tstamp=1790649682.83}} ``` ### Get-PiHoleNetworkDevice @@ -1444,23 +1488,23 @@ Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password ``` -Id : 1 -HwAddr : 74:56:3c:bb:f4:0b -Interface : wlan0 -FirstSeen : 9/29/2026 9:14:49 AM -LastQuery : 9/29/2026 9:37:20 AM -NumQueries : 280 -MacVendor : Giga-Byte Technology Co.,Ltd. -Ips : @{Ip=192.168.1.162; Name=; LastSeen=9/29/2026 9:50:00 AM; NameUpdated=12/31/1969 6:00:00 PM} - Id : 2 HwAddr : ip-127.0.0.1 Interface : lo -FirstSeen : 9/29/2026 9:14:50 AM -LastQuery : 9/29/2026 9:14:51 AM -NumQueries : 3 +FirstSeen : 9/30/2026 7:34:38 AM +LastQuery : 9/30/2026 9:00:01 AM +NumQueries : 12 MacVendor : -Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=9/29/2026 9:50:00 AM; NameUpdated=9/29/2026 9:50:00 AM} +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=9/30/2026 9:17:00 AM; NameUpdated=9/30/2026 9:17:00 AM} + +Id : 1 +HwAddr : 74:56:3c:bb:f4:0b +Interface : wlan0 +FirstSeen : 9/30/2026 7:34:37 AM +LastQuery : 9/30/2026 7:49:13 AM +NumQueries : 275 +MacVendor : Giga-Byte Technology Co.,Ltd. +Ips : @{Ip=192.168.1.162; Name=; LastSeen=9/30/2026 9:17:00 AM; NameUpdated=12/31/1969 6:00:00 PM} ``` ### Remove-PiHoleNetworkDevice @@ -1491,9 +1535,9 @@ CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/29/2026 9:42:08 AM -LastActive : 9/29/2026 9:42:13 AM -ValidUntil : 9/29/2026 10:12:13 AM +LoginAt : 9/30/2026 9:09:06 AM +LastActive : 9/30/2026 9:09:11 AM +ValidUntil : 9/30/2026 9:39:11 AM 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/FTLInformation/Get-PiHoleLogDnsmasq.Integration.Tests.ps1 b/tests/FTLInformation/Get-PiHoleLogDnsmasq.Integration.Tests.ps1 new file mode 100644 index 0000000..f48b94d --- /dev/null +++ b/tests/FTLInformation/Get-PiHoleLogDnsmasq.Integration.Tests.ps1 @@ -0,0 +1,55 @@ +# 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-PiHoleLogDnsmasq (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 DNS log content as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogDnsmasq -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.NextID | Should -BeGreaterOrEqual 0 + $result.File | Should -Not -BeNullOrEmpty + } + + It 'only returns lines added after NextID on a follow-up call' -Skip:(-not $script:ConfigAvailable) { + $first = Get-PiHoleLogDnsmasq -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $second = Get-PiHoleLogDnsmasq -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -NextID $first.NextID + $second | Format-List | Out-String | Write-Host + + # No guarantee new log lines were generated between the two calls, so this just + # confirms the call succeeds and NextID never goes backwards. + $second.NextID | Should -BeGreaterOrEqual $first.NextID + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogDnsmasq -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.PSObject.Properties.Name | Should -Contain 'log' + $result.PSObject.Properties.Name | Should -Contain 'nextID' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogDnsmasq -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/FTLInformation/Get-PiHoleLogFtl.Integration.Tests.ps1 b/tests/FTLInformation/Get-PiHoleLogFtl.Integration.Tests.ps1 new file mode 100644 index 0000000..8c89e1e --- /dev/null +++ b/tests/FTLInformation/Get-PiHoleLogFtl.Integration.Tests.ps1 @@ -0,0 +1,55 @@ +# 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-PiHoleLogFtl (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 FTL log content as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogFtl -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.NextID | Should -BeGreaterOrEqual 0 + $result.File | Should -Not -BeNullOrEmpty + } + + It 'only returns lines added after NextID on a follow-up call' -Skip:(-not $script:ConfigAvailable) { + $first = Get-PiHoleLogFtl -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $second = Get-PiHoleLogFtl -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -NextID $first.NextID + $second | Format-List | Out-String | Write-Host + + # No guarantee new log lines were generated between the two calls, so this just + # confirms the call succeeds and NextID never goes backwards. + $second.NextID | Should -BeGreaterOrEqual $first.NextID + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogFtl -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.PSObject.Properties.Name | Should -Contain 'log' + $result.PSObject.Properties.Name | Should -Contain 'nextID' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogFtl -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/GroupManagement/Remove-PiHoleGroup.Integration.Tests.ps1 b/tests/GroupManagement/Remove-PiHoleGroup.Integration.Tests.ps1 index 5bb76e8..51525ab 100644 --- a/tests/GroupManagement/Remove-PiHoleGroup.Integration.Tests.ps1 +++ b/tests/GroupManagement/Remove-PiHoleGroup.Integration.Tests.ps1 @@ -11,6 +11,7 @@ Describe 'Remove-PiHoleGroup (Integration)' -Tag 'Integration' { Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force $script:TestGroupName = 'PesterGroup' + $script:TestGroupNames = 'PesterBatchGroup1', 'PesterBatchGroup2', 'PesterBatchGroup3' $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' if (Test-Path $configPath) { @@ -19,14 +20,16 @@ Describe 'Remove-PiHoleGroup (Integration)' -Tag 'Integration' { $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl - # Defensive cleanup in case a previous failed run left the test group behind + # Defensive cleanup in case a previous failed run left the test group(s) behind Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null } } AfterAll { if ($script:PiHoleServer) { Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null } } @@ -44,6 +47,39 @@ Describe 'Remove-PiHoleGroup (Integration)' -Tag 'Integration' { $remaining | Should -BeNullOrEmpty } + It 'removes multiple groups in a single batch call' -Skip:(-not $script:ConfigAvailable) { + foreach ($Name in $script:TestGroupNames) { + New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $Name | Out-Null + } + + $result = Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + @($result).Count | Should -Be $script:TestGroupNames.Count + $result.Name | Should -Be $script:TestGroupNames + $result.Status | Should -Be @('Deleted', 'Deleted', 'Deleted') + + $remaining = Get-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl | Where-Object { $_.Name -in $script:TestGroupNames } + $remaining | Should -BeNullOrEmpty + } + + It 'errors and deletes nothing when one of several group names does not exist' -Skip:(-not $script:ConfigAvailable) { + New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames[0] | Out-Null + + $result = Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName @($script:TestGroupNames[0], 'DefinitelyNotARealGroup') -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + # The real group named alongside the fake one must survive - this function validates + # every name before deleting any of them, rather than partially applying the batch. + $stillThere = Get-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames[0] + $stillThere | Should -Not -BeNullOrEmpty + + Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupNames[0] -ErrorAction SilentlyContinue | Out-Null + } + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName | Out-Null diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index 2f9b2c1..c58b1ad 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -237,6 +237,14 @@ Add-Example -Category Config -FunctionName 'Get-PiHoleLogWebserver' ` -Invocation 'Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +Add-Example -Category Config -FunctionName 'Get-PiHoleLogDnsmasq' ` + -Invocation 'Get-PiHoleLogDnsmasq -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleLogDnsmasq -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleLogFtl' ` + -Invocation 'Get-PiHoleLogFtl -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleLogFtl -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + $teleporterFolder = Join-Path ([System.IO.Path]::GetTempPath()) 'PiHoleShellDocsExample' if (Test-Path $teleporterFolder) { Remove-Item $teleporterFolder -Recurse -Force } New-Item -ItemType Directory -Path $teleporterFolder | Out-Null