diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index a28485d..6f5330e 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -34,7 +34,9 @@ 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-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', 'Get-PiHoleLogWebserver', ` #History - 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient' + 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient', ` + #Teleporter + 'Get-PiHoleTeleporterDownload' ) \ No newline at end of file diff --git a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 index 845b89d..feaff04 100644 --- a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 +++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 @@ -1,11 +1,32 @@ - function Get-PiHoleLogWebserver { <# .SYNOPSIS -Get info about logs for webserver +Get webserver log content + +.DESCRIPTION +Request content from the log of the embedded CivetWeb HTTP server. 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-PiHoleLogWebserver -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/logs/webserver')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -13,19 +34,16 @@ Get info about logs for webserver [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [int]$NextID, + [Nullable[int]]$NextID, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - if ($NextID) { - $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver?nextId=$NextId" - - } - else { - $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver" + $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver" + if ($PSBoundParameters.ContainsKey('NextID')) { + $Uri += "?nextID=$NextID" } $Params = @{ @@ -43,13 +61,26 @@ Get info about logs for webserver } else { - #$ObjectFinal = @() + $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 - break } finally { @@ -57,4 +88,4 @@ Get info about logs for webserver Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 index 50d09ba..70d58f8 100644 --- a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 +++ b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 @@ -3,20 +3,31 @@ function Get-PiHoleList { .SYNOPSIS Get lists +.DESCRIPTION +Request Pi-hole's subscribed allow/block lists. Omit -List to get every list; 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 List +The URL of a specific list to return. Omit to return every list + .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -List "https://hosts-file.net/ad_servers.txt" #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/lists/-list-')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( diff --git a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 index 4da0524..07fc347 100644 --- a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 +++ b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 @@ -1,11 +1,32 @@ function Get-PiHoleTeleporterDownload { <# .SYNOPSIS -Get info about logs for webserver +Export Pi-hole settings +.DESCRIPTION +Downloads an archived copy of Pi-hole's current configuration (a Teleporter backup) to disk. +The API always returns a binary application/zip archive, not JSON, so there's no -RawOutput +option here - the downloaded file is the only output. + +.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 FolderPath +The folder to save the backup file into. Must already exist + +.PARAMETER FileName +The name to give the backup file, without an extension - ".zip" is appended automatically + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.EXAMPLE +Get-PiHoleTeleporterDownload -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FolderPath "C:\Backups" -FileName "pihole-backup" #> - #Work In Progress - [CmdletBinding()] + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/teleporter')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( [Parameter(Mandatory = $true)] @@ -16,51 +37,41 @@ Get info about logs for webserver [System.IO.DirectoryInfo]$FolderPath, [Parameter(Mandatory = $true)] [string]$FileName, - [bool]$IgnoreSsl = $false, - [bool]$RawOutput = $false + [bool]$IgnoreSsl = $false ) try { - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - if (!(Test-Path -Path $FolderPath)) { throw "$FolderPath does not exist!" } - $FileName = "$FileName.tar.gz" + $FileName = "$FileName.zip" $OutFile = "$FolderPath\$FileName" if (Test-Path -Path $OutFile) { throw "$OutFile already exists!" } + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $Params = @{ Headers = @{sid = $($Sid) } Uri = "$($PiHoleServer.OriginalString)/api/teleporter" Method = "Get" SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" + OutFile = $OutFile } - $Response = Invoke-RestMethod @Params -OutFile $OutFile - - if ($RawOutput) { - Write-Output $Response - } + Invoke-RestMethod @Params - else { - $ObjectFinal = @() - $Object = [PSCustomObject]@{ - FileName = $FileName - FilePath = $OutFile - RootFolder = $FolderPath - FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB) - } - $ObjectFinal += $Object - Write-Output $ObjectFinal + $Object = [PSCustomObject]@{ + FileName = $FileName + FilePath = $OutFile + RootFolder = $FolderPath + FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB) } + Write-Output $Object } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -68,4 +79,4 @@ Get info about logs for webserver Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index b4711ea..59c247c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A PowerShell module for automating and scripting against the **Pi-hole v6 REST A - [Getting an API Password](#getting-an-api-password) - [Quick Start](#quick-start) - [Command Reference](#command-reference) +- [Example Output](docs/EXAMPLES.md) - [Testing](#testing) - [Contributing](#contributing) - [License](#license) @@ -82,6 +83,8 @@ Every function accepts the same core parameters: Functions marked 🚧 are still under active development — signatures and output shapes may change. This section is generated from the module's actual exported functions by `tools/Update-ReadmeCommandReference.ps1`, and kept in sync automatically on every `develop` → `main` pull request — don't hand-edit the block below. +See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every function below. + ### Actions @@ -113,7 +116,7 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| | `Add-PiHoleList` | Add a new list | -| `Get-PiHoleList` 🚧 | Get lists | +| `Get-PiHoleList` | Get lists | | `Remove-PiHoleList` | Remove a list | | `Search-PiHoleListDomain` | _No description yet_ | | `Update-PiHoleList` | Update a list | @@ -155,7 +158,9 @@ Functions marked 🚧 are still under active development — signatures and outp | `Get-PiHoleInfoSensors` | Get info about various sensors | | `Get-PiHoleInfoSystem` | Get info about various system parameters | | `Get-PiHoleInfoVersion` | Get Pi-hole version | +| `Get-PiHoleLogWebserver` | Get webserver log content | | `Get-PiHolePadd` | Get summarized data for PADD | +| `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md new file mode 100644 index 0000000..69b539a --- /dev/null +++ b/docs/EXAMPLES.md @@ -0,0 +1,1048 @@ +# Example Output + +Real output captured from every exported function against a live Pi-hole v6 server, generated by `tools/Update-ExampleOutput.ps1`. Values (query counts, IDs, timestamps, etc.) reflect whatever that specific server had at capture time - the shape of the output is what matters here, not the exact numbers. + +Regenerate with: + +```powershell +./tools/Update-ExampleOutput.ps1 +# or, to also capture the small number of functions with real side effects on a live server: +./tools/Update-ExampleOutput.ps1 -IncludeDisruptive +``` + +## Actions + +### Invoke-PiHoleFlushNetwork + +```powershell +Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +Status : Flushed + +_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_ +``` + +### Invoke-PiHoleFlushLogs + +```powershell +Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +Status : Flushed + +_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_ +``` + +### Restart-PiHoleDnsService + +```powershell +Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +Status : Restarted + +_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_ +``` + +### Update-PiHoleActionsGravity + +_Rebuilds the entire gravity database - typically takes 1-2 minutes._ + +```powershell +Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +Status : Completed + +_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_ +``` + +## DNS Control + +### Get-PiHoleDnsBlockingStatus + +```powershell +Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Blocking : enabled +Timer : 0 +``` + +### Set-PiHoleDnsBlocking + +```powershell +Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 60 +``` + +``` +_Not captured this run - re-run with -IncludeDisruptive to include a live example._ +``` + +## Group Management + +### New-PiHoleGroup + +```powershell +New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup" -Comment "Example group" +``` + +``` + +Name : PiHoleShellDocsExampleGroup +Comment : Example group +Enabled : True +Id : 4 +DateAdded : 9/26/2026 11:55:24 AM +DateModified : 9/26/2026 11:55:24 AM +``` + +### Get-PiHoleGroup + +```powershell +Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Name : Default +Comment : The default group +Enabled : True +Id : 0 +DateAdded : 6/25/2025 12:32:45 AM +DateModified : 6/25/2025 12:32:45 AM + +Name : testgroup2 +Comment : +Enabled : True +Id : 2 +DateAdded : 7/1/2025 10:13:36 PM +DateModified : 7/1/2025 10:13:36 PM + +Name : testgroup1 +Comment : +Enabled : True +Id : 3 +DateAdded : 7/1/2025 10:13:40 PM +DateModified : 7/1/2025 10:13:40 PM + +Name : PiHoleShellDocsExampleGroup +Comment : Example group +Enabled : True +Id : 4 +DateAdded : 9/26/2026 11:55:24 AM +DateModified : 9/26/2026 11:55:24 AM +``` + +### Update-PiHoleGroup + +```powershell +Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup" -Enabled $false +``` + +``` + +Name : PiHoleShellDocsExampleGroup +Comment : Example group +Enabled : False +Id : 4 +DateAdded : 9/26/2026 11:55:24 AM +DateModified : 9/26/2026 11:55:56 AM +``` + +### Remove-PiHoleGroup + +```powershell +Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup" +``` + +``` + +Name : PiHoleShellDocsExampleGroup +Status : Deleted +``` + +## List Management + +### Add-PiHoleList + +```powershell +Add-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block -Comment "Example list" +``` + +``` + +Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt +Comment : Example list +Groups : {Default} +Enabled : True +Id : 65 +DateAdded : 9/26/2026 11:57:06 AM +DateModified : 9/26/2026 11:57:06 AM +Type : Block +DateUpdated : +Number : 0 +InvalidDomains : 0 +AbpEntries : 0 +Status : 0 +``` + +### Get-PiHoleList + +```powershell +Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Address : https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts +Comment : +Groups : {Default} +Enabled : True +Id : 51 +DateAdded : 7/6/2025 2:20:13 AM +DateModified : 7/6/2025 2:20:13 AM +Type : block +DateUpdated : 7/6/2025 2:20:13 AM +Number : 75945 +InvalidDomains : 1 +AbpEntries : 0 +Status : 2 + +Address : https://adaway.org/hosts.txt +Comment : +Groups : {Default} +Enabled : True +Id : 52 +DateAdded : 7/6/2025 2:20:40 AM +DateModified : 7/6/2025 2:20:40 AM +Type : block +DateUpdated : 7/6/2025 2:20:40 AM +Number : 6540 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Address : https://v.firebog.net/hosts/AdguardDNS.txt +Comment : +Groups : {Default} +Enabled : True +Id : 53 +DateAdded : 7/6/2025 2:20:46 AM +DateModified : 7/6/2025 2:20:46 AM +Type : block +DateUpdated : 7/6/2025 2:20:46 AM +Number : 181864 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Address : https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt +Comment : +Groups : {Default} +Enabled : True +Id : 54 +DateAdded : 7/6/2025 2:20:59 AM +DateModified : 7/6/2025 2:20:59 AM +Type : block +DateUpdated : 7/6/2025 2:20:59 AM +Number : 42531 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Address : https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt +Comment : +Groups : {Default} +Enabled : True +Id : 55 +DateAdded : 7/6/2025 2:21:09 AM +DateModified : 7/6/2025 2:21:09 AM +Type : block +DateUpdated : 7/6/2025 2:21:09 AM +Number : 2700 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +_(showing 5 of 15 results)_ +``` + +### Update-PiHoleList + +```powershell +Update-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block -Enabled $false +``` + +``` + +Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt +Comment : Example list +Groups : {Default} +Enabled : False +Id : 65 +DateAdded : 9/26/2026 11:57:06 AM +DateModified : 9/26/2026 11:58:08 AM +Type : Block +DateUpdated : +Number : 0 +InvalidDomains : 0 +AbpEntries : 0 +Status : 0 +``` + +### Search-PiHoleListDomain + +```powershell +Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $Password -Domain "doubleclick.net" +``` + +``` + +Domain : doubleclick.net +Address : https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts +Comment : +Enabled : True +Id : 51 +Type : block +Groups : {0} +DateAdded : 7/6/2025 2:20:13 AM +DateModified : 7/6/2025 2:20:13 AM +DateUpdated : 9/25/2026 3:30:51 PM +Number : 75945 +InvalidDomains : 1 +AbpEntries : 0 +Status : 2 + +Domain : doubleclick.net +Address : https://adaway.org/hosts.txt +Comment : +Enabled : True +Id : 52 +Type : block +Groups : {0} +DateAdded : 7/6/2025 2:20:40 AM +DateModified : 7/6/2025 2:20:40 AM +DateUpdated : 7/6/2025 2:25:21 AM +Number : 6540 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Domain : doubleclick.net +Address : https://v.firebog.net/hosts/AdguardDNS.txt +Comment : +Enabled : True +Id : 53 +Type : block +Groups : {0} +DateAdded : 7/6/2025 2:20:46 AM +DateModified : 7/6/2025 2:20:46 AM +DateUpdated : 9/25/2026 12:13:06 PM +Number : 181864 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Domain : doubleclick.net +Address : https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt +Comment : +Enabled : True +Id : 54 +Type : block +Groups : {0} +DateAdded : 7/6/2025 2:20:59 AM +DateModified : 7/6/2025 2:20:59 AM +DateUpdated : 12/27/2025 9:28:21 PM +Number : 42531 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +Domain : doubleclick.net +Address : https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt +Comment : +Enabled : True +Id : 55 +Type : block +Groups : {0} +DateAdded : 7/6/2025 2:21:09 AM +DateModified : 7/6/2025 2:21:09 AM +DateUpdated : 6/6/2026 11:13:26 PM +Number : 2700 +InvalidDomains : 0 +AbpEntries : 0 +Status : 2 + +_(showing 5 of 7 results)_ +``` + +### Remove-PiHoleList + +```powershell +Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block +``` + +``` + +Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt +Type : Block +Status : Removed +``` + +## Metrics + +### Get-PiHoleStatsSummary + +```powershell +Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Total : 0 +Blocked : 0 +PercentBlocked : 0 +UniqueDomains : 0 +Forwarded : 0 +Cached : 0 +Frequency : 0 +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=500421; LastUpdate=1790441671} +``` + +### Get-PiHoleStatsRecentBlocked + +```powershell +Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleStatsQueryType + +```powershell +Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Type : A +Count : 0 + +Type : AAAA +Count : 0 + +Type : ANY +Count : 0 + +Type : SRV +Count : 0 + +Type : SOA +Count : 0 + +_(showing 5 of 16 results)_ +``` + +### Get-PiHoleStatsTopDomain + +```powershell +Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleStatsTopClient + +```powershell +Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleStatsUpstream + +```powershell +Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +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 + +```powershell +Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Domain : {} +ClientIp : {} +ClientName : {} +Upstream : {blocklist, cache, permitted} +Type : {A, AAAA, ANY, SRV…} +Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} +Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} +Dnssec : {UNKNOWN, SECURE, INSECURE, BOGUS…} +``` + +### Get-PiHoleStatsDatabaseSummary + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +SumQueries : 0 +SumBlocked : 0 +PercentBlocked : 0 +TotalClients : 0 +``` + +### Get-PiHoleStatsDatabaseQueryType + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Type : A +Count : 0 + +Type : AAAA +Count : 0 + +Type : ANY +Count : 0 + +Type : SRV +Count : 0 + +Type : SOA +Count : 0 + +_(showing 5 of 16 results)_ +``` + +### Get-PiHoleStatsDatabaseTopDomain + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleStatsDatabaseTopClient + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleStatsDatabaseUpstream + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +TotalQueries : 0 +ForwardedQueries : 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 + +### Get-PiHoleConfig + +```powershell +Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +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=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-PiHolePadd + +```powershell +Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +CpuPercent : 0 +MemoryPercent : 0 +ActiveClients : 0 +Blocking : enabled +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 : 500421 +HostModel : Raspberry Pi Zero W Rev 1.1 +IFace : @{v4=; v6=} +NodeName : dns3.localdomain +Pid : 389 +Queries : @{Total=0; Blocked=0; PercentBlocked=0; QueryFrequency=0} +RecentBlocked : +Sensors : @{CpuTemp=40.622; HotLimit=60; Unit=C} +System : @{Uptime=558344; Memory=; Procs=75; Cpu=; Ftl=} +TopBlocked : +TopClient : +TopDomain : +Version : @{Core=; Web=; Ftl=; Docker=} +``` + +### Get-PiHoleInfoHost + +```powershell +Get-PiHoleInfoHost -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +DomainName : (none) +Machine : armv6l +NodeName : dns3.localdomain +Release : 6.12.109+rpt-rpi-v6 +SysName : Linux +Version : #1 Raspbian 1:6.12.109-1+rpt1 (2026-09-11) +Model : Raspberry Pi Zero W Rev 1.1 +BiosVendor : +BoardName : +BoardVendor : +BoardVersion : +ProductName : +ProductFamily : +ProductVersion : +SysVendor : +``` + +### Get-PiHoleInfoSystem + +```powershell +Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Uptime : 558364 +Memory : @{Ram=; Swap=} +Procs : 74 +Cpu : @{NumProcessors=1; PercentCpu=100.800003051758; Load=} +Ftl : @{PercentMemory=2.29744291305542; PercentCpu=49.7000007629395} +``` + +### Get-PiHoleInfoFtl + +```powershell +Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Database : @{Gravity=500421; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} +PrivacyLevel : 0 +QueryFrequency : 0 +Clients : @{Total=0; Active=0} +Pid : 389 +Uptime : 160835.739884 +PercentMemory : 2.29415488243103 +PercentCpu : 49.7999992370605 +AllowDestructive : True +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 + +```powershell +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 : 40.084 +HotLimit : 60 +Unit : C +``` + +### Get-PiHoleInfoDatabase + +```powershell +Get-PiHoleInfoDatabase -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Size : 13983744 +Type : Regular file +Mode : rw-r----- +AccessTime : 6/25/2025 12:32:44 AM +ModifiedTime : 9/26/2026 11:50:35 AM +ChangeTime : 9/26/2026 11:50:35 AM +Owner : @{User=; Group=} +Queries : 0 +EarliestTimestamp : 9/25/2026 11:50:36 AM +QueriesDisk : 0 +EarliestTimestampDisk : +SqliteVersion : 3.53.1 +``` + +### Get-PiHoleInfoVersion + +```powershell +Get-PiHoleInfoVersion -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Core : @{Local=; Remote=} +Web : @{Local=; Remote=} +Ftl : @{Local=; Remote=} +Docker : @{Local=; Remote=} +``` + +### Get-PiHoleInfoMetrics + +```powershell +Get-PiHoleInfoMetrics -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Dns : @{Cache=; Replies=} +Dhcp : @{Ack=0; Nak=0; Decline=0; Offer=0; Discover=0; Inform=0; Request=0; Release=0; NoAnswer=0; Bootp=0; Pxe=0; Leases=} +``` + +### Get-PiHoleInfoClient + +_Does not require -Password - the API does not authenticate this endpoint._ + +```powershell +Get-PiHoleInfoClient -PiHoleServer $PiHoleServer +``` + +``` + +RemoteAddress : 192.168.1.162 +HttpVersion : 1.1 +Method : GET +Headers : {@{Name=Host; Value=dns3.localdomain:8489}, @{Name=User-Agent; Value=Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6}, @{Name=Accept-Encoding; Value=gzip, deflate, br}, @{Name=Content-Type; Value=application/json}…} +``` + +### Get-PiHoleInfoLogin + +_Does not require -Password - the API does not authenticate this endpoint (it is meant to be usable before logging in)._ + +```powershell +Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer +``` + +``` + +HttpsPort : 8489 +Dns : True +``` + +### Get-PiHoleInfoMessage + +```powershell +Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Id : 1 +Timestamp : 9/26/2026 11:50:37 AM +Type : LOAD +Plain : Long-term load (15min avg) larger than number of processors: 1.6 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.6 > 1
This may slow down DNS resolution and can cause bottlenecks. +``` + +### Get-PiHoleInfoMessageCount + +```powershell +Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Count : 1 +``` + +### Remove-PiHoleInfoMessage + +_Diagnosis messages arise from real FTL warnings and cannot be manufactured on demand, so this example shows the error for a message ID that does not exist rather than a fabricated success._ + +```powershell +Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password -MessageId 3 +``` + +``` + +Error : Response status code does not indicate success: 404 (Not Found). +``` + +### Get-PiHoleLogWebserver + +```powershell +Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Log : @{Timestamp=9/26/2026 11:50:37 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} +NextID : 1 +Pid : 389 +File : /var/log/pihole/webserver.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._ + +```powershell +Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -FolderPath "C:\Backups" -FileName "pihole-backup" +``` + +``` + +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 +``` + +### Get-PiHoleHistory + +```powershell +Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Timestamp : 9/25/2026 12:05:00 PM +Total : 0 +Cached : 0 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/25/2026 12:15:00 PM +Total : 0 +Cached : 0 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/25/2026 12:25:00 PM +Total : 0 +Cached : 0 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/25/2026 12:35:00 PM +Total : 0 +Cached : 0 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/25/2026 12:45:00 PM +Total : 0 +Cached : 0 +Blocked : 0 +Forwarded : 0 + +_(showing 5 of 145 results)_ +``` + +### Get-PiHoleHistoryClient + +```powershell +Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Timestamp : 9/25/2026 12:05:00 PM +Clients : @{IP=others; Name=; Count=0} + +Timestamp : 9/25/2026 12:15:00 PM +Clients : @{IP=others; Name=; Count=0} + +Timestamp : 9/25/2026 12:25:00 PM +Clients : @{IP=others; Name=; Count=0} + +Timestamp : 9/25/2026 12:35:00 PM +Clients : @{IP=others; Name=; Count=0} + +Timestamp : 9/25/2026 12:45:00 PM +Clients : @{IP=others; Name=; Count=0} + +_(showing 5 of 145 results)_ +``` + +### Get-PiHoleHistoryDatabase + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +### Get-PiHoleHistoryDatabaseClient + +_Defaults to the last 8 hours; pass -From/-Until for a different window._ + +```powershell +Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` +(no output) +``` + +## Authentication + +### Get-PiHoleCurrentAuthSession + +```powershell +Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Id : 0 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/26/2026 11:26:28 AM +LastActive : 9/26/2026 11:26:28 AM +ValidUntil : 9/26/2026 11:56:28 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 : +App : True +Cli : False + +Id : 1 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/26/2026 11:50:14 AM +LastActive : 9/26/2026 11:50:23 AM +ValidUntil : 9/26/2026 12:20:23 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/26/2026 11:50:38 AM +LastActive : 9/26/2026 11:50:42 AM +ValidUntil : 9/26/2026 12:20:42 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 : 3 +CurrentSession : True +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/26/2026 11:51:30 AM +LastActive : 9/26/2026 11:51:38 AM +ValidUntil : 9/26/2026 12:21:38 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 +``` + +### Remove-PiHoleAuthSession + +_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 3 +``` + +``` + + +Id : 3 +Status : Removed +``` + +### Remove-PiHoleCurrentAuthSession + +_The internal best-effort logout every other function calls automatically when it finishes - it never produces output, even on failure (a warning only). Shown here for completeness since it is still a publicly exported function._ + +```powershell +Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid +``` + +``` +(no output) +``` diff --git a/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 b/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 new file mode 100644 index 0000000..05e5d71 --- /dev/null +++ b/tests/FTLInformation/Get-PiHoleLogWebserver.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-PiHoleLogWebserver (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 webserver log content as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogWebserver -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-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $second = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -NextID $first.NextID + $second | Format-List | Out-String | Write-Host + + # No guarantee new webserver 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-PiHoleLogWebserver -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-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 new file mode 100644 index 0000000..730f051 --- /dev/null +++ b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 @@ -0,0 +1,72 @@ +# 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-PiHoleTeleporterDownload (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 + } + + $script:TestFolder = Join-Path ([System.IO.Path]::GetTempPath()) "PiHoleShellPesterTeleporter" + if (Test-Path $script:TestFolder) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + New-Item -ItemType Directory -Path $script:TestFolder | Out-Null + } + + AfterAll { + if (Test-Path $script:TestFolder) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + } + + It 'downloads a real ZIP backup and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'pester-backup' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.FileName | Should -Be 'pester-backup.zip' + $result.FileSizeKB | Should -BeGreaterThan 0 + Test-Path $result.FilePath | Should -BeTrue + + # Confirm it's a genuine, openable ZIP archive, not just a file with a .zip name. + $zip = [System.IO.Compression.ZipFile]::OpenRead($result.FilePath) + try { + $zip.Entries.Count | Should -BeGreaterThan 0 + } + finally { + $zip.Dispose() + } + } + + It 'errors when the destination file already exists' -Skip:(-not $script:ConfigAvailable) { + Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' | Out-Null + + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when the destination folder does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath (Join-Path $script:TestFolder 'does-not-exist') -FileName 'x' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'bad-password-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 new file mode 100644 index 0000000..c72e23c --- /dev/null +++ b/tools/Update-ExampleOutput.ps1 @@ -0,0 +1,375 @@ +<# +.SYNOPSIS +Regenerates docs/EXAMPLES.md with real, captured output from every exported PiHoleShell function. + +.DESCRIPTION +Runs each exported function against a real Pi-hole server (using tests/IntegrationConfig.local.ps1 +for credentials, same as the integration tests) and captures its actual output into a per-function +Markdown section, grouped the same way as README.md's Command Reference. + +State-changing functions that create/update/delete a real resource (groups, lists, sessions) do so +against a clearly-named throwaway resource ("PiHoleShellDocsExample...") that is removed immediately +after its output is captured - the same create/verify/cleanup pattern already used throughout this +module's own integration tests. + +Functions with real, non-trivial side effects on a live system (rebuilding gravity, restarting the +DNS resolver, flushing the network/query log tables, toggling DNS blocking) are NOT invoked by +default. Pass -IncludeDisruptive to also live-capture those; without it, their sections keep a +static, dated example that was captured previously and is embedded in this script. + +.PARAMETER IncludeDisruptive +Also live-run functions with real side effects on a running Pi-hole server: gravity rebuild, DNS +service restart, network/query-log flush, and DNS blocking toggle. Off by default. + +.PARAMETER RepoRoot +Path to the repository root. Defaults to the parent of this script's folder. +#> +[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Console progress/status output for an interactive tool script, not module code.')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Invoke-Quietly intentionally swallows errors for best-effort setup/cleanup calls that are not the example being documented.')] +param ( + [switch]$IncludeDisruptive, + [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')) +) + +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $RepoRoot 'PiHoleShell/PiHoleShell.psm1' +$configPath = Join-Path $RepoRoot 'tests/IntegrationConfig.local.ps1' +$outputPath = Join-Path $RepoRoot 'docs/EXAMPLES.md' + +if (-not (Test-Path $configPath)) { + throw "Missing $configPath - copy it from tests/IntegrationConfig.example.ps1 and fill in your real Pi-hole server details first." +} + +Import-Module $modulePath -Force +. $configPath +# $PiHoleServer, $PiHoleToken, $PiHoleIgnoreSsl are now set by the config file above. + +$categoryOrder = [ordered]@{ + Actions = 'Actions' + DnsControl = 'DNS Control' + GroupManagement = 'Group Management' + ListManagement = 'List Management' + Metrics = 'Metrics' + Config = 'Configuration & Diagnostics' + Authentication = 'Authentication' +} +$sectionsByCategory = [ordered]@{} +foreach ($key in $categoryOrder.Keys) { $sectionsByCategory[$key] = [System.Collections.Generic.List[object]]::new() } + +function Add-Example { + param ( + [Parameter(Mandatory)] [string]$Category, + [Parameter(Mandatory)] [string]$FunctionName, + [Parameter(Mandatory)] [string]$Invocation, + [string]$Note, + $Result, + [switch]$Skipped, + [string]$StaticOutput + ) + + if ($Skipped -and $StaticOutput) { + $output = "$StaticOutput`n`n_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_" + } + elseif ($Skipped) { + $output = "_Not captured this run - re-run with -IncludeDisruptive to include a live example._" + } + elseif ($null -eq $Result -or ($Result -is [array] -and $Result.Count -eq 0)) { + $output = '(no output)' + } + else { + $items = @($Result) + $truncated = $items.Count -gt 5 + $shown = if ($truncated) { $items | Select-Object -First 5 } else { $items } + $output = ($shown | Format-List | Out-String).TrimEnd() + if ($truncated) { + $output += "`n`n_(showing 5 of $($items.Count) results)_" + } + } + + $sectionsByCategory[$Category].Add([PSCustomObject]@{ + FunctionName = $FunctionName + Invocation = $Invocation + Note = $Note + Output = $output + }) +} + +function Invoke-Quietly { + # Runs a state-changing call purely for its side effect (setup/cleanup between examples) + # without that call itself becoming a documented example. + param([scriptblock]$ScriptBlock) + try { & $ScriptBlock | Out-Null } catch { } +} + +Write-Host "Capturing example output against $PiHoleServer ..." + +#region Authentication +Add-Example -Category Authentication -FunctionName 'Get-PiHoleCurrentAuthSession' ` + -Invocation 'Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +# Remove-PiHoleAuthSession/-CurrentAuthSession both end a session, so each needs its own throwaway +# session to remove rather than the one the documented call itself would otherwise need to log in +# with - removing your only active session out from under yourself isn't something to demonstrate. +$null = & (Get-Module PiHoleShell) { param($s, $p, $i) Request-PiHoleAuth -PiHoleServer $s -Password $p -IgnoreSsl $i } $PiHoleServer $PiHoleToken $PiHoleIgnoreSsl +Start-Sleep -Seconds 1 +# Excludes CurrentSession (Get-PiHoleCurrentAuthSession's own transient session for this very +# call) so this reliably picks $extraSid1 above rather than the session used to look it up. +$extraSessionId = (Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl | Where-Object { -not $_.CurrentSession } | Sort-Object LoginAt -Descending | Select-Object -First 1).Id +Add-Example -Category Authentication -FunctionName 'Remove-PiHoleAuthSession' ` + -Invocation "Remove-PiHoleAuthSession -PiHoleServer `$PiHoleServer -Password `$Password -Id $extraSessionId" ` + -Note 'Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller''s own session.' ` + -Result (Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Id $extraSessionId) + +$extraSid2 = & (Get-Module PiHoleShell) { param($s, $p, $i) Request-PiHoleAuth -PiHoleServer $s -Password $p -IgnoreSsl $i } $PiHoleServer $PiHoleToken $PiHoleIgnoreSsl +Add-Example -Category Authentication -FunctionName 'Remove-PiHoleCurrentAuthSession' ` + -Invocation 'Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid' ` + -Note 'The internal best-effort logout every other function calls automatically when it finishes - it never produces output, even on failure (a warning only). Shown here for completeness since it is still a publicly exported function.' ` + -Result $null +Invoke-Quietly { Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $extraSid2 -IgnoreSsl $PiHoleIgnoreSsl } +#endregion + +#region DnsControl +Add-Example -Category DnsControl -FunctionName 'Get-PiHoleDnsBlockingStatus' ` + -Invocation 'Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +if ($IncludeDisruptive) { + Add-Example -Category DnsControl -FunctionName 'Set-PiHoleDnsBlocking' ` + -Invocation 'Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 5' ` + -Note 'Temporarily disables blocking, then it re-enables automatically after the given time.' ` + -Result (Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Blocking False -TimeInSeconds 5) + Start-Sleep -Seconds 6 +} +else { + Add-Example -Category DnsControl -FunctionName 'Set-PiHoleDnsBlocking' ` + -Invocation 'Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 60' -Skipped +} +#endregion + +#region Config +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-PiHolePadd' ` + -Invocation 'Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +foreach ($fn in 'Get-PiHoleInfoHost', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics') { + Add-Example -Category Config -FunctionName $fn ` + -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" ` + -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +} + +Add-Example -Category Config -FunctionName 'Get-PiHoleInfoClient' ` + -Invocation 'Get-PiHoleInfoClient -PiHoleServer $PiHoleServer' ` + -Note 'Does not require -Password - the API does not authenticate this endpoint.' ` + -Result (Get-PiHoleInfoClient -PiHoleServer $PiHoleServer -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleInfoLogin' ` + -Invocation 'Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer' ` + -Note 'Does not require -Password - the API does not authenticate this endpoint (it is meant to be usable before logging in).' ` + -Result (Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleInfoMessage' ` + -Invocation 'Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category Config -FunctionName 'Get-PiHoleInfoMessageCount' ` + -Invocation 'Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +$dummyMessageId = 999999 +Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -MessageId $dummyMessageId -Confirm:$false -ErrorVariable removeMessageError -ErrorAction SilentlyContinue | Out-Null +Add-Example -Category Config -FunctionName 'Remove-PiHoleInfoMessage' ` + -Invocation 'Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password -MessageId 3' ` + -Note 'Diagnosis messages arise from real FTL warnings and cannot be manufactured on demand, so this example shows the error for a message ID that does not exist rather than a fabricated success.' ` + -Result $(if ($removeMessageError) { [PSCustomObject]@{ Error = $removeMessageError[-1].Exception.Message } }) + +Add-Example -Category Config -FunctionName 'Get-PiHoleLogWebserver' ` + -Invocation 'Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleLogWebserver -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 +Add-Example -Category Config -FunctionName 'Get-PiHoleTeleporterDownload' ` + -Invocation 'Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -FolderPath "C:\Backups" -FileName "pihole-backup"' ` + -Note 'FolderPath/FileName are yours to choose; the captured output below used a scratch temp folder for this run instead of C:\Backups.' ` + -Result (Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -FolderPath $teleporterFolder -FileName 'pihole-backup') +Remove-Item $teleporterFolder -Recurse -Force +#endregion + +#region GroupManagement +$docsGroupName = 'PiHoleShellDocsExampleGroup' +Invoke-Quietly { Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -WarningAction SilentlyContinue } + +Add-Example -Category GroupManagement -FunctionName 'New-PiHoleGroup' ` + -Invocation "New-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`" -Comment `"Example group`"" ` + -Result (New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -Comment 'Example group') + +# Same settle-time reasoning as the list section below - give the test server a moment before +# relying on the group just being created. +Start-Sleep -Seconds 3 + +Add-Example -Category GroupManagement -FunctionName 'Get-PiHoleGroup' ` + -Invocation 'Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category GroupManagement -FunctionName 'Update-PiHoleGroup' ` + -Invocation "Update-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`" -Enabled `$false" ` + -Result (Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -Enabled $false) + +Add-Example -Category GroupManagement -FunctionName 'Remove-PiHoleGroup' ` + -Invocation "Remove-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`"" ` + -Result (Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName) +#endregion + +#region ListManagement +$docsListAddress = 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt' +Invoke-Quietly { Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Confirm:$false } + +Add-Example -Category ListManagement -FunctionName 'Add-PiHoleList' ` + -Invocation "Add-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block -Comment `"Example list`"" ` + -Result (Add-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Comment 'Example list') + +# The test server takes a moment to make a just-added list queryable again - a real hardware +# limitation of this Pi Zero W, not a module bug (confirmed by re-running the same calls a few +# seconds apart by hand). Give it a moment before relying on the list existing below. +Start-Sleep -Seconds 3 + +Add-Example -Category ListManagement -FunctionName 'Get-PiHoleList' ` + -Invocation 'Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +Add-Example -Category ListManagement -FunctionName 'Update-PiHoleList' ` + -Invocation "Update-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block -Enabled `$false" ` + -Result (Update-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Enabled $false) + +Add-Example -Category ListManagement -FunctionName 'Search-PiHoleListDomain' ` + -Invocation 'Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $Password -Domain "doubleclick.net"' ` + -Result (Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Domain 'doubleclick.net') + +Add-Example -Category ListManagement -FunctionName 'Remove-PiHoleList' ` + -Invocation "Remove-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block" ` + -Result (Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -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 ` + -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" ` + -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +} + +foreach ($fn in 'Get-PiHoleStatsDatabaseSummary', 'Get-PiHoleStatsDatabaseQueryType', 'Get-PiHoleStatsDatabaseTopDomain', 'Get-PiHoleStatsDatabaseTopClient', 'Get-PiHoleStatsDatabaseUpstream') { + Add-Example -Category Metrics -FunctionName $fn ` + -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" ` + -Note 'Defaults to the last 8 hours; pass -From/-Until for a different window.' ` + -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +} +#endregion + +#region History +# The History folder isn't its own README category (Update-ReadmeCommandReference.ps1 folds it, +# and a few other small folders, into "Configuration & Diagnostics") - matching that here too. +foreach ($fn in 'Get-PiHoleHistory', 'Get-PiHoleHistoryClient') { + Add-Example -Category Config -FunctionName $fn ` + -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" ` + -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +} +foreach ($fn in 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryDatabaseClient') { + Add-Example -Category Config -FunctionName $fn ` + -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" ` + -Note 'Defaults to the last 8 hours; pass -From/-Until for a different window.' ` + -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +} +#endregion + +#region Actions +if ($IncludeDisruptive) { + Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' ` + -Invocation 'Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false) + + Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushLogs' ` + -Invocation 'Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false) + + Add-Example -Category Actions -FunctionName 'Restart-PiHoleDnsService' ` + -Invocation 'Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + + Add-Example -Category Actions -FunctionName 'Update-PiHoleActionsGravity' ` + -Invocation 'Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password' ` + -Note 'Rebuilds the entire gravity database - typically takes 1-2 minutes.' ` + -Result (Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false) +} +else { + # These four were verified with real output during this module's development; reusing that + # rather than fabricating a plausible-looking one or re-running a disruptive action by default. + Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' ` + -Invocation 'Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password' ` + -Skipped -StaticOutput "Status : Flushed" + Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushLogs' ` + -Invocation 'Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password' ` + -Skipped -StaticOutput "Status : Flushed" + Add-Example -Category Actions -FunctionName 'Restart-PiHoleDnsService' ` + -Invocation 'Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password' ` + -Skipped -StaticOutput "Status : Restarted" + Add-Example -Category Actions -FunctionName 'Update-PiHoleActionsGravity' ` + -Invocation 'Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password' ` + -Note 'Rebuilds the entire gravity database - typically takes 1-2 minutes.' ` + -Skipped -StaticOutput "Status : Completed" +} +#endregion + +# --- Assemble the Markdown document --- +$lines = [System.Collections.Generic.List[string]]::new() +$lines.Add('# Example Output') +$lines.Add('') +$lines.Add("Real output captured from every exported function against a live Pi-hole v6 server, generated by ``tools/Update-ExampleOutput.ps1``. Values (query counts, IDs, timestamps, etc.) reflect whatever that specific server had at capture time - the shape of the output is what matters here, not the exact numbers.") +$lines.Add('') +$lines.Add('Regenerate with:') +$lines.Add('') +$lines.Add('```powershell') +$lines.Add('./tools/Update-ExampleOutput.ps1') +$lines.Add('# or, to also capture the small number of functions with real side effects on a live server:') +$lines.Add('./tools/Update-ExampleOutput.ps1 -IncludeDisruptive') +$lines.Add('```') +$lines.Add('') + +foreach ($key in $categoryOrder.Keys) { + $examples = $sectionsByCategory[$key] + if ($examples.Count -eq 0) { continue } + + $lines.Add("## $($categoryOrder[$key])") + $lines.Add('') + + foreach ($example in $examples) { + $lines.Add("### $($example.FunctionName)") + $lines.Add('') + if ($example.Note) { + $lines.Add("_$($example.Note)_") + $lines.Add('') + } + $lines.Add('```powershell') + $lines.Add($example.Invocation) + $lines.Add('```') + $lines.Add('') + $lines.Add('```') + $lines.Add($example.Output) + $lines.Add('```') + $lines.Add('') + } +} + +$docsDir = Split-Path -Path $outputPath -Parent +if (-not (Test-Path $docsDir)) { + New-Item -ItemType Directory -Path $docsDir | Out-Null +} +Set-Content -Path $outputPath -Value ($lines -join "`n") -NoNewline +Write-Host "Wrote $outputPath"