From 49c0919f219c9153d9602e3472e5f3424065a406 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Tue, 6 Oct 2026 22:13:05 -0500 Subject: [PATCH] feat: add Import-PiHoleTeleporter, completing API coverage Adds Import-PiHoleTeleporter (POST /teleporter), the last remaining functional gap in the Pi-hole v6 API. Restores a Teleporter archive (as produced by Get-PiHoleTeleporterDownload), with flat switch parameters (-Config, -DhcpLeases, -Group, -Adlist, -AdlistByGroup, -Domainlist, -DomainlistByGroup, -Client, -ClientByGroup) selecting which items to restore instead of Pi-hole's own nested request shape. Omitting every switch imports everything, matching the API's own default. This overwrites the current configuration, so unlike every other function in the module it prompts for confirmation by default (ConfirmImpact = 'High') rather than only when -Confirm is passed explicitly - pass -Confirm:$false to skip the prompt, or -WhatIf for a dry run. Confirmed two things by hand against a real server that don't match the OpenAPI spec or weren't otherwise obvious: - The response's processed-items array is actually returned under "files", not "processed" as the spec documents. - Pi-hole reloads internally right after any import, briefly refusing connections - the integration tests and the docs example script both settle afterward to avoid a flaky next call. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 2 +- .../Teleporter/Import-PiHoleTeleporter.ps1 | 165 ++++++ README.md | 1 + docs/EXAMPLES.md | 474 ++++++++++-------- ...ort-PiHoleTeleporter.Integration.Tests.ps1 | 83 +++ tools/Update-ExampleOutput.ps1 | 12 + 6 files changed, 521 insertions(+), 216 deletions(-) create mode 100644 PiHoleShell/Public/Teleporter/Import-PiHoleTeleporter.ps1 create mode 100644 tests/Teleporter/Import-PiHoleTeleporter.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 85b431d..72c39c0 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -38,7 +38,7 @@ Export-ModuleMember -Function @( #History 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient', ` #Teleporter - 'Get-PiHoleTeleporterDownload', ` + 'Get-PiHoleTeleporterDownload', 'Import-PiHoleTeleporter', ` #DomainManagement 'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain', ` #ClientManagement diff --git a/PiHoleShell/Public/Teleporter/Import-PiHoleTeleporter.ps1 b/PiHoleShell/Public/Teleporter/Import-PiHoleTeleporter.ps1 new file mode 100644 index 0000000..a9471d0 --- /dev/null +++ b/PiHoleShell/Public/Teleporter/Import-PiHoleTeleporter.ps1 @@ -0,0 +1,165 @@ +function Import-PiHoleTeleporter { + <# +.SYNOPSIS +Import Pi-hole settings + +.DESCRIPTION +Uploads a Teleporter archive (as produced by Get-PiHoleTeleporterDownload) to restore Pi-hole +from it. This overwrites your current configuration, so unlike most functions in this module it +prompts for confirmation by default - pass -Confirm:$false to skip the prompt, or -WhatIf to see +what would happen without making any change. + +By default every importable item in the archive is restored. Pass one or more of the switch +parameters below to import only those specific items instead - anything not switched on is left +alone. + +.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 FilePath +Path to the Teleporter .zip archive to upload + +.PARAMETER Config +Import Pi-hole's configuration + +.PARAMETER DhcpLeases +Import Pi-hole's DHCP leases + +.PARAMETER Group +Import Pi-hole's groups table + +.PARAMETER Adlist +Import Pi-hole's adlist table + +.PARAMETER AdlistByGroup +Import Pi-hole's table relating adlist entries to groups + +.PARAMETER Domainlist +Import Pi-hole's domainlist table + +.PARAMETER DomainlistByGroup +Import Pi-hole's table relating domainlist entries to groups + +.PARAMETER Client +Import Pi-hole's client table + +.PARAMETER ClientByGroup +Import Pi-hole's table relating client entries to groups + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Import-PiHoleTeleporter -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FilePath "C:\Backups\pihole-backup.zip" + +.EXAMPLE +Import-PiHoleTeleporter -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FilePath "C:\Backups\pihole-backup.zip" -Group -Adlist -AdlistByGroup + +.EXAMPLE +Import-PiHoleTeleporter -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FilePath "C:\Backups\pihole-backup.zip" -WhatIf + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/teleporter')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [System.IO.FileInfo]$FilePath, + [switch]$Config, + [switch]$DhcpLeases, + [switch]$Group, + [switch]$Adlist, + [switch]$AdlistByGroup, + [switch]$Domainlist, + [switch]$DomainlistByGroup, + [switch]$Client, + [switch]$ClientByGroup, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + if (-not (Test-Path -Path $FilePath)) { + throw "$FilePath does not exist!" + } + + # Flat switches rather than Pi-hole's own nested { config, dhcp_leases, gravity: {...} } + # shape - whether any were passed at all decides whether an "import" field is sent; if + # none were, the field is omitted entirely so Pi-hole imports everything (its own default + # when the field is missing), matching this function's own "import everything" default. + $ImportSwitchNames = 'Config', 'DhcpLeases', 'Group', 'Adlist', 'AdlistByGroup', 'Domainlist', 'DomainlistByGroup', 'Client', 'ClientByGroup' + $AnySwitchSpecified = $false + foreach ($Name in $ImportSwitchNames) { + if ($PSBoundParameters.ContainsKey($Name)) { + $AnySwitchSpecified = $true + break + } + } + + $Target = "Pi-Hole at $PiHoleServer" + $Action = "Import Teleporter archive '$FilePath' (this will overwrite your current configuration)" + if ($PSCmdlet.ShouldProcess($Target, $Action)) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $FormData = @{ + file = Get-Item -Path $FilePath + } + + if ($AnySwitchSpecified) { + $ImportBody = [ordered]@{ + config = [bool]$Config + dhcp_leases = [bool]$DhcpLeases + gravity = [ordered]@{ + group = [bool]$Group + adlist = [bool]$Adlist + adlist_by_group = [bool]$AdlistByGroup + domainlist = [bool]$Domainlist + domainlist_by_group = [bool]$DomainlistByGroup + client = [bool]$Client + client_by_group = [bool]$ClientByGroup + } + } + $FormData['import'] = $ImportBody | ConvertTo-Json -Depth 5 -Compress + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/teleporter" + Method = "Post" + SkipCertificateCheck = $IgnoreSsl + Form = $FormData + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + # The live API returns this array under "files", despite the OpenAPI spec + # documenting the field as "processed" - confirmed against a real server. + $Object = [PSCustomObject]@{ + Processed = $Response.files + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/README.md b/README.md index 7a4e239..188f251 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleNetworkRoute` | Get info about the routes of your Pi-hole | | `Get-PiHolePadd` | Get summarized data for PADD | | `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | +| `Import-PiHoleTeleporter` | Import Pi-hole settings | | `Remove-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleDhcpLease` | Remove a DHCP lease | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index a701363..0602c4e 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 : 57 -DateAdded : 10/1/2026 3:13:46 PM -DateModified : 10/1/2026 3:13:46 PM +Id : 63 +DateAdded : 10/6/2026 10:06:30 PM +DateModified : 10/6/2026 10:06:30 PM ``` ### Get-PiHoleGroup @@ -143,9 +143,9 @@ DateModified : 9/30/2026 8:48:26 AM Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 57 -DateAdded : 10/1/2026 3:13:46 PM -DateModified : 10/1/2026 3:13:46 PM +Id : 63 +DateAdded : 10/6/2026 10:06:30 PM +DateModified : 10/6/2026 10:06:30 PM ``` ### Update-PiHoleGroup @@ -159,9 +159,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False -Id : 57 -DateAdded : 10/1/2026 3:13:46 PM -DateModified : 10/1/2026 3:14:04 PM +Id : 63 +DateAdded : 10/6/2026 10:06:30 PM +DateModified : 10/6/2026 10:06:48 PM ``` ### Remove-PiHoleGroup @@ -190,9 +190,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : True -Id : 71 -DateAdded : 10/1/2026 3:14:41 PM -DateModified : 10/1/2026 3:14:41 PM +Id : 81 +DateAdded : 10/6/2026 10:07:25 PM +DateModified : 10/6/2026 10:07:25 PM Type : Block DateUpdated : Number : 0 @@ -218,7 +218,7 @@ 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 : 74761 +Number : 72234 InvalidDomains : 1 AbpEntries : 0 Status : 2 @@ -246,7 +246,7 @@ 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 : 175912 +Number : 176582 InvalidDomains : 0 AbpEntries : 0 Status : 2 @@ -294,9 +294,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : False -Id : 71 -DateAdded : 10/1/2026 3:14:41 PM -DateModified : 10/1/2026 3:15:14 PM +Id : 81 +DateAdded : 10/6/2026 10:07:25 PM +DateModified : 10/6/2026 10:07:58 PM Type : Block DateUpdated : Number : 0 @@ -322,8 +322,8 @@ Type : block Groups : {0} DateAdded : 7/6/2025 2:20:13 AM DateModified : 7/6/2025 2:20:13 AM -DateUpdated : 9/26/2026 11:28:08 PM -Number : 74761 +DateUpdated : 10/2/2026 9:10:24 PM +Number : 72234 InvalidDomains : 1 AbpEntries : 0 Status : 2 @@ -352,8 +352,8 @@ Type : block Groups : {0} DateAdded : 7/6/2025 2:20:46 AM DateModified : 7/6/2025 2:20:46 AM -DateUpdated : 9/30/2026 10:20:00 AM -Number : 175912 +DateUpdated : 10/2/2026 9:43:40 AM +Number : 176582 InvalidDomains : 0 AbpEntries : 0 Status : 2 @@ -421,9 +421,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 10/1/2026 3:15:52 PM -DateModified : 10/1/2026 3:15:52 PM +Id : 18 +DateAdded : 10/6/2026 10:08:36 PM +DateModified : 10/6/2026 10:08:36 PM ``` ### Get-PiHoleDomain @@ -441,9 +441,9 @@ Kind : exact Comment : Example domain Groups : {Default} Enabled : True -Id : 22 -DateAdded : 10/1/2026 3:15:52 PM -DateModified : 10/1/2026 3:15:52 PM +Id : 18 +DateAdded : 10/6/2026 10:08:36 PM +DateModified : 10/6/2026 10:08:36 PM ``` ### Update-PiHoleDomain @@ -461,9 +461,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : False -Id : 22 -DateAdded : 10/1/2026 3:15:52 PM -DateModified : 10/1/2026 3:16:25 PM +Id : 18 +DateAdded : 10/6/2026 10:08:36 PM +DateModified : 10/6/2026 10:09:10 PM ``` ### Remove-PiHoleDomain @@ -494,9 +494,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 22 -DateAdded : 10/1/2026 3:16:56 PM -DateModified : 10/1/2026 3:16:56 PM +Id : 20 +DateAdded : 10/6/2026 10:09:40 PM +DateModified : 10/6/2026 10:09:40 PM ``` ### Get-PiHoleClient @@ -511,9 +511,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 22 -DateAdded : 10/1/2026 3:16:56 PM -DateModified : 10/1/2026 3:16:56 PM +Id : 20 +DateAdded : 10/6/2026 10:09:40 PM +DateModified : 10/6/2026 10:09:40 PM ``` ### Get-PiHoleClientSuggestion @@ -526,13 +526,13 @@ Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password HwAddr : ip-127.0.0.1 MacVendor : -LastQuery : 10/1/2026 3:00:00 PM +LastQuery : 10/6/2026 10:06:14 PM Addresses : 127.0.0.1 Names : localhost HwAddr : 74:56:3c:bb:f4:0b MacVendor : Giga-Byte Technology Co.,Ltd. -LastQuery : 10/1/2026 9:03:33 AM +LastQuery : 10/6/2026 10:03:16 PM Addresses : 192.168.1.162 Names : ``` @@ -549,9 +549,9 @@ Client : 192.168.99.99 Name : Comment : Updated comment Groups : {Default} -Id : 22 -DateAdded : 10/1/2026 3:16:56 PM -DateModified : 10/1/2026 3:17:35 PM +Id : 20 +DateAdded : 10/6/2026 10:09:40 PM +DateModified : 10/6/2026 10:10:18 PM ``` ### Remove-PiHoleClient @@ -576,18 +576,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 52 -Blocked : 10 -PercentBlocked : 19.230770111084 -UniqueDomains : 15 +Total : 72 +Blocked : 8 +PercentBlocked : 11.1111106872559 +UniqueDomains : 16 Forwarded : 27 -Cached : 15 +Cached : 37 Frequency : 0 -Types : @{A=20; AAAA=19; ANY=0; SRV=0; SOA=0; PTR=13; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=10; Forwarded=27; Cache=15; 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=10; Nodata=3; Nxdomain=3; Cname=0; Ip=26; Domain=10; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=18; AAAA=17; ANY=0; SRV=0; SOA=0; PTR=37; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=8; Forwarded=27; Cache=37; 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=7; Nodata=3; Nxdomain=2; Cname=0; Ip=25; Domain=35; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=495564; LastUpdate=1790861541} +Gravity : @{DomainsBeingBlocked=526957; LastUpdate=1791140144} ``` ### Get-PiHoleStatsRecentBlocked @@ -598,7 +598,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : googleadservices.com +Blocked : googlesyndication.com ``` ### Get-PiHoleStatsQueryType @@ -610,10 +610,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 20 +Count : 18 Type : AAAA -Count : 19 +Count : 17 Type : ANY Count : 0 @@ -636,18 +636,18 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` Domain : 1.0.0.127.in-addr.arpa -Count : 7 +Count : 31 -Domain : example.com +Domain : bbc.com Count : 4 -Domain : cloudflare.com +Domain : example.com Count : 4 -Domain : reddit.com +Domain : python.org Count : 4 -Domain : bbc.com +Domain : wikipedia.org Count : 4 _(showing 5 of 10 results)_ @@ -661,13 +661,13 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password ``` -IP : 192.168.1.162 -Name : -Count : 39 - IP : 127.0.0.1 Name : localhost -Count : 13 +Count : 37 + +IP : 192.168.1.162 +Name : +Count : 35 ``` ### Get-PiHoleStatsUpstream @@ -678,9 +678,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 52 +TotalQueries : 72 ForwardedQueries : 27 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=10; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=15; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=27; ResponseTime=0.0202921777963638; Variance=0.00264818341859654}} +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=37; ResponseTime=0; Variance=0}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=23; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=4; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -691,10 +691,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {1.0.0.127.in-addr.arpa, example.com, cloudflare.com, reddit.com…} -ClientIp : {192.168.1.162, 127.0.0.1} +Domain : {1.0.0.127.in-addr.arpa, bbc.com, example.com, python.org…} +ClientIp : {127.0.0.1, 192.168.1.162} 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…} @@ -711,10 +711,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 52 -SumBlocked : 10 -PercentBlocked : 19.230770111084 -TotalClients : 3 +SumQueries : 56 +SumBlocked : 8 +PercentBlocked : 14.2857141494751 +TotalClients : 2 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -728,7 +728,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 19 +Count : 17 Type : AAAA Count : 0 @@ -740,7 +740,7 @@ Type : SRV Count : 0 Type : SOA -Count : 13 +Count : 21 _(showing 5 of 16 results)_ ``` @@ -756,7 +756,10 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` Domain : 1.0.0.127.in-addr.arpa -Count : 7 +Count : 15 + +Domain : python.org +Count : 4 Domain : bbc.com Count : 4 @@ -767,9 +770,6 @@ Count : 4 Domain : example.com Count : 4 -Domain : stackoverflow.com -Count : 4 - _(showing 5 of 10 results)_ ``` @@ -785,15 +785,11 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 29 +Count : 27 IP : 127.0.0.1 Name : localhost -Count : 11 - -IP : 127.0.0.1 -Name : -Count : 2 +Count : 21 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -806,9 +802,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 52 +TotalQueries : 56 ForwardedQueries : 27 -Upstreams : {@{Ip=2; Name=; Port=4294967295; Count=27; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=15; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=10; ResponseTime=0; Variance=0}} +Upstreams : {@{Ip=1; Name=; Port=-1; Count=23; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=-1; Count=4; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=21; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=8; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleQuery @@ -821,69 +817,69 @@ Get-PiHoleQuery -PiHoleServer $PiHoleServer -Password $Password -Length 5 ``` -Id : 51 -Time : 10/1/2026 3:00:00 PM +Id : 394 +Time : 10/6/2026 10:06:15 PM Type : PTR -Domain : 1.0.0.127.in-addr.arpa +Domain : 4.4.8.8.in-addr.arpa Cname : -Status : CACHE +Status : FORWARDED Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.000154972076416016} +Reply : @{Type=DOMAIN; Time=0.0241849422454834} ListId : -Upstream : +Upstream : 8.8.8.8#53 Ede : @{Code=-1; Text=} -Id : 50 -Time : 10/1/2026 2:00:01 PM +Id : 393 +Time : 10/6/2026 10:06:15 PM Type : PTR -Domain : 1.0.0.127.in-addr.arpa +Domain : 8.8.8.8.in-addr.arpa Cname : -Status : CACHE +Status : FORWARDED Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.000155925750732422} +Reply : @{Type=DOMAIN; Time=0.0249054431915283} ListId : -Upstream : +Upstream : 8.8.8.8#53 Ede : @{Code=-1; Text=} -Id : 49 -Time : 10/1/2026 1:00:00 PM +Id : 392 +Time : 10/6/2026 10:06:15 PM Type : PTR -Domain : 1.0.0.127.in-addr.arpa +Domain : 162.1.168.192.in-addr.arpa Cname : Status : CACHE Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.000154018402099609} +Reply : @{Type=NXDOMAIN; Time=0.0157008171081543} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 48 -Time : 10/1/2026 12:00:00 PM +Id : 391 +Time : 10/6/2026 10:06:15 PM Type : PTR Domain : 1.0.0.127.in-addr.arpa Cname : Status : CACHE Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.000212192535400391} +Reply : @{Type=DOMAIN; Time=0.0260829925537109} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 47 -Time : 10/1/2026 11:00:01 AM +Id : 390 +Time : 10/6/2026 10:03:17 PM Type : PTR -Domain : 8.8.8.8.in-addr.arpa +Domain : 4.4.8.8.in-addr.arpa Cname : -Status : CACHE +Status : FORWARDED Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.00014805793762207} +Reply : @{Type=DOMAIN; Time=0.0180368423461914} ListId : -Upstream : +Upstream : 8.8.4.4#53 Ede : @{Code=-1; Text=} ``` @@ -916,9 +912,27 @@ Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password ``` -Key : misc.readOnly +Key : webserver.advancedOpts +Reason : read_only +Description : Variable can only be set in pihole.toml, through an environment variable or using pihole-FTL --config, not via API + +Key : webserver.paths.webroot Reason : read_only -Description : Variable can only be set in pihole.toml, not via API +Description : Variable can only be set in pihole.toml, through an environment variable or using pihole-FTL --config, not via API + +Key : webserver.paths.webhome +Reason : read_only +Description : Variable can only be set in pihole.toml, through an environment variable or using pihole-FTL --config, not via API + +Key : webserver.paths.prefix +Reason : read_only +Description : Variable can only be set in pihole.toml, through an environment variable or using pihole-FTL --config, not via API + +Key : misc.dnsmasq_lines +Reason : read_only +Description : Variable can only be set in pihole.toml, through an environment variable or using pihole-FTL --config, not via API + +_(showing 5 of 6 results)_ ``` ### Set-PiHoleConfig @@ -978,18 +992,18 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=35; Evicted=0} +Cache : @{Size=10000; Inserted=34; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} -GravitySize : 495564 +GravitySize : 526957 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain -Pid : 388 -Queries : @{Total=52; Blocked=10; PercentBlocked=19.230770111084; QueryFrequency=0} -RecentBlocked : googleadservices.com -Sensors : @{CpuTemp=39.546; HotLimit=60; Unit=C} -System : @{Uptime=235882; Memory=; Procs=72; Cpu=; Ftl=} -TopBlocked : googleadservices.com +Pid : 389 +Queries : @{Total=69; Blocked=8; PercentBlocked=11.5942029953003; QueryFrequency=0} +RecentBlocked : googlesyndication.com +Sensors : @{CpuTemp=40.622; HotLimit=60; Unit=C} +System : @{Uptime=384699; Memory=; Procs=70; Cpu=; Ftl=} +TopBlocked : analytics.google.com TopClient : 192.168.1.162 TopDomain : 1.0.0.127.in-addr.arpa Version : @{Core=; Web=; Ftl=; Docker=} @@ -1028,11 +1042,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 235892 +Uptime : 384709 Memory : @{Ram=; Swap=} -Procs : 74 -Cpu : @{NumProcessors=1; PercentCpu=97.1999969482422; Load=} -Ftl : @{PercentMemory=2.52266621589661; PercentCpu=96.4000015258789} +Procs : 70 +Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=} +Ftl : @{PercentMemory=3.02243185043335; PercentCpu=96.5} ``` ### Get-PiHoleInfoFtl @@ -1043,16 +1057,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Database : @{Gravity=495564; Antigravity=0; Groups=4; Lists=14; Clients=0; Domains=; Regex=} +Database : @{Gravity=526957; Antigravity=0; Groups=4; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} -Pid : 388 -Uptime : 24254906.207769 -PercentMemory : 2.52266621589661 -PercentCpu : 96.4000015258789 +Pid : 389 +Uptime : 528324.275473 +PercentMemory : 3.02243185043335 +PercentCpu : 96.5 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=35; DnsCacheLiveFreed=0; DnsQueriesForwarded=27; DnsAuthAnswered=0; DnsLocalAnswered=25; 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} +Dnsmasq : @{DnsCacheInserted=34; DnsCacheLiveFreed=0; DnsQueriesForwarded=25; DnsAuthAnswered=0; DnsLocalAnswered=15; 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 @@ -1064,7 +1078,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 : 40.084 +CpuTemp : 41.16 HotLimit : 60 Unit : C ``` @@ -1081,13 +1095,13 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 10/1/2026 2:06:00 PM -ChangeTime : 10/1/2026 2:06:00 PM +ModifiedTime : 10/6/2026 9:56:20 PM +ChangeTime : 10/6/2026 9:56:20 PM Owner : @{User=; Group=} -Queries : 52 -EarliestTimestamp : 9/30/2026 3:10:00 PM -QueriesDisk : 52 -EarliestTimestampDisk : +Queries : 69 +EarliestTimestamp : 10/5/2026 10:00:00 PM +QueriesDisk : 391 +EarliestTimestampDisk : 10/4/2026 2:31:09 PM SqliteVersion : 3.53.1 ``` @@ -1154,18 +1168,7 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` ``` - -Id : 1 -Timestamp : 10/1/2026 8:28:21 AM -Type : LIST -Plain : List with ID 63 (https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/domains/light.txt) was inaccessible during last gravity run -Html : List with ID 63 (https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/domains/light.txt) was inaccessible during last gravity run - -Id : 2 -Timestamp : 10/1/2026 9:38:21 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. +(no output) ``` ### Get-PiHoleInfoMessageCount @@ -1176,7 +1179,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password ``` -Count : 2 +Count : 0 ``` ### Remove-PiHoleInfoMessage @@ -1200,9 +1203,9 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/1/2026 8:28:22 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=10/1/2026 8:31:16 AM; Message=ACCESS: 192.168.1.162 - - [01/Oct/2026:13:31:15 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4588 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/1/2026 8:31:17 AM; Message=ACCESS: 192.168.1.162 - - [01/Oct/2026:13:31:16 +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=10/1/2026 8:31:26 AM; Message=ACCESS: 192.168.1.162 - - [01/Oct/2026:13:31:17 +0000] "POST /api/auth HTTP/1.1" 200 807 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}…} -NextID : 31 -Pid : 388 +Log : {@{Timestamp=10/6/2026 9:56:22 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=10/6/2026 10:04:28 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:27 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4588 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/6/2026 10:04:29 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:28 +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=10/6/2026 10:04:33 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:28 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}} +NextID : 4 +Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -1214,9 +1217,9 @@ Get-PiHoleLogDnsmasq -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/1/2026 8:28:22 AM; Message=started, version pi-hole-v2.93 cachesize 10000; Priority=}, @{Timestamp=10/1/2026 8:28:22 AM; Message=DNS service limited to local subnets; Priority=}, @{Timestamp=10/1/2026 8:28:22 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=10/1/2026 8:28:22 AM; Message=using nameserver 8.8.8.8#53; Priority=}…} -NextID : 69 -Pid : 388 +Log : {@{Timestamp=10/6/2026 9:56:22 PM; Message=started, version pi-hole-v2.93 cachesize 10000; Priority=}, @{Timestamp=10/6/2026 9:56:22 PM; Message=DNS service limited to local subnets; Priority=}, @{Timestamp=10/6/2026 9:56:22 PM; 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=10/6/2026 9:56:22 PM; Message=using nameserver 8.8.8.8#53; Priority=}…} +NextID : 21 +Pid : 389 File : /var/log/pihole/pihole.log ``` @@ -1228,9 +1231,9 @@ Get-PiHoleLogFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/1/2026 8:54:42 AM; Message=Wrote config file:; Priority=INFO}, @{Timestamp=10/1/2026 8:54:42 AM; Message= - 167 total entries; Priority=INFO}, @{Timestamp=10/1/2026 8:54:42 AM; Message= - 157 entries are default; Priority=INFO}, @{Timestamp=10/1/2026 8:54:42 AM; Message= - 10 entries are modified; Priority=INFO}…} -NextID : 797 -Pid : 388 +Log : {@{Timestamp=10/6/2026 9:56:21 PM; Message=PID of FTL process: 389; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=listening on 0.0.0.0 port 53; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=listening on :: port 53; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=Database version is 22; Priority=INFO}…} +NextID : 109 +Pid : 389 File : /var/log/pihole/FTL.log ``` @@ -1250,6 +1253,19 @@ RootFolder : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample FileSizeKB : 25 ``` +### Import-PiHoleTeleporter + +_Prompts for confirmation by default since it can overwrite your whole configuration - pass -Confirm:$false to skip the prompt, or -WhatIf for a dry run. Omitting every item switch (as here, Group is just one example) imports everything in the archive instead of only the selected items._ + +```powershell +Import-PiHoleTeleporter -PiHoleServer $PiHoleServer -Password $Password -FilePath "C:\Backups\pihole-backup.zip" -Group +``` + +``` + +Processed : {etc/pihole/gravity.db->group} +``` + ### Get-PiHoleHistory ```powershell @@ -1258,31 +1274,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/30/2026 3:15:00 PM +Timestamp : 10/5/2026 10:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/30/2026 3:25:00 PM +Timestamp : 10/5/2026 10:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/30/2026 3:35:00 PM +Timestamp : 10/5/2026 10:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/30/2026 3:45:00 PM +Timestamp : 10/5/2026 10:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/30/2026 3:55:00 PM +Timestamp : 10/5/2026 10:55:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1299,20 +1315,20 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/30/2026 3:15:00 PM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/5/2026 10:15:00 PM +Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/30/2026 3:25:00 PM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/5/2026 10:25:00 PM +Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/30/2026 3:35:00 PM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/5/2026 10:35:00 PM +Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/30/2026 3:45:00 PM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/5/2026 10:45:00 PM +Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/30/2026 3:55:00 PM -Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/5/2026 10:55:00 PM +Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ ``` @@ -1327,37 +1343,37 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/1/2026 9:00:00 AM -Total : 42 -Cached : 5 -Blocked : 10 -Forwarded : 27 +Timestamp : 10/6/2026 3:00:00 PM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 -Timestamp : 10/1/2026 10:00:00 AM -Total : 3 -Cached : 3 +Timestamp : 10/6/2026 4:00:00 PM +Total : 1 +Cached : 1 Blocked : 0 Forwarded : 0 -Timestamp : 10/1/2026 11:00:00 AM -Total : 3 -Cached : 3 +Timestamp : 10/6/2026 5:00:00 PM +Total : 1 +Cached : 1 Blocked : 0 Forwarded : 0 -Timestamp : 10/1/2026 12:00:00 PM +Timestamp : 10/6/2026 6:00:00 PM Total : 1 Cached : 1 Blocked : 0 Forwarded : 0 -Timestamp : 10/1/2026 1:00:00 PM +Timestamp : 10/6/2026 7:00:00 PM Total : 1 Cached : 1 Blocked : 0 Forwarded : 0 -_(showing 5 of 7 results)_ +_(showing 5 of 10 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1370,22 +1386,22 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/1/2026 3:00:00 PM -Clients : @{ClientId=3; Count=1} +Timestamp : 10/6/2026 10:00:00 PM +Clients : {@{ClientId=5; Count=1}, @{ClientId=3; Count=1}} -Timestamp : 10/1/2026 2:00:00 PM +Timestamp : 10/6/2026 9:50:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 10/1/2026 1:00:00 PM +Timestamp : 10/6/2026 9:40:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 10/1/2026 12:00:00 PM +Timestamp : 10/6/2026 9:00:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 10/1/2026 11:00:00 AM +Timestamp : 10/6/2026 8:00:00 PM Clients : @{ClientId=3; Count=1} -_(showing 5 of 7 results)_ +_(showing 5 of 10 results)_ ``` ### Get-PiHoleNetworkGateway @@ -1481,7 +1497,7 @@ ProtoDown : False Address : 00:00:00:00:00:00 Broadcast : 00:00:00:00:00:00 Stats : @{RxBytes=; TxBytes=; Bits=64} -Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1790649663.02; Tstamp=1790649663.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1790649663.02; Tstamp=1790649663.02}} +Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1790957600.02; Tstamp=1790957600.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1790957600.02; Tstamp=1790957600.02}} Name : wlan0 Speed : @@ -1494,7 +1510,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=63266; Valid=63266; Cstamp=1790649687.41; Tstamp=1790862827.1}, @{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=62449; Valid=62449; Cstamp=1790957620.44; Tstamp=1791318773.93}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1790957619.8; Tstamp=1790957619.8}} ``` ### Get-PiHoleNetworkDevice @@ -1510,20 +1526,20 @@ Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password Id : 2 HwAddr : ip-127.0.0.1 Interface : lo -FirstSeen : 10/1/2026 9:03:33 AM -LastQuery : 10/1/2026 3:00:00 PM -NumQueries : 13 +FirstSeen : 10/4/2026 2:31:10 PM +LastQuery : 10/6/2026 10:06:14 PM +NumQueries : 82 MacVendor : -Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=10/1/2026 3:19:00 PM; NameUpdated=10/1/2026 3:19:00 PM} +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=10/6/2026 10:12:00 PM; NameUpdated=10/6/2026 10:12:00 PM} Id : 1 HwAddr : 74:56:3c:bb:f4:0b Interface : wlan0 -FirstSeen : 10/1/2026 9:03:32 AM -LastQuery : 10/1/2026 9:03:33 AM -NumQueries : 39 +FirstSeen : 10/4/2026 2:31:09 PM +LastQuery : 10/6/2026 10:03:16 PM +NumQueries : 318 MacVendor : Giga-Byte Technology Co.,Ltd. -Ips : @{Ip=192.168.1.162; Name=; LastSeen=10/1/2026 3:19:00 PM; NameUpdated=12/31/1969 6:00:00 PM} +Ips : @{Ip=192.168.1.162; Name=; LastSeen=10/6/2026 10:12:00 PM; NameUpdated=12/31/1969 6:00:00 PM} ``` ### Remove-PiHoleNetworkDevice @@ -1576,8 +1592,8 @@ Get-PiHoleAuthStatus -PiHoleServer $PiHoleServer -Password $Password Valid : True Totp : False -Sid : 8sHZ0t+C5DwLRgIcKIM7/g= -Csrf : 1xmNq2FNUPFo7bnCRoUzIQ= +Sid : +dfslUHafhrdo0TwRT5nxQ= +Csrf : Em6pqkbJ6cuWxjQw7EtUFw= Validity : 1800 Message : correct password ``` @@ -1597,8 +1613,8 @@ Algorithm : SHA1 Digits : 6 Period : 30 Offset : 0 -Secret : XP4YEGF2S75KCDBN7BSW37AEA65AQZBQ -Codes : {414882, 273855, 226959, 897753…} +Secret : EG7DLOTZKTQWIRWBFR2JJLS5Q3BYNUN3 +Codes : {201435, 619358, 736784, 226002…} ``` ### Get-PiHoleCurrentAuthSession @@ -1610,13 +1626,41 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 10/6/2026 9:44:13 PM +LastActive : 10/6/2026 9:44:17 PM +ValidUntil : 10/6/2026 10:14:17 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 +XForwardedFor : +App : True +Cli : False + +Id : 1 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 10/6/2026 9:44:59 PM +LastActive : 10/6/2026 9:44:59 PM +ValidUntil : 10/6/2026 10:14:59 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 : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 10/1/2026 3:10:59 PM -LastActive : 10/1/2026 3:11:03 PM -ValidUntil : 10/1/2026 3:41:03 PM +LoginAt : 10/6/2026 10:03:33 PM +LastActive : 10/6/2026 10:03:37 PM +ValidUntil : 10/6/2026 10:33:37 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 : @@ -1629,13 +1673,13 @@ Cli : False _Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._ ```powershell -Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 0 +Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 2 ``` ``` -Id : 0 +Id : 2 Status : Removed ``` diff --git a/tests/Teleporter/Import-PiHoleTeleporter.Integration.Tests.ps1 b/tests/Teleporter/Import-PiHoleTeleporter.Integration.Tests.ps1 new file mode 100644 index 0000000..8bc9636 --- /dev/null +++ b/tests/Teleporter/Import-PiHoleTeleporter.Integration.Tests.ps1 @@ -0,0 +1,83 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: The "real import" test below selectively restores only the groups table from a backup +# taken of the server's own current state, so it's a no-op restore rather than a destructive +# change - safe to run repeatedly. Confirmed by hand that Pi-hole briefly reloads internally +# right after any import (a transient 401/connection error on the very next call), so this file +# settles with a short delay afterward rather than asserting anything about server state +# immediately following an import. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Import-PiHoleTeleporter (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()) 'PiHoleShellPesterTeleporterImport' + if (Test-Path $script:TestFolder) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + New-Item -ItemType Directory -Path $script:TestFolder | Out-Null + + # -ErrorAction Stop so a real failure here fails BeforeAll loudly instead of leaving + # $script:BackupFilePath silently $null (Get-PiHoleTeleporterDownload traps its own + # errors internally and just writes a non-terminating error otherwise). + $backup = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'import-fixture' -ErrorAction Stop + $script:BackupFilePath = $backup.FilePath + } + } + + AfterAll { + if ($script:TestFolder -and (Test-Path $script:TestFolder)) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + } + + It 'imports a selected item from a real backup and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Import-PiHoleTeleporter -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FilePath $script:BackupFilePath -Group -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Processed | Should -Not -BeNullOrEmpty + $result.Processed | Should -Match 'group' + + # Pi-hole reloads internally right after an import - give it a moment before the next test. + Start-Sleep -Seconds 5 + } + + It 'does not import anything when -WhatIf is set' -Skip:(-not $script:ConfigAvailable) { + { Import-PiHoleTeleporter -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FilePath $script:BackupFilePath -Group -WhatIf } | Should -Not -Throw + } + + It 'errors when the file does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Import-PiHoleTeleporter -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FilePath (Join-Path $script:TestFolder 'does-not-exist.zip') -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when the uploaded file is not a valid ZIP archive' -Skip:(-not $script:ConfigAvailable) { + $badFile = Join-Path $script:TestFolder 'not-a-zip.zip' + 'this is not a zip file' | Set-Content -Path $badFile + + $result = Import-PiHoleTeleporter -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FilePath $badFile -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Import-PiHoleTeleporter -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -FilePath $script:BackupFilePath -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index 865609d..35cb802 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -261,6 +261,18 @@ 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') + +# Restoring the archive just downloaded above is a no-op (it's a backup of the server's own +# current state), so this is safe to run for real rather than needing a throwaway target. +Add-Example -Category Config -FunctionName 'Import-PiHoleTeleporter' ` + -Invocation 'Import-PiHoleTeleporter -PiHoleServer $PiHoleServer -Password $Password -FilePath "C:\Backups\pihole-backup.zip" -Group' ` + -Note 'Prompts for confirmation by default since it can overwrite your whole configuration - pass -Confirm:$false to skip the prompt, or -WhatIf for a dry run. Omitting every item switch (as here, Group is just one example) imports everything in the archive instead of only the selected items.' ` + -Result (Import-PiHoleTeleporter -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -FilePath (Join-Path $teleporterFolder 'pihole-backup.zip') -Group -Confirm:$false) + +# Pi-hole reloads internally right after any import, briefly refusing connections - confirmed by +# hand against a real server. Without this, the very next example's own request can fail. +Start-Sleep -Seconds 5 + Remove-Item $teleporterFolder -Recurse -Force #endregion