diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index da20726..db7e9a3 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -24,7 +24,7 @@ Export-ModuleMember -Function @( #DnsControl 'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', ` #Config - 'Get-PiHoleConfig', ` + 'Get-PiHoleConfig', 'Set-PiHoleConfig', 'Add-PiHoleConfigArrayItem', 'Remove-PiHoleConfigArrayItem', 'Get-PiHoleConfigProperty', ` #Padd 'Get-PiHolePadd', ` #Metrics diff --git a/PiHoleShell/Private/Misc.ps1 b/PiHoleShell/Private/Misc.ps1 index a537078..3b0c02e 100644 --- a/PiHoleShell/Private/Misc.ps1 +++ b/PiHoleShell/Private/Misc.ps1 @@ -98,6 +98,44 @@ function ConvertTo-PiHolePascalCaseObject { } } +function ConvertTo-PiHoleFriendlyErrorMessage { + #INTERNAL FUNCTION + # + # Pi-hole's own error responses often carry a clearer message/hint than the generic HTTP + # exception text (e.g. "Unable to change configuration (read-only): ...app_sudo is false" + # vs just "403 Forbidden"). This extracts and combines them when present, and adds a + # concrete pointer to fix the most common cause of a blocked config write - the app + # password's app_sudo setting - since Pi-hole's own hint says what's wrong but not how to + # fix it. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Falls back to the raw exception message when the response body isn't valid JSON - nothing to report.")] + param ( + [Parameter(Mandatory = $true)] + $ErrorRecord + ) + + $Message = $ErrorRecord.Exception.Message + + if ($ErrorRecord.ErrorDetails.Message) { + try { + $ApiError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).error + if ($ApiError.message) { + $Message = $ApiError.message + if ($ApiError.hint) { + $Message += ": $($ApiError.hint)" + } + if ($ApiError.hint -like '*app_sudo*') { + $Message += " Enable it in your Pi-hole admin UI under Settings > All Settings by searching for 'app_sudo' and setting webserver.api.app_sudo to true." + } + } + } + catch { + # ErrorDetails.Message wasn't valid JSON - fall back to the raw exception message + } + } + + return $Message +} + function Remove-PiHoleCurrentAuthSession { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")] [CmdletBinding()] diff --git a/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..2d31bc9 --- /dev/null +++ b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,101 @@ +function Add-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Add config array item + +.DESCRIPTION +Adds one item to an array-type Pi-hole configuration setting - for example, an upstream DNS +server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record (dns/cnameRecords). +Use Set-PiHoleConfig instead for non-array settings. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Element +The array-type configuration setting to add to, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The item to add. For dns/hosts this is " "; for dns/cnameRecords this is +",[,]" + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when adding several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Add-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/config/-element-/-value-')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Put" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + # A successful add returns 201 Created with no body, so there's no response to + # build a rich object from. + $Object = [PSCustomObject]@{ + Element = $Element + Value = $Value + Status = "Added" + } + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 index 12cb171..f6fe735 100644 --- a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 +++ b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 @@ -7,7 +7,8 @@ Get current configuration of Pi-hole Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver, files, misc, and debug settings). The formatted output mirrors the API response as nested objects with PascalCase property names, so the entire configuration is available for -inspection rather than a hand-picked subset. +inspection rather than a hand-picked subset. Pass -Element to request just one subset of +the tree instead of everything. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -15,6 +16,13 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h .PARAMETER Password The API Password you generated from your PiHole server +.PARAMETER Element +Only return this part of the configuration tree, as a slash-separated path (e.g. +"dns/upstreams" or "dns/hosts"). Omit to return the entire configuration + +.PARAMETER Detailed +Include detailed information about the configuration (e.g. value types and validation info) + .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,6 +34,9 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a .EXAMPLE (Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams + +.EXAMPLE +Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/upstreams" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -34,15 +45,26 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, + [string]$Element, + [Nullable[bool]]$Detailed, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($Element) { + $Uri += "/$($Element.Trim('/'))" + } + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/config" + Uri = $Uri Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 new file mode 100644 index 0000000..f9f6d50 --- /dev/null +++ b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 @@ -0,0 +1,73 @@ +function Get-PiHoleConfigProperty { + <# +.SYNOPSIS +Get special properties of your Pi-hole configuration + +.DESCRIPTION +Returns the configuration properties that cannot be changed through the API (e.g. because +they're only settable in pihole.toml, or are controlled by an environment variable), along with +why each one is restricted. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleConfigProperty -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config/_properties')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/config/_properties" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $ObjectFinal = foreach ($Item in $Response.config.read_only) { + [PSCustomObject]@{ + Key = $Item.key + Reason = $Item.reason + Description = $Item.description + } + } + Write-Output $ObjectFinal + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..f197ea8 --- /dev/null +++ b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,103 @@ +function Remove-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Delete config array item + +.DESCRIPTION +Removes one item from an array-type Pi-hole configuration setting - for example, an upstream +DNS server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record +(dns/cnameRecords). + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Element +The array-type configuration setting to remove from, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The exact item to remove, as it appears in the array (see Get-PiHoleConfig) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when removing several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/config/-element-/-value-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Target = "Pi-Hole config item $Value in $Element" + if ($PSCmdlet.ShouldProcess($Target, "Remove config array item")) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Delete" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + + else { + # A successful delete returns 204 No Content, so there's no response body to + # build a rich object from. + $Object = [PSCustomObject]@{ + Element = $Element + Value = $Value + Status = "Removed" + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 new file mode 100644 index 0000000..2c4eaf0 --- /dev/null +++ b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 @@ -0,0 +1,97 @@ +function Set-PiHoleConfig { + <# +.SYNOPSIS +Change configuration of your Pi-hole + +.DESCRIPTION +Updates one or more Pi-hole configuration settings in a single call. -Settings takes a +hashtable shaped like the configuration tree returned by Get-PiHoleConfig - only include the +properties you want to change; everything else is left untouched. Some changes require FTL to +restart to take effect; this happens automatically unless -Restart is set to $false. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. Some settings can never be +changed via the API at all (only in pihole.toml, or via an environment variable) - see +Get-PiHoleConfigProperty for the current list. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Settings +A hashtable of the configuration properties to change, shaped like the tree returned by +Get-PiHoleConfig (e.g. @{ dns = @{ CNAMEdeepInspect = $true } }) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when making several changes independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Set-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Settings @{ dns = @{ CNAMEdeepInspect = $true } } + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#patch-/config')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [hashtable]$Settings, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Body = @{ + config = $Settings + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Patch" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.config + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/README.md b/README.md index d1cf8e7..798ebf2 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,9 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | Function | Description | |---|---| +| `Add-PiHoleConfigArrayItem` | Add config array item | | `Get-PiHoleConfig` | Get current configuration of Pi-hole | +| `Get-PiHoleConfigProperty` | Get special properties of your Pi-hole configuration | | `Get-PiHoleHistory` | Get activity graph data | | `Get-PiHoleHistoryClient` | Get per-client activity graph data | | `Get-PiHoleHistoryDatabase` | Get activity graph data (long-term data) | @@ -184,8 +186,10 @@ 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 | +| `Remove-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | | `Remove-PiHoleNetworkDevice` | Delete a device from the network table | +| `Set-PiHoleConfig` | Change configuration of your Pi-hole | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 1cc2273..7bb7f63 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -99,9 +99,9 @@ New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHo Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 4 -DateAdded : 9/27/2026 11:54:34 AM -DateModified : 9/27/2026 11:54:34 AM +Id : 17 +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Get-PiHoleGroup @@ -136,9 +136,9 @@ DateModified : 7/1/2025 10:13:40 PM Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 4 -DateAdded : 9/27/2026 11:54:34 AM -DateModified : 9/27/2026 11:54:34 AM +Id : 17 +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Update-PiHoleGroup @@ -152,9 +152,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False -Id : 4 -DateAdded : 9/27/2026 11:54:34 AM -DateModified : 9/27/2026 11:55:06 AM +Id : 17 +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:06:06 PM ``` ### Remove-PiHoleGroup @@ -183,9 +183,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : True -Id : 65 -DateAdded : 9/27/2026 11:56:17 AM -DateModified : 9/27/2026 11:56:17 AM +Id : 79 +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:06:43 PM Type : Block DateUpdated : Number : 0 @@ -287,9 +287,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : False -Id : 65 -DateAdded : 9/27/2026 11:56:17 AM -DateModified : 9/27/2026 11:57:19 AM +Id : 79 +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:07:17 PM Type : Block DateUpdated : Number : 0 @@ -414,9 +414,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : True -Id : 12 -DateAdded : 9/27/2026 11:58:32 AM -DateModified : 9/27/2026 11:58:32 AM +Id : 16 +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Get-PiHoleDomain @@ -434,20 +434,9 @@ Kind : exact Comment : Example domain Groups : {Default} Enabled : True -Id : 12 -DateAdded : 9/27/2026 11:58:32 AM -DateModified : 9/27/2026 11:58:32 AM - -Domain : piholeshell-test-domain.example.com -Unicode : piholeshell-test-domain.example.com -Type : allow -Kind : exact -Comment : -Groups : {Default} -Enabled : True -Id : 13 -DateAdded : 9/27/2026 11:58:40 AM -DateModified : 9/27/2026 11:58:40 AM +Id : 16 +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Update-PiHoleDomain @@ -465,9 +454,9 @@ Kind : Exact Comment : Example domain Groups : {Default} Enabled : False -Id : 12 -DateAdded : 9/27/2026 11:58:32 AM -DateModified : 9/27/2026 11:59:34 AM +Id : 16 +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:08:28 PM ``` ### Remove-PiHoleDomain @@ -498,9 +487,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 1 -DateAdded : 9/27/2026 12:00:33 PM -DateModified : 9/27/2026 12:00:33 PM +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM ``` ### Get-PiHoleClient @@ -515,9 +504,9 @@ Client : 192.168.99.99 Name : Comment : Example client Groups : {Default} -Id : 1 -DateAdded : 9/27/2026 12:00:33 PM -DateModified : 9/27/2026 12:00:33 PM +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM ``` ### Get-PiHoleClientSuggestion @@ -527,7 +516,18 @@ Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +HwAddr : ip-127.0.0.1 +MacVendor : +LastQuery : 9/28/2026 3:00:00 PM +Addresses : 127.0.0.1 +Names : localhost + +HwAddr : 74:56:3c:bb:f4:0b +MacVendor : Giga-Byte Technology Co.,Ltd. +LastQuery : 9/27/2026 5:34:26 PM +Addresses : 192.168.1.162 +Names : ``` ### Update-PiHoleClient @@ -542,9 +542,9 @@ Client : 192.168.99.99 Name : Comment : Updated comment Groups : {Default} -Id : 1 -DateAdded : 9/27/2026 12:00:33 PM -DateModified : 9/27/2026 12:01:45 PM +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:09:37 PM ``` ### Remove-PiHoleClient @@ -569,18 +569,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 0 -Blocked : 0 -PercentBlocked : 0 -UniqueDomains : 0 -Forwarded : 0 -Cached : 0 +Total : 299 +Blocked : 103 +PercentBlocked : 34.4481620788574 +UniqueDomains : 22 +Forwarded : 39 +Cached : 157 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=496171; LastUpdate=1790527593} +Types : @{A=134; AAAA=133; ANY=0; SRV=0; SOA=0; PTR=32; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=103; Forwarded=39; Cache=127; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=30; ExternalBlockedEde15=0} +Replies : @{Unknown=13; Nodata=24; Nxdomain=3; Cname=0; Ip=230; Domain=29; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Clients : @{Active=2; Total=2} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790545835} ``` ### Get-PiHoleStatsRecentBlocked @@ -590,7 +590,8 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Blocked : googlesyndication.com ``` ### Get-PiHoleStatsQueryType @@ -602,10 +603,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 0 +Count : 134 Type : AAAA -Count : 0 +Count : 133 Type : ANY Count : 0 @@ -626,7 +627,23 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Domain : bbc.com +Count : 23 + +Domain : 1.0.0.127.in-addr.arpa +Count : 23 + +Domain : nytimes.com +Count : 20 + +Domain : cloudflare.com +Count : 16 + +Domain : github.com +Count : 15 + +_(showing 5 of 10 results)_ ``` ### Get-PiHoleStatsTopClient @@ -636,7 +653,14 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +IP : 192.168.1.162 +Name : +Count : 267 + +IP : 127.0.0.1 +Name : localhost +Count : 32 ``` ### Get-PiHoleStatsUpstream @@ -647,9 +671,9 @@ 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}} +TotalQueries : 299 +ForwardedQueries : 39 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=103; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=157; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=21; ResponseTime=0.0285110175609589; Variance=0.00743893922252483}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=18; ResponseTime=0.0329826772212982; Variance=0.00822325505214882}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -660,10 +684,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {} -ClientIp : {} -ClientName : {} -Upstream : {blocklist, cache, permitted} +Domain : {bbc.com, 1.0.0.127.in-addr.arpa, nytimes.com, cloudflare.com…} +ClientIp : {192.168.1.162, 127.0.0.1} +ClientName : {localhost} +Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), 8.8.4.4#53 (dns.google)…} Type : {A, AAAA, ANY, SRV…} Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} @@ -680,10 +704,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 0 +SumQueries : 8 SumBlocked : 0 PercentBlocked : 0 -TotalClients : 0 +TotalClients : 1 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -709,7 +733,7 @@ Type : SRV Count : 0 Type : SOA -Count : 0 +Count : 8 _(showing 5 of 16 results)_ ``` @@ -723,7 +747,9 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Domain : 1.0.0.127.in-addr.arpa +Count : 8 ``` ### Get-PiHoleStatsDatabaseTopClient @@ -735,7 +761,10 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +IP : 127.0.0.1 +Name : localhost +Count : 8 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -748,9 +777,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 0 +TotalQueries : 8 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}} +Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}} ``` ## Configuration & Diagnostics @@ -774,6 +803,64 @@ Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsma Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=False; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} ``` +### Get-PiHoleConfigProperty + +```powershell +Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Key : misc.readOnly +Reason : read_only +Description : Variable can only be set in pihole.toml, not via API +``` + +### Set-PiHoleConfig + +```powershell +Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } } +``` + +``` + +Dns : @{Upstreams=System.Object[]; CNAMEdeepInspect=True; BlockESNI=True; EDNS0ECS=True; IgnoreLocalhost=False; ShowDNSSEC=True; AnalyzeOnlyAandAAAA=False; PiholePTR=PI.HOLE; ReplyWhenBusy=ALLOW; BlockTTL=2; Hosts=System.Object[]; DomainNeeded=False; ExpandHosts=False; BogusPriv=True; Dnssec=False; Interface=wlan0; HostRecord=; ListeningMode=LOCAL; QueryLogging=False; CnameRecords=; Port=53; Localise=True; RevServers=; Domain=; Cache=; Blocking=; SpecialDomains=; Reply=; RateLimit=} +Dhcp : @{Active=False; Start=; End=; Router=; Netmask=; LeaseTime=; Ipv6=False; RapidCommit=False; MultiDNS=False; Logging=False; IgnoreUnknownClients=False; Hosts=} +Ntp : @{Ipv4=; Ipv6=; Sync=} +Resolver : @{ResolveIPv4=True; ResolveIPv6=True; MacNames=True; NetworkNames=True; RefreshNames=IPV4_ONLY} +Database : @{DBimport=True; MaxDBdays=2; DBinterval=60; UseWAL=True; ForceDisk=False; Network=} +Webserver : @{Domain=pi.hole; Acl=; Port=8089,8489s; Threads=50; Headers=System.Object[]; ServeAll=False; AdvancedOpts=; Session=; Tls=; Paths=; Interface=; Api=} +Files : @{Database=/etc/pihole/pihole-FTL.db; TmpDb=/etc/pihole/pihole-tmp.db; Gravity=/etc/pihole/gravity.db; GravityTmp=/tmp; Macvendor=/etc/pihole/macvendor.db; Pcap=; Log=} +Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsmasqD=False; DnsmasqLines=; ExtraLogging=False; ReadOnly=False; NormalizeCPU=True; HideDnsmasqWarn=False; HideConnectionError=False; Check=} +Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=True; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} +``` + +### Add-PiHoleConfigArrayItem + +```powershell +Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Added +``` + +### Remove-PiHoleConfigArrayItem + +```powershell +Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Removed +``` + ### Get-PiHolePadd ```powershell @@ -784,22 +871,22 @@ Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password CpuPercent : 0 MemoryPercent : 0 -ActiveClients : 0 +ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=0; Evicted=0} +Cache : @{Size=10000; Inserted=131; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} GravitySize : 496171 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 389 -Queries : @{Total=0; Blocked=0; PercentBlocked=0; QueryFrequency=0} -RecentBlocked : -Sensors : @{CpuTemp=40.084; HotLimit=60; Unit=C} -System : @{Uptime=644738; Memory=; Procs=71; Cpu=; Ftl=} -TopBlocked : -TopClient : -TopDomain : +Queries : @{Total=299; Blocked=103; PercentBlocked=34.4481620788574; QueryFrequency=0} +RecentBlocked : googlesyndication.com +Sensors : @{CpuTemp=39.008; HotLimit=60; Unit=C} +System : @{Uptime=742734; Memory=; Procs=72; Cpu=; Ftl=} +TopBlocked : googleadservices.com +TopClient : 192.168.1.162 +TopDomain : bbc.com Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -836,11 +923,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 644757 +Uptime : 742744 Memory : @{Ram=; Swap=} -Procs : 71 -Cpu : @{NumProcessors=1; PercentCpu=100; Load=} -Ftl : @{PercentMemory=2.33196616172791; PercentCpu=99.0999984741211} +Procs : 72 +Cpu : @{NumProcessors=1; PercentCpu=96.9000015258789; Load=} +Ftl : @{PercentMemory=3.11284995079041; PercentCpu=96.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -854,13 +941,13 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password Database : @{Gravity=496171; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 -Clients : @{Total=0; Active=0} +Clients : @{Total=2; Active=2} Pid : 389 -Uptime : 495351.278596 -PercentMemory : 2.32703423500061 -PercentCpu : 99.3000030517578 +Uptime : 80190954.666427 +PercentMemory : 3.11284995079041 +PercentCpu : 96.3000030517578 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} +Dnsmasq : @{DnsCacheInserted=131; DnsCacheLiveFreed=0; DnsQueriesForwarded=69; DnsAuthAnswered=0; DnsLocalAnswered=230; DnsStaleAnswered=30; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} ``` ### Get-PiHoleInfoSensors @@ -872,7 +959,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password ``` List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}} -CpuTemp : 40.622 +CpuTemp : 40.084 HotLimit : 60 Unit : C ``` @@ -889,12 +976,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/27/2026 11:44:09 AM -ChangeTime : 9/27/2026 11:44:09 AM +ModifiedTime : 9/28/2026 2:23:00 PM +ChangeTime : 9/28/2026 2:23:00 PM Owner : @{User=; Group=} -Queries : 0 -EarliestTimestamp : 9/26/2026 11:50:00 AM -QueriesDisk : 0 +Queries : 299 +EarliestTimestamp : 9/27/2026 3:00:00 PM +QueriesDisk : 299 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -964,10 +1051,10 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` Id : 1 -Timestamp : 9/27/2026 11:49:11 AM +Timestamp : 9/27/2026 5:33:13 PM Type : LOAD -Plain : Long-term load (15min avg) larger than number of processors: 1.3 > 1 -Html : Long-term load (15min avg) larger than number of processors: 1.3 > 1
This may slow down DNS resolution and can cause bottlenecks. +Plain : Long-term load (15min avg) larger than number of processors: 1.1 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.1 > 1
This may slow down DNS resolution and can cause bottlenecks. ``` ### Get-PiHoleInfoMessageCount @@ -1002,8 +1089,8 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : @{Timestamp=9/27/2026 11:44:11 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} -NextID : 1 +Log : {@{Timestamp=9/27/2026 4:48:13 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/28/2026 3:04:02 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:01 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:03 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:07 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}} +NextID : 4 Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -1021,7 +1108,7 @@ Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -Fo FileName : pihole-backup.zip FilePath : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample\pihole-backup.zip RootFolder : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample -FileSizeKB : 24 +FileSizeKB : 25 ``` ### Get-PiHoleHistory @@ -1032,31 +1119,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 12:05:00 PM +Timestamp : 9/27/2026 3:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 12:15:00 PM +Timestamp : 9/27/2026 3:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 12:25:00 PM +Timestamp : 9/27/2026 3:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 12:35:00 PM +Timestamp : 9/27/2026 3:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 12:45:00 PM +Timestamp : 9/27/2026 3:55:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1073,20 +1160,20 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 12:05:00 PM -Clients : @{IP=others; Name=; Count=0} +Timestamp : 9/27/2026 3:15:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 12:15:00 PM -Clients : @{IP=others; Name=; Count=0} +Timestamp : 9/27/2026 3:25:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 12:25:00 PM -Clients : @{IP=others; Name=; Count=0} +Timestamp : 9/27/2026 3:35:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 12:35:00 PM -Clients : @{IP=others; Name=; Count=0} +Timestamp : 9/27/2026 3:45:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 12:45:00 PM -Clients : @{IP=others; Name=; Count=0} +Timestamp : 9/27/2026 3:55:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ ``` @@ -1100,7 +1187,38 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Timestamp : 9/28/2026 8:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 9:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 10:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 11:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 12:00:00 PM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +_(showing 5 of 8 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1112,7 +1230,23 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Timestamp : 9/28/2026 3:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 2:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 1:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 12:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 11:00:00 AM +Clients : @{ClientId=3; Count=1} + +_(showing 5 of 8 results)_ ``` ### Get-PiHoleNetworkGateway @@ -1208,7 +1342,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=1789883183.02; Tstamp=1789883183.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883183.02; Tstamp=1789883183.02}} +Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1789883140.02; Tstamp=1789883140.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883140.02; Tstamp=1789883140.02}} Name : wlan0 Speed : @@ -1221,7 +1355,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=61931; Valid=61931; Cstamp=1789883203.32; Tstamp=1790074734.89}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883202.81; Tstamp=1789883202.81}} +Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=65600; Valid=65600; Cstamp=1789883160.32; Tstamp=1790175981.82}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883159.81; Tstamp=1789883159.81}} ``` ### Get-PiHoleNetworkDevice @@ -1233,7 +1367,24 @@ Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Id : 2 +HwAddr : ip-127.0.0.1 +Interface : lo +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/28/2026 3:00:00 PM +NumQueries : 32 +MacVendor : +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=9/28/2026 3:11:00 PM} + +Id : 1 +HwAddr : 74:56:3c:bb:f4:0b +Interface : wlan0 +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/27/2026 5:34:26 PM +NumQueries : 267 +MacVendor : Giga-Byte Technology Co.,Ltd. +Ips : @{Ip=192.168.1.162; Name=; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=12/31/1969 6:00:00 PM} ``` ### Remove-PiHoleNetworkDevice @@ -1260,41 +1411,13 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 -CurrentSession : False -Valid : True -TlsLogin : True -TlsMixed : False -LoginAt : 9/27/2026 11:50:34 AM -LastActive : 9/27/2026 11:50:34 AM -ValidUntil : 9/27/2026 12:20:34 PM -RemoteAddress : 192.168.1.162 -UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 -XForwardedFor : -App : True -Cli : False - -Id : 1 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/27/2026 11:50:38 AM -LastActive : 9/27/2026 11:50:47 AM -ValidUntil : 9/27/2026 12:20:47 PM -RemoteAddress : 192.168.1.162 -UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 -XForwardedFor : -App : True -Cli : False - -Id : 2 -CurrentSession : False -Valid : True -TlsLogin : True -TlsMixed : False -LoginAt : 9/27/2026 11:50:39 AM -LastActive : 9/27/2026 11:50:39 AM -ValidUntil : 9/27/2026 12:20:39 PM +LoginAt : 9/28/2026 3:03:07 PM +LastActive : 9/28/2026 3:03:11 PM +ValidUntil : 9/28/2026 3:33:11 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : diff --git a/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..046d999 --- /dev/null +++ b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,68 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Add-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds an item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain $script:TestValue + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when the item already exists' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 index 4461c04..8ef8b5d 100644 --- a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 +++ b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 @@ -28,6 +28,15 @@ Describe 'Get-PiHoleConfig (Integration)' -Tag 'Integration' { $result.Dhcp | Should -Not -BeNullOrEmpty } + It 'filters to just one element when -Element is specified' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/upstreams' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Dns.Upstreams | Should -Not -BeNullOrEmpty + $result.Dhcp | Should -BeNullOrEmpty + } + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true $result | Format-List | Out-String | Write-Host diff --git a/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 new file mode 100644 index 0000000..2522f3b --- /dev/null +++ b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 @@ -0,0 +1,43 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleConfigProperty (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns read-only properties as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + # misc.readOnly is documented as always read-only, so it's a stable thing to assert on. + $result | Should -Not -BeNullOrEmpty + $result.Key | Should -Contain 'misc.readOnly' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..5f9c24e --- /dev/null +++ b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,76 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an existing item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Removed' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'removes the item when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here - + # the item actually being gone afterward is the real signal of success. + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -RawOutput $true -Confirm:$false + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'errors when the item does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 new file mode 100644 index 0000000..2e14527 --- /dev/null +++ b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 @@ -0,0 +1,69 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests change and restore a genuinely harmless debug flag (debug.api) rather than +# anything that affects DNS resolution or other real behavior. They require the app password +# used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same as the +# functions themselves - see their .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Set-PiHoleConfig (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'changes a setting and restores it, without restarting FTL' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Debug.Api | Should -Be (-not $original) + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.config | Should -Not -BeNullOrEmpty + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'errors and gives a clear reason when the property can only be set in pihole.toml' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ misc = @{ readOnly = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + $errOut[-1] | Should -Match 'pihole.toml' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index 0f55591..6423ee8 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -156,6 +156,48 @@ Add-Example -Category Config -FunctionName 'Get-PiHoleConfig' ` -Invocation 'Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +Add-Example -Category Config -FunctionName 'Get-PiHoleConfigProperty' ` + -Invocation 'Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +# Set-PiHoleConfig/Add-PiHoleConfigArrayItem/Remove-PiHoleConfigArrayItem all require the app +# password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings) - Pi-hole +# blocks config changes from app passwords by default. Skip these three with a placeholder if +# that isn't the case, rather than failing the whole run. +$appSudoEnabled = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Webserver.Api.AppSudo +if ($appSudoEnabled) { + $originalDebugApi = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Debug.Api + $toggledDebugApi = -not $originalDebugApi + + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation "Set-PiHoleConfig -PiHoleServer `$PiHoleServer -Password `$Password -Settings @{ debug = @{ api = `$$($toggledDebugApi.ToString().ToLower()) } }" ` + -Result (Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $toggledDebugApi } } -Restart $false) + Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $originalDebugApi } } -Restart $false | Out-Null + + $docsHostEntry = '192.0.2.1 piholeshell-docs-example.com' + Invoke-Quietly { Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false } + + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation "Add-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false) + + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation "Remove-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false) +} +else { + $appSudoNote = 'Requires "app_sudo" enabled for this app password (Settings > All Settings in your Pi-hole admin UI) - not captured this run since it is disabled here.' + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation 'Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } }' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation 'Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation 'Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped +} + Add-Example -Category Config -FunctionName 'Get-PiHolePadd' ` -Invocation 'Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)