Compare commits

..

2 Commits

Author SHA1 Message Date
Christian König
f3183dc95f Source directly, no intermediate variables
Some checks are pending
CodeQL / Analyze (pull_request) Waiting to run
Test Supported Distributions / smoke-tests (pull_request) Waiting to run
Test Supported Distributions / distro-test (centos_10) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (centos_9) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (debian_11) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (debian_12) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (fedora_40) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (fedora_41) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (fedora_42) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (ubuntu_20) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (ubuntu_22) (pull_request) Blocked by required conditions
Test Supported Distributions / distro-test (ubuntu_24) (pull_request) Blocked by required conditions
Signed-off-by: Christian König <github@yubiuser.dev>
2025-06-01 15:10:14 +02:00
Christian König
18d4aef37c Add forgotten PI_HOLE_FILES_DIR
Signed-off-by: Christian König <github@yubiuser.dev>
2025-05-31 08:49:01 +02:00
69 changed files with 1619 additions and 1740 deletions

View File

@@ -8,10 +8,6 @@ updates:
time: "10:00"
open-pull-requests-limit: 10
target-branch: development
groups:
github-actions-dependencies:
patterns:
- "*"
- package-ecosystem: pip
directory: "/test"
schedule:
@@ -20,7 +16,3 @@ updates:
time: "10:00"
open-pull-requests-limit: 10
target-branch: development
groups:
python-dependencies:
patterns:
- "*"

1
.github/release.yml vendored
View File

@@ -2,7 +2,6 @@ changelog:
exclude:
labels:
- internal
- dependencies
authors:
- dependabot
- github-actions

View File

@@ -25,16 +25,16 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
uses: actions/checkout@v4.2.2
# Initializes the CodeQL tools for scanning.
-
name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 #v4.32.6
uses: github/codeql-action/init@v3
with:
languages: 'python'
-
name: Autobuild
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 #v4.32.6
uses: github/codeql-action/autobuild@v3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 #v4.32.6
uses: github/codeql-action/analyze@v3

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check if PRs are have merge conflicts
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 #v3.0.3
uses: eps1lon/actions-label-merge-conflict@v3.0.3
with:
dirtyLabel: "PR: Merge Conflict"
repoToken: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -17,14 +17,14 @@ jobs:
issues: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0
- uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
days-before-close: 5
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.'
stale-issue-label: '${{ env.stale_label }}'
exempt-issue-labels: 'Internal, Fixed in next release, Bug: Confirmed, Documentation Needed, never-stale'
exempt-issue-labels: 'Internal, Fixed in next release, Bug: Confirmed, Documentation Needed'
exempt-all-issue-assignees: true
operations-per-run: 300
close-issue-reason: 'not_planned'
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
uses: actions/checkout@v4.2.2
- name: Remove 'stale' label
run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }}
env:

View File

@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0
- uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Do not automatically mark PR/issue as stale

View File

@@ -33,7 +33,7 @@ jobs:
name: Syncing branches
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
uses: actions/checkout@v4.2.2
- name: Opening pull request
run: gh pr create -B development -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal'
env:

View File

@@ -7,42 +7,53 @@ on:
permissions:
contents: read
env:
FORCE_COLOR: 1
PYTHONUNBUFFERED: 1
PYTHONUTF8: 1
jobs:
smoke-tests:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Differential ShellCheck requires full git history
- name: Check scripts in repository are executable
run: |
IFS=$'\n';
for f in $(find . -name '*.sh' -o -name '*.bats'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done
for f in $(find . -name '*.sh'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done
unset IFS;
# If FAIL is 1 then we fail.
[[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!"
- name: Differential ShellCheck
if: github.event_name == 'pull_request'
uses: redhat-plumbers-in-action/differential-shellcheck@d965e66ec0b3b2f821f75c8eff9b12442d9a7d1e #v5.5.6
uses: redhat-plumbers-in-action/differential-shellcheck@v5
with:
severity: warning
display-engine: sarif-fmt
- name: Spell-Checking
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 #v2.2
uses: codespell-project/actions-codespell@master
with:
ignore_words_file: .codespellignore
- name: Get editorconfig-checker
uses: editorconfig-checker/action-editorconfig-checker@4b6cd6190d435e7e084fb35e36a096e98506f7b9 #v2.1.0
uses: editorconfig-checker/action-editorconfig-checker@main # tag v1.0.0 is really out of date
- name: Run editorconfig-checker
run: editorconfig-checker
- name: Check python code formatting with black
uses: psf/black@stable
with:
src: "./test"
options: "--check --diff --color"
distro-test:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
@@ -54,7 +65,6 @@ jobs:
[
debian_11,
debian_12,
debian_13,
ubuntu_20,
ubuntu_22,
ubuntu_24,
@@ -63,14 +73,23 @@ jobs:
fedora_40,
fedora_41,
fedora_42,
fedora_43,
alpine_3_21,
alpine_3_22,
alpine_3_23,
]
env:
DISTRO: ${{matrix.distro}}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
uses: actions/checkout@v4.2.2
- name: Run BATS test suite for ${{ matrix.distro }}
run: DISTRO=${{ matrix.distro }} bash test/run.sh
- name: Set up Python 3.10
uses: actions/setup-python@v5.6.0
with:
python-version: "3.10"
- name: Install wheel
run: pip install wheel
- name: Install dependencies
run: pip install -r test/requirements.txt
- name: Test with tox
run: tox -c test/tox.${DISTRO}.ini

10
.gitignore vendored
View File

@@ -1,7 +1,15 @@
.DS_Store
*.pyc
*.swp
__pycache__
.cache
.pytest_cache
.tox
.eggs
*.egg-info
.idea/
*.iml
.vscode/
.venv/
.fleet/
test/libs/
.cache/

View File

@@ -1,6 +1,2 @@
external-sources=true # allow shellcheck to read external sources
disable=SC3043 #disable SC3043: In POSIX sh, local is undefined.
enable=useless-use-of-cat # disabled by default as of shellcheck 0.11.0
enable=avoid-negated-conditions # avoid-negated-conditions is optional as of shellcheck 0.11.0
enable=require-variable-braces
enable=deprecate-which

View File

@@ -1,12 +1,11 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034 # Disable warning about unused variables
# Determine if terminal is capable of showing colors
# When COL_TABLE is sourced via gravity invoked by FTL, FORCE_COLOR is set to true
if { [ -t 1 ] && [ "$(tput colors)" -ge 8 ]; } || [ "${FORCE_COLOR}" ]; then
if [ -t 1 ] && [ "$(tput colors)" -ge 8 ]; then
# Bold and underline may not show up on all clients
# If something MUST be emphasized, use both
COL_BOLD=''
COL_ULINE=''
COL_NC=''
COL_GRAY=''
COL_RED=''
@@ -18,6 +17,8 @@ if { [ -t 1 ] && [ "$(tput colors)" -ge 8 ]; } || [ "${FORCE_COLOR}" ]; then
else
# Provide empty variables for `set -u`
COL_BOLD=""
COL_ULINE=""
COL_NC=""
COL_GRAY=""
COL_RED=""
@@ -28,8 +29,22 @@ else
COL_CYAN=""
fi
# Deprecated variables
COL_WHITE="${COL_BOLD}"
COL_BLACK="${COL_NC}"
COL_LIGHT_BLUE="${COL_BLUE}"
COL_LIGHT_GREEN="${COL_GREEN}"
COL_LIGHT_CYAN="${COL_CYAN}"
COL_LIGHT_RED="${COL_RED}"
COL_URG_RED="${COL_RED}${COL_BOLD}${COL_ULINE}"
COL_LIGHT_PURPLE="${COL_PURPLE}"
COL_BROWN="${COL_YELLOW}"
COL_LIGHT_GRAY="${COL_GRAY}"
COL_DARK_GRAY="${COL_GRAY}"
TICK="[${COL_GREEN}✓${COL_NC}]"
CROSS="[${COL_RED}✗${COL_NC}]"
INFO="[i]"
QST="[?]"
DONE="${COL_GREEN} done!${COL_NC}"
OVER="\\r"

View File

@@ -22,10 +22,8 @@ TestAPIAvailability() {
local chaos_api_list authResponse authStatus authData apiAvailable DNSport
# as we are running locally, we can get the port value from FTL directly
PI_HOLE_SCRIPT_DIR="/opt/pihole"
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source=./advanced/Scripts/utils.sh
. "${utilsfile}"
. "/opt/pihole/utils.sh"
DNSport=$(getFTLConfigValue dns.port)
@@ -150,6 +148,7 @@ LoginAPI() {
# Try to login again until the session is valid
while [ ! "${validSession}" = true ] ; do
echo "Authentication failed. Please enter your Pi-hole password"
# Print the error message if there is one
if [ ! "${sessionError}" = "null" ] && [ "${1}" = "verbose" ]; then
@@ -160,14 +159,6 @@ LoginAPI() {
echo "Error: ${sessionMessage}"
fi
if [ "${1}" = "verbose" ]; then
# If we are not in verbose mode, no need to print the error message again
echo "Please enter your Pi-hole password"
else
echo "Authentication failed. Please enter your Pi-hole password"
fi
# secretly read the password
secretRead; printf '\n'
@@ -190,20 +181,13 @@ Authentication() {
echo "No response from FTL server. Please check connectivity"
exit 1
fi
# obtain validity, session ID and sessionMessage from session response
validSession=$(echo "${sessionResponse}"| jq .session.valid 2>/dev/null)
SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null)
sessionMessage=$(echo "${sessionResponse}"| jq --raw-output .session.message 2>/dev/null)
# obtain validity, session ID, sessionMessage and error message from
# session response, apply default values if none returned
result=$(echo "${sessionResponse}" | jq -r '
(.session.valid // false),
(.session.sid // null),
(.session.message // null),
(.error.message // null)
' 2>/dev/null)
validSession=$(echo "${result}" | sed -n '1p')
SID=$(echo "${result}" | sed -n '2p')
sessionMessage=$(echo "${result}" | sed -n '3p')
sessionError=$(echo "${result}" | sed -n '4p')
# obtain the error message from the session response
sessionError=$(echo "${sessionResponse}"| jq --raw-output .error.message 2>/dev/null)
if [ "${1}" = "verbose" ]; then
if [ "${validSession}" = true ]; then
@@ -367,9 +351,12 @@ apiFunc() {
if [ "${verbosity}" = "verbose" ]; then
echo "Data:"
fi
# Attempt to print the data with jq, if it is not valid JSON, or not installed
# then print the plain text.
echo "${data}" | jq . 2>/dev/null || echo "${data}"
if command -v jq >/dev/null && echo "${data}" | jq . >/dev/null 2>&1; then
echo "${data}" | jq .
else
echo "${data}"
fi
# Delete the session
LogoutAPI "${verbosity}"

View File

@@ -150,10 +150,4 @@ upgrade_gravityDB(){
pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/18_to_19.sql"
version=19
fi
if [[ "$version" == "19" ]]; then
# Update views to use new allowlist/denylist names
echo -e " ${INFO} Upgrading gravity database from version 19 to 20"
pihole-FTL sqlite3 -ni "${database}" < "${scriptPath}/19_to_20.sql"
version=20
fi
}

View File

@@ -1,43 +0,0 @@
.timeout 30000
BEGIN TRANSACTION;
DROP VIEW vw_whitelist;
CREATE VIEW vw_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1)
AND domainlist.type = 0
ORDER BY domainlist.id;
DROP VIEW vw_blacklist;
CREATE VIEW vw_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1)
AND domainlist.type = 1
ORDER BY domainlist.id;
DROP VIEW vw_regex_whitelist;
CREATE VIEW vw_regex_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1)
AND domainlist.type = 2
ORDER BY domainlist.id;
DROP VIEW vw_regex_blacklist;
CREATE VIEW vw_regex_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1)
AND domainlist.type = 3
ORDER BY domainlist.id;
UPDATE info SET value = 20 WHERE property = 'version';
COMMIT;

View File

@@ -9,12 +9,12 @@
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
PI_HOLE_SCRIPT_DIR="/opt/pihole"
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source="./advanced/Scripts/utils.sh"
source "${utilsfile}"
apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
# shellcheck source="./advanced/Scripts/api.sh"
source "${apifile}"

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Pi-hole: A black hole for Internet advertisements
# (c) 2019 Pi-hole, LLC (https://pi-hole.net)
# Network-wide ad blocking via your own hardware.
#
# ARP table interaction
#
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
coltable="/opt/pihole/COL_TABLE"
if [[ -f ${coltable} ]]; then
# shellcheck source="./advanced/Scripts/COL_TABLE"
source ${coltable}
fi
# shellcheck source=./advanced/Scripts/utils.sh
source "/opt/pihole/utils.sh"
SKIP_INSTALL="true"
# shellcheck source="./automated install/basic-install.sh"
source "/etc/.pihole/automated install/basic-install.sh"
# stop_service() is defined in basic-install.sh
# restart_service() is defined in basic-install.sh
# Determine database location
DBFILE=$(getFTLConfigValue "files.database")
if [ -z "$DBFILE" ]; then
DBFILE="/etc/pihole/pihole-FTL.db"
fi
flushARP(){
local output
if [[ "${args[1]}" != "quiet" ]]; then
echo -ne " ${INFO} Flushing network table ..."
fi
# Stop FTL to prevent database access
if ! output=$(stop_service pihole-FTL 2>&1); then
echo -e "${OVER} ${CROSS} Failed to stop FTL"
echo " Output: ${output}"
return 1
fi
# Truncate network_addresses table in pihole-FTL.db
# This needs to be done before we can truncate the network table due to
# foreign key constraints
if ! output=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM network_addresses" 2>&1); then
echo -e "${OVER} ${CROSS} Failed to truncate network_addresses table"
echo " Database location: ${DBFILE}"
echo " Output: ${output}"
return 1
fi
# Truncate network table in pihole-FTL.db
if ! output=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM network" 2>&1); then
echo -e "${OVER} ${CROSS} Failed to truncate network table"
echo " Database location: ${DBFILE}"
echo " Output: ${output}"
return 1
fi
# Flush ARP cache of the host
if ! output=$(ip -s -s neigh flush all 2>&1); then
echo -e "${OVER} ${CROSS} Failed to flush ARP cache"
echo " Output: ${output}"
return 1
fi
# Start FTL again
if ! output=$(restart_service pihole-FTL 2>&1); then
echo -e "${OVER} ${CROSS} Failed to restart FTL"
echo " Output: ${output}"
return 1
fi
if [[ "${args[1]}" != "quiet" ]]; then
echo -e "${OVER} ${TICK} Flushed network table"
fi
}
args=("$@")
case "${args[0]}" in
"arpflush" ) flushARP;;
esac

View File

@@ -26,7 +26,7 @@ source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
warning1() {
echo " Please note that changing branches severely alters your Pi-hole subsystems"
echo " Features that work on the master branch, may not on a development branch"
echo -e " ${COL_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}"
echo -e " ${COL_LIGHT_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}"
read -r -p " Have you read and understood this? [y/N] " response
case "${response}" in
[yY][eE][sS]|[yY])
@@ -55,19 +55,19 @@ checkout() {
# This is unlikely
if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e " ${COL_RED}Error: Core Pi-hole repo is missing from system!"
echo -e " ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!"
echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
exit 1;
fi
if ! is_repo "${webInterfaceDir}" ; then
echo -e " ${COL_RED}Error: Web Admin repo is missing from system!"
echo -e " ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!"
echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
exit 1;
fi
if [[ -z "${1}" ]]; then
echo -e " ${COL_RED}Invalid option${COL_NC}"
echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}"
echo -e " Try 'pihole checkout --help' for more information."
exit 1
fi
@@ -238,7 +238,7 @@ checkout() {
if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then
exit 0
else
echo -e " ${COL_RED} Error: Unable to complete update, please contact support${COL_NC}"
echo -e " ${COL_LIGHT_RED} Error: Unable to complete update, please contact support${COL_NC}"
exit 1
fi
fi

View File

@@ -169,7 +169,7 @@ initialize_debug() {
# Display that the debug process is beginning
log_write "${COL_PURPLE}*** [ INITIALIZING ]${COL_NC}"
# Timestamp the start of the log
log_write "${INFO} $(date "+%Y-%m-%d %H:%M:%S") debug log has been initialized."
log_write "${INFO} $(date "+%Y-%m-%d:%H:%M:%S") debug log has been initialized."
# Uptime of the system
# credits to https://stackoverflow.com/questions/28353409/bash-format-uptime-to-show-days-hours-minutes
system_uptime=$(uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}')
@@ -367,7 +367,7 @@ check_firewalld() {
# test common required service ports
local firewalld_enabled_services
firewalld_enabled_services=$(firewall-cmd --list-services)
local firewalld_expected_services=("http" "https" "dns" "dhcp" "dhcpv6" "ntp")
local firewalld_expected_services=("http" "dns" "dhcp" "dhcpv6")
for i in "${firewalld_expected_services[@]}"; do
if [[ "${firewalld_enabled_services}" =~ ${i} ]]; then
log_write "${TICK} ${COL_GREEN} Allow Service: ${i}${COL_NC}";
@@ -375,6 +375,30 @@ check_firewalld() {
log_write "${CROSS} ${COL_RED} Allow Service: ${i}${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})"
fi
done
# check for custom FTL FirewallD zone
local firewalld_zones
firewalld_zones=$(firewall-cmd --get-zones)
if [[ "${firewalld_zones}" =~ "ftl" ]]; then
log_write "${TICK} ${COL_GREEN}FTL Custom Zone Detected${COL_NC}";
# check FTL custom zone interface: lo
local firewalld_ftl_zone_interfaces
firewalld_ftl_zone_interfaces=$(firewall-cmd --zone=ftl --list-interfaces)
if [[ "${firewalld_ftl_zone_interfaces}" =~ "lo" ]]; then
log_write "${TICK} ${COL_GREEN} Local Interface Detected${COL_NC}";
else
log_write "${CROSS} ${COL_RED} Local Interface Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})"
fi
# check FTL custom zone port: 4711
local firewalld_ftl_zone_ports
firewalld_ftl_zone_ports=$(firewall-cmd --zone=ftl --list-ports)
if [[ "${firewalld_ftl_zone_ports}" =~ "4711/tcp" ]]; then
log_write "${TICK} ${COL_GREEN} FTL Port 4711/tcp Detected${COL_NC}";
else
log_write "${CROSS} ${COL_RED} FTL Port 4711/tcp Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})"
fi
else
log_write "${CROSS} ${COL_RED}FTL Custom Zone Not Detected${COL_NC} (${FAQ_HARDWARE_REQUIREMENTS_FIREWALLD})"
fi
fi
else
log_write "${TICK} ${COL_GREEN}Firewalld service not detected${COL_NC}";
@@ -473,25 +497,16 @@ ping_gateway() {
ping_ipv4_or_ipv6 "${protocol}"
# Check if we are using IPv4 or IPv6
# Find the default gateways using IPv4 or IPv6
local gateway gateway_addr gateway_iface default_route
local gateway gateway_addr gateway_iface
log_write "${INFO} Default IPv${protocol} gateway(s):"
while IFS= read -r default_route; do
gateway_addr=$(jq -r '.gateway' <<< "${default_route}")
gateway_iface=$(jq -r '.dev' <<< "${default_route}")
log_write " ${gateway_addr}%${gateway_iface}"
done < <(ip -j -"${protocol}" route | jq -c '.[] | select(.dst == "default")')
# Find the first default route
default_route=$(ip -j -"${protocol}" route show default)
if echo "$default_route" | grep 'gateway' | grep -q 'dev'; then
gateway_addr=$(echo "$default_route" | jq -r -c '.[0].gateway')
gateway_iface=$(echo "$default_route" | jq -r -c '.[0].dev')
else
log_write " Unable to determine gateway address for IPv${protocol}"
fi
while IFS= read -r gateway; do
log_write " $(cut -d ' ' -f 3 <<< "${gateway}")%$(cut -d ' ' -f 5 <<< "${gateway}")"
done < <(ip -"${protocol}" route | grep default)
gateway_addr=$(ip -"${protocol}" route | grep default | cut -d ' ' -f 3 | head -n 1)
gateway_iface=$(ip -"${protocol}" route | grep default | cut -d ' ' -f 5 | head -n 1)
# If there was at least one gateway
if [ -n "${gateway_addr}" ]; then
# Append the interface to the gateway address if it is a link-local address
@@ -577,21 +592,18 @@ check_required_ports() {
# Add port 53
ports_configured+=("53")
local protocol_type port_number service_name
# Now that we have the values stored,
for i in "${!ports_in_use[@]}"; do
# loop through them and assign some local variables
read -r protocol_type port_number service_name <<< "$(
awk '{
p=$1; n=$5; s=$7
gsub(/users:\(\("/,"",s)
gsub(/".*/,"",s)
print p, n, s
}' <<< "${ports_in_use[$i]}"
)"
local service_name
service_name=$(echo "${ports_in_use[$i]}" | awk '{gsub(/users:\(\("/,"",$7);gsub(/".*/,"",$7);print $7}')
local protocol_type
protocol_type=$(echo "${ports_in_use[$i]}" | awk '{print $1}')
local port_number
port_number="$(echo "${ports_in_use[$i]}" | awk '{print $5}')" # | awk '{gsub(/^.*:/,"",$5);print $5}')
# Check if the right services are using the right ports
if [[ ${ports_configured[*]} =~ ${port_number##*:} ]]; then
if [[ ${ports_configured[*]} =~ $(echo "${port_number}" | rev | cut -d: -f1 | rev) ]]; then
compare_port_to_service_assigned "${ftl}" "${service_name}" "${protocol_type}:${port_number}"
else
# If it's not a default port that Pi-hole needs, just print it out for the user to see
@@ -659,7 +671,7 @@ dig_at() {
local record_type="A"
fi
# Find a random blocked url that has not been allowlisted and is not ABP style.
# Find a random blocked url that has not been whitelisted and is not ABP style.
# This helps emulate queries to different domains that a user might query
# It will also give extra assurance that Pi-hole is correctly resolving and blocking domains
local random_url
@@ -709,7 +721,7 @@ dig_at() {
fi
# Check if Pi-hole can use itself to block a domain
if local_dig="$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" "${record_type}" -p "$(get_ftl_conf_value "dns.port")")"; then
if local_dig="$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" "${record_type}")"; then
# If it can, show success
if [[ "${local_dig}" == *"status: NOERROR"* ]]; then
local_dig="NOERROR"
@@ -765,7 +777,7 @@ process_status(){
:
else
# non-Docker system
if service "${i}" status | grep -q -E 'is\srunning|started'; then
if service "${i}" status | grep -E 'is\srunning' &> /dev/null; then
status_of_process="active"
else
status_of_process="inactive"
@@ -803,27 +815,42 @@ ftl_full_status(){
make_array_from_file() {
local filename="${1}"
# If the file is a directory do nothing since it cannot be parsed
[[ -d "${filename}" ]] && return
# The second argument can put a limit on how many line should be read from the file
# Since some of the files are so large, this is helpful to limit the output
local limit=${2}
# A local iterator for testing if we are at the limit above
local i=0
# If the file is a directory
if [[ -d "${filename}" ]]; then
# do nothing since it cannot be parsed
:
else
# Otherwise, read the file line by line
while IFS= read -r line;do
# Otherwise, strip out comments and blank lines
new_line=$(echo "${line}" | sed -e 's/^\s*#.*$//' -e '/^$/d')
# If the line still has content (a non-zero value)
if [[ -n "${new_line}" ]]; then
# Process the file, strip out comments and blank lines
local processed
processed=$(sed -e 's/^\s*#.*$//' -e '/^$/d' "${filename}")
# If the string contains "### CHANGED", highlight this part in red
if [[ "${new_line}" == *"### CHANGED"* ]]; then
new_line="${new_line//### CHANGED/${COL_RED}### CHANGED${COL_NC}}"
fi
while IFS= read -r line; do
# If the string contains "### CHANGED", highlight this part in red
log_write " ${line//### CHANGED/${COL_RED}### CHANGED${COL_NC}}"
((i++))
# if the limit of lines we want to see is exceeded do nothing
[[ -n ${limit} && $i -eq ${limit} ]] && break
done <<< "$processed"
# Finally, write this line to the log
log_write " ${new_line}"
fi
# Increment the iterator +1
i=$((i+1))
# but if the limit of lines we want to see is exceeded
if [[ -z ${limit} ]]; then
# do nothing
:
elif [[ $i -eq ${limit} ]]; then
break
fi
done < "${filename}"
fi
}
parse_file() {
@@ -896,38 +923,38 @@ list_files_in_dir() {
fi
# Store the files found in an array
local files_found=("${dir_to_parse}"/*)
mapfile -t files_found < <(ls "${dir_to_parse}")
# For each file in the array,
for each_file in "${files_found[@]}"; do
if [[ -d "${each_file}" ]]; then
if [[ -d "${dir_to_parse}/${each_file}" ]]; then
# If it's a directory, do nothing
:
elif [[ "${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \
[[ "${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \
[[ "${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \
[[ "${each_file}" == "${PIHOLE_LOG}" ]] || \
[[ "${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then
elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then
:
elif [[ "${dir_to_parse}" == "${DNSMASQ_D_DIRECTORY}" ]]; then
# in case of the dnsmasq directory include all files in the debug output
log_write "\\n${COL_GREEN}$(ls -lhd "${each_file}")${COL_NC}"
make_array_from_file "${each_file}"
log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}"
make_array_from_file "${dir_to_parse}/${each_file}"
else
# Then, parse the file's content into an array so each line can be analyzed if need be
for i in "${!REQUIRED_FILES[@]}"; do
if [[ "${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then
if [[ "${dir_to_parse}/${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then
# display the filename
log_write "\\n${COL_GREEN}$(ls -lhd "${each_file}")${COL_NC}"
log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}"
# Check if the file we want to view has a limit (because sometimes we just need a little bit of info from the file, not the entire thing)
case "${each_file}" in
case "${dir_to_parse}/${each_file}" in
# If it's Web server log, give the first and last 25 lines
"${PIHOLE_WEBSERVER_LOG}") head_tail_log "${each_file}" 25
"${PIHOLE_WEBSERVER_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 25
;;
# Same for the FTL log
"${PIHOLE_FTL_LOG}") head_tail_log "${each_file}" 35
"${PIHOLE_FTL_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 35
;;
# parse the file into an array in case we ever need to analyze it line-by-line
*) make_array_from_file "${each_file}";
*) make_array_from_file "${dir_to_parse}/${each_file}";
esac
else
# Otherwise, do nothing since it's not a file needed for Pi-hole so we don't care about it
@@ -963,7 +990,6 @@ head_tail_log() {
local filename="${1}"
# The number of lines to use for head and tail
local qty="${2}"
local filebasename="${filename##*/}"
local head_line
local tail_line
# Put the current Internal Field Separator into another variable so it can be restored later
@@ -972,14 +998,14 @@ head_tail_log() {
IFS=$'\r\n'
local log_head=()
mapfile -t log_head < <(head -n "${qty}" "${filename}")
log_write " ${COL_CYAN}-----head of ${filebasename}------${COL_NC}"
log_write " ${COL_CYAN}-----head of $(basename "${filename}")------${COL_NC}"
for head_line in "${log_head[@]}"; do
log_write " ${head_line}"
done
log_write ""
local log_tail=()
mapfile -t log_tail < <(tail -n "${qty}" "${filename}")
log_write " ${COL_CYAN}-----tail of ${filebasename}------${COL_NC}"
log_write " ${COL_CYAN}-----tail of $(basename "${filename}")------${COL_NC}"
for tail_line in "${log_tail[@]}"; do
log_write " ${tail_line}"
done
@@ -1006,24 +1032,6 @@ show_db_entries() {
)
for line in "${entries[@]}"; do
# Use gray color for "no". Normal color for "yes"
line=${line//--no---/${COL_GRAY} no ${COL_NC}}
line=${line//--yes--/ yes }
# Use red for "deny" and green for "allow"
if [ "$title" = "Domainlist" ]; then
line=${line//regex-deny/${COL_RED}regex-deny${COL_NC}}
line=${line//regex-allow/${COL_GREEN}regex-allow${COL_NC}}
line=${line//exact-deny/${COL_RED}exact-deny${COL_NC}}
line=${line//exact-allow/${COL_GREEN}exact-allow${COL_NC}}
fi
# Use red for "block" and green for "allow"
if [ "$title" = "Adlists" ]; then
line=${line//-BLOCK-/${COL_RED} Block ${COL_NC}}
line=${line//-ALLOW-/${COL_GREEN} Allow ${COL_NC}}
fi
log_write " ${line}"
done
@@ -1071,15 +1079,15 @@ check_dhcp_servers() {
}
show_groups() {
show_db_entries "Groups" "SELECT id,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,name,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 7 50 19 19 50"
show_db_entries "Groups" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,name,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 7 50 19 19 50"
}
show_adlists() {
show_db_entries "Adlists" "SELECT id,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,GROUP_CONCAT(adlist_by_group.group_id) group_ids, CASE type WHEN '0' THEN '-BLOCK-' WHEN '1' THEN '-ALLOW-' ELSE type END type, address,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist LEFT JOIN adlist_by_group ON adlist.id = adlist_by_group.adlist_id GROUP BY id;" "5 7 12 7 100 19 19 50"
show_db_entries "Adlists" "SELECT id,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(adlist_by_group.group_id) group_ids,address,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist LEFT JOIN adlist_by_group ON adlist.id = adlist_by_group.adlist_id GROUP BY id;" "5 7 12 100 19 19 50"
}
show_domainlist() {
show_db_entries "Domainlist" "SELECT id,CASE type WHEN '0' THEN 'exact-allow' WHEN '1' THEN 'exact-deny' WHEN '2' THEN 'regex-allow' WHEN '3' THEN 'regex-deny' ELSE type END type,CASE enabled WHEN '0' THEN '--no---' WHEN '1' THEN '--yes--' ELSE enabled END enabled,GROUP_CONCAT(domainlist_by_group.group_id) group_ids,domain,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist LEFT JOIN domainlist_by_group ON domainlist.id = domainlist_by_group.domainlist_id GROUP BY id;" "5 11 7 12 90 19 19 50"
show_db_entries "Domainlist (0/1 = exact white-/blacklist, 2/3 = regex white-/blacklist)" "SELECT id,CASE type WHEN '0' THEN '0 ' WHEN '1' THEN ' 1 ' WHEN '2' THEN ' 2 ' WHEN '3' THEN ' 3' ELSE type END type,CASE enabled WHEN '0' THEN ' 0' WHEN '1' THEN ' 1' ELSE enabled END enabled,GROUP_CONCAT(domainlist_by_group.group_id) group_ids,domain,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist LEFT JOIN domainlist_by_group ON domainlist.id = domainlist_by_group.domainlist_id GROUP BY id;" "5 4 7 12 100 19 19 50"
}
show_clients() {

View File

@@ -12,10 +12,14 @@ colfile="/opt/pihole/COL_TABLE"
# shellcheck source="./advanced/Scripts/COL_TABLE"
source ${colfile}
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source="./advanced/Scripts/utils.sh"
source "${utilsfile}"
source "/opt/pihole/utils.sh"
SKIP_INSTALL="true"
# shellcheck source="./automated install/basic-install.sh"
source "/etc/.pihole/automated install/basic-install.sh"
# stop_service() is defined in basic-install.sh
# restart_service() is defined in basic-install.sh
# In case we're running at the same time as a system logrotate, use a
# separate logrotate state file to prevent stepping on each other's
@@ -86,7 +90,6 @@ if [[ "$*" == *"once"* ]]; then
if [[ "$*" != *"quiet"* ]]; then
echo -ne " ${INFO} Running logrotate ..."
fi
mkdir -p "${STATEFILE%/*}"
/usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate
else
# Handle rotation for each log file
@@ -105,14 +108,16 @@ else
fi
# Stop FTL to make sure it doesn't write to the database while we're deleting data
service pihole-FTL stop
stop_service pihole-FTL >/dev/null
# Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history)
deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1")
# Restart FTL
service pihole-FTL restart
restart_service pihole-FTL >/dev/null
if [[ "$*" != *"quiet"* ]]; then
echo -e "${OVER} ${TICK} Deleted ${deleted} queries from long-term query database"
fi
fi

View File

@@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Pi-hole: A black hole for Internet advertisements
# (c) 2019 Pi-hole, LLC (https://pi-hole.net)
# Network-wide ad blocking via your own hardware.
#
# Network table flush
#
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
coltable="/opt/pihole/COL_TABLE"
if [[ -f ${coltable} ]]; then
# shellcheck source="./advanced/Scripts/COL_TABLE"
source ${coltable}
fi
PI_HOLE_SCRIPT_DIR="/opt/pihole"
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source=./advanced/Scripts/utils.sh
source "${utilsfile}"
# Source api functions
# shellcheck source="./advanced/Scripts/api.sh"
. "${PI_HOLE_SCRIPT_DIR}/api.sh"
flushNetwork(){
local output
echo -ne " ${INFO} Flushing network table ..."
local data status error
# Authenticate with FTL
LoginAPI
# send query again
data=$(PostFTLData "action/flush/network" "" "status")
# Separate the status from the data
status=$(printf %s "${data#"${data%???}"}")
data=$(printf %s "${data%???}")
# If there is an .error object in the returned data, display it
local error
error=$(jq --compact-output <<< "${data}" '.error')
if [[ $error != "null" && $error != "" ]]; then
echo -e "${OVER} ${CROSS} Failed to flush the network table:"
echo -e " $(jq <<< "${data}" '.error')"
LogoutAPI
exit 1
elif [[ "${status}" == "200" ]]; then
echo -e "${OVER} ${TICK} Flushed network table"
fi
# Delete session
LogoutAPI
}
flushArp(){
# Flush ARP cache of the host
if ! output=$(ip -s -s neigh flush all 2>&1); then
echo -e "${OVER} ${CROSS} Failed to flush ARP cache"
echo " Output: ${output}"
return 1
fi
}
# Process all options (if present)
while [ "$#" -gt 0 ]; do
case "$1" in
"--arp" ) doARP=true ;;
esac
shift
done
flushNetwork
if [[ "${doARP}" == true ]]; then
echo -ne " ${INFO} Flushing ARP cache"
if flushArp; then
echo -e "${OVER} ${TICK} Flushed ARP cache"
fi
fi

View File

@@ -37,16 +37,19 @@ Options:
}
GenerateOutput() {
local counts data num_gravity num_lists search_type_str
local gravity_data_csv lists_data_csv line url type color
local data gravity_data lists_data num_gravity num_lists search_type_str
local gravity_data_csv lists_data_csv line current_domain url type color
data="${1}"
# Get count of list and gravity matches
# Use JQ to count number of entries in lists and gravity
# (output is number of list matches then number of gravity matches)
counts=$(printf %s "${data}" | jq --raw-output '(.search.domains | length), (.search.gravity | group_by(.address,.type) | length)')
num_lists=$(echo "$counts" | sed -n '1p')
num_gravity=$(echo "$counts" | sed -n '2p')
# construct a new json for the list results where each object contains the domain and the related type
lists_data=$(printf %s "${data}" | jq '.search.domains | [.[] | {domain: .domain, type: .type}]')
# construct a new json for the gravity results where each object contains the adlist URL and the related domains
gravity_data=$(printf %s "${data}" | jq '.search.gravity | group_by(.address,.type) | map({ address: (.[0].address), type: (.[0].type), domains: [.[] | .domain] })')
# number of objects in each json
num_gravity=$(printf %s "${gravity_data}" | jq length)
num_lists=$(printf %s "${lists_data}" | jq length)
if [ "${partial}" = true ]; then
search_type_str="partially"
@@ -59,7 +62,7 @@ GenerateOutput() {
if [ "${num_lists}" -gt 0 ]; then
# Convert the data to a csv, each line is a "domain,type" string
# not using jq's @csv here as it quotes each value individually
lists_data_csv=$(printf %s "${data}" | jq --raw-output '.search.domains | map([.domain, .type] | join(",")) | join("\n")')
lists_data_csv=$(printf %s "${lists_data}" | jq --raw-output '.[] | [.domain, .type] | join(",")')
# Generate output for each csv line, separating line in a domain and type substring at the ','
echo "${lists_data_csv}" | while read -r line; do
@@ -68,11 +71,11 @@ GenerateOutput() {
fi
# Results from gravity
printf "%s\n\n" "Found ${num_gravity} lists ${search_type_str} matching '${COL_BLUE}${domain}${COL_NC}'."
printf "%s\n\n" "Found ${num_gravity} adlists ${search_type_str} matching '${COL_BLUE}${domain}${COL_NC}'."
if [ "${num_gravity}" -gt 0 ]; then
# Convert the data to a csv, each line is a "URL,type,domain,domain,...." string
# Convert the data to a csv, each line is a "URL,domain,domain,...." string
# not using jq's @csv here as it quotes each value individually
gravity_data_csv=$(printf %s "${data}" | jq --raw-output '.search.gravity | group_by(.address,.type) | map([.[0].address, .[0].type, (.[] | .domain)] | join(",")) | join("\n")')
gravity_data_csv=$(printf %s "${gravity_data}" | jq --raw-output '.[] | [.address, .type, .domains[]] | join(",")')
# Generate line-by-line output for each csv line
echo "${gravity_data_csv}" | while read -r line; do
@@ -94,8 +97,15 @@ GenerateOutput() {
# cut off type, leaving "domain,domain,...."
line=${line#*,}
# Replace commas with newlines and format output
echo "${line}" | sed 's/,/\n/g' | sed "s/^/ - ${COL_GREEN}/" | sed "s/$/${COL_NC}/"
# print each domain and remove it from the string until nothing is left
while [ ${#line} -gt 0 ]; do
current_domain=${line%%,*}
printf ' - %s\n' "${COL_GREEN}${current_domain}${COL_NC}"
# we need to remove the current_domain and the comma in two steps because
# the last domain won't have a trailing comma and the while loop wouldn't exit
line=${line#"${current_domain}"}
line=${line#,}
done
printf "\n\n"
done
fi

View File

@@ -47,7 +47,7 @@ GitCheckUpdateAvail() {
# Fetch latest changes in this repo
if ! git fetch --quiet origin ; then
echo -e "\\n ${COL_RED}Error: Unable to update local repository. Contact Pi-hole Support.${COL_NC}"
echo -e "\\n ${COL_LIGHT_RED}Error: Unable to update local repository. Contact Pi-hole Support.${COL_NC}"
exit 1
fi
@@ -76,13 +76,13 @@ GitCheckUpdateAvail() {
if [[ "${#LOCAL}" == 0 ]]; then
echo -e "\\n ${COL_RED}Error: Local revision could not be obtained, please contact Pi-hole Support"
echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support"
echo -e " Additional debugging output:${COL_NC}"
git status
exit 1
fi
if [[ "${#REMOTE}" == 0 ]]; then
echo -e "\\n ${COL_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support"
echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support"
echo -e " Additional debugging output:${COL_NC}"
git status
exit 1
@@ -103,7 +103,7 @@ GitCheckUpdateAvail() {
}
main() {
local basicError="\\n ${COL_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}"
local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}"
local core_update
local web_update
local FTL_update
@@ -120,7 +120,7 @@ main() {
# This is unlikely
if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e "\\n ${COL_RED}Error: Core Pi-hole repo is missing from system!"
echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!"
echo -e " Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1;
fi
@@ -132,11 +132,11 @@ main() {
echo -e " ${INFO} Pi-hole Core:\\t${COL_YELLOW}update available${COL_NC}"
else
core_update=false
echo -e " ${INFO} Pi-hole Core:\\t${COL_GREEN}up to date${COL_NC}"
echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
if ! is_repo "${ADMIN_INTERFACE_DIR}" ; then
echo -e "\\n ${COL_RED}Error: Web Admin repo is missing from system!"
echo -e "\\n ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!"
echo -e " Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1;
fi
@@ -146,7 +146,7 @@ main() {
echo -e " ${INFO} Web Interface:\\t${COL_YELLOW}update available${COL_NC}"
else
web_update=false
echo -e " ${INFO} Web Interface:\\t${COL_GREEN}up to date${COL_NC}"
echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
local funcOutput
@@ -160,18 +160,17 @@ main() {
else
case $? in
1)
echo -e " ${INFO} FTL:\\t\\t${COL_GREEN}up to date${COL_NC}"
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
;;
2)
echo -e " ${INFO} FTL:\\t\\t${COL_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch."
exit 1
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_LIGHT_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch."
;;
3)
echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, cannot reach download server${COL_NC}"
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Something has gone wrong, cannot reach download server${COL_NC}"
exit 1
;;
*)
echo -e " ${INFO} FTL:\\t\\t${COL_RED}Something has gone wrong, contact support${COL_NC}"
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Something has gone wrong, contact support${COL_NC}"
exit 1
esac
FTL_update=false
@@ -188,7 +187,7 @@ main() {
if [[ ! "${ftlBranch}" == "master" && ! "${ftlBranch}" == "development" ]]; then
# Notify user that they are on a custom branch which might mean they they are lost
# behind if a branch was merged to development and got abandoned
printf " %b %bWarning:%b You are using FTL from a custom branch (%s) and might be missing future releases.\\n" "${INFO}" "${COL_RED}" "${COL_NC}" "${ftlBranch}"
printf " %b %bWarning:%b You are using FTL from a custom branch (%s) and might be missing future releases.\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" "${ftlBranch}"
fi
if [[ "${core_update}" == false && "${web_update}" == false && "${FTL_update}" == false ]]; then

View File

@@ -73,9 +73,7 @@ getFTLPID() {
# Example getFTLConfigValue dns.piholePTR
#######################
getFTLConfigValue(){
# Pipe to cat to avoid pihole-FTL assuming this is an interactive command
# returning colored output.
pihole-FTL --config -q "${1}" | cat
pihole-FTL --config -q "${1}"
}
#######################
@@ -88,17 +86,9 @@ getFTLConfigValue(){
# setFTLConfigValue dns.upstreams '[ "8.8.8.8" , "8.8.4.4" ]'
#######################
setFTLConfigValue(){
local err
{ pihole-FTL --config "${1}" "${2}" >/dev/null; err="$?"; } || true
case $err in
0) ;;
5)
# FTL returns 5 if the value was set by an environment variable and is therefore read-only
printf " %s %s set by environment variable. Please unset it to use this function\n" "${CROSS}" "${1}";
exit 5;;
*)
printf " %s Failed to set %s. Try with sudo power\n" "${CROSS}" "${1}"
exit 1
esac
pihole-FTL --config "${1}" "${2}" >/dev/null
if [ $? -eq 5 ]; then
printf " %s %s set by environment variable. Please unset it to use this function\n" "${CROSS}" "${1}"
exit 5
fi
}

View File

@@ -66,7 +66,7 @@ CREATE TABLE info
value TEXT NOT NULL
);
INSERT INTO "info" VALUES('version','20');
INSERT INTO "info" VALUES('version','19');
/* This is a flag to indicate if gravity was restored from a backup
false = not restored,
failed = restoration failed due to no backup
@@ -111,7 +111,7 @@ CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist
UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain;
END;
CREATE VIEW vw_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
@@ -119,7 +119,7 @@ CREATE VIEW vw_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_gr
AND domainlist.type = 0
ORDER BY domainlist.id;
CREATE VIEW vw_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
@@ -127,7 +127,7 @@ CREATE VIEW vw_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_gro
AND domainlist.type = 1
ORDER BY domainlist.id;
CREATE VIEW vw_regex_allowlist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id
@@ -135,7 +135,7 @@ CREATE VIEW vw_regex_allowlist AS SELECT domain, domainlist.id AS id, domainlist
AND domainlist.type = 2
ORDER BY domainlist.id;
CREATE VIEW vw_regex_denylist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id
FROM domainlist
LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id
LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id

View File

@@ -8,20 +8,12 @@ utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# Get file paths
FTL_PID_FILE="$(getFTLConfigValue files.pid)"
FTL_LOG_FILE="$(getFTLConfigValue files.log.ftl)"
PIHOLE_LOG_FILE="$(getFTLConfigValue files.log.dnsmasq)"
WEBSERVER_LOG_FILE="$(getFTLConfigValue files.log.webserver)"
FTL_PID_FILE="${FTL_PID_FILE:-/run/pihole-FTL.pid}"
FTL_LOG_FILE="${FTL_LOG_FILE:-/var/log/pihole/FTL.log}"
PIHOLE_LOG_FILE="${PIHOLE_LOG_FILE:-/var/log/pihole/pihole.log}"
WEBSERVER_LOG_FILE="${WEBSERVER_LOG_FILE:-/var/log/pihole/webserver.log}"
# Ensure that permissions are set so that pihole-FTL can edit all necessary files
mkdir -p /var/log/pihole
chown -R pihole:pihole /etc/pihole/ /var/log/pihole/
# allow all users read version file (and use pihole -v)
touch /etc/pihole/versions
chmod 0644 /etc/pihole/versions
# allow pihole to access subdirs in /etc/pihole (sets execution bit on dirs)
@@ -36,7 +28,7 @@ chown root:root /etc/pihole/logrotate
# Touch files to ensure they exist (create if non-existing, preserve if existing)
[ -f "${FTL_PID_FILE}" ] || install -D -m 644 -o pihole -g pihole /dev/null "${FTL_PID_FILE}"
[ -f "${FTL_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${FTL_LOG_FILE}"
[ -f "${PIHOLE_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${PIHOLE_LOG_FILE}"
[ -f "${WEBSERVER_LOG_FILE}" ] || install -m 640 -o pihole -g pihole /dev/null "${WEBSERVER_LOG_FILE}"
[ -f /var/log/pihole/FTL.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/FTL.log
[ -f /var/log/pihole/pihole.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/pihole.log
[ -f /var/log/pihole/webserver.log ] || install -m 640 -o pihole -g pihole /dev/null /var/log/pihole/webserver.log
[ -f /etc/pihole/dhcp.leases ] || install -m 644 -o pihole -g pihole /dev/null /etc/pihole/dhcp.leases

View File

@@ -1,40 +0,0 @@
#!/sbin/openrc-run
# shellcheck shell=sh disable=SC2034
: "${PI_HOLE_SCRIPT_DIR:=/opt/pihole}"
command="/usr/bin/pihole-FTL"
command_user="pihole:pihole"
supervisor=supervise-daemon
command_args_foreground="-f"
command_background=true
pidfile="/run/${RC_SVCNAME}_openrc.pid"
extra_started_commands="reload"
respawn_max=5
respawn_period=60
capabilities="^CAP_NET_BIND_SERVICE,^CAP_NET_RAW,^CAP_NET_ADMIN,^CAP_SYS_NICE,^CAP_IPC_LOCK,^CAP_CHOWN,^CAP_SYS_TIME"
depend() {
want net
provide dns
}
checkconfig() {
$command -f test
}
start_pre() {
sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-prestart.sh"
}
stop_post() {
sh "${PI_HOLE_SCRIPT_DIR}/pihole-FTL-poststop.sh"
}
reload() {
checkconfig || return $?
ebegin "Reloading ${RC_SVCNAME}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}

View File

@@ -17,15 +17,15 @@ StartLimitIntervalSec=60s
[Service]
User=pihole
PermissionsStartOnly=true
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN CAP_SYS_TIME
# Run prestart with elevated permissions
ExecStartPre=+/opt/pihole/pihole-FTL-prestart.sh
ExecStartPre=/opt/pihole/pihole-FTL-prestart.sh
ExecStart=/usr/bin/pihole-FTL -f
Restart=on-failure
RestartSec=5s
ExecReload=/bin/kill -HUP $MAINPID
ExecStopPost=+/opt/pihole/pihole-FTL-poststop.sh
ExecStopPost=/opt/pihole/pihole-FTL-poststop.sh
# Use graceful shutdown with a reasonable timeout
TimeoutStopSec=60s

View File

@@ -0,0 +1,51 @@
_pihole() {
local cur prev opts opts_checkout opts_debug opts_logging opts_query opts_update opts_version
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
prev2="${COMP_WORDS[COMP_CWORD-2]}"
case "${prev}" in
"pihole")
opts="allow allow-regex allow-wild deny checkout debug disable enable flush help logging query repair regex reloaddns reloadlists status tail uninstall updateGravity updatePihole version wildcard arpflush api"
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
;;
"allow"|"deny"|"wildcard"|"regex"|"allow-regex"|"allow-wild")
opts_lists="\not \--delmode \--quiet \--list \--help"
COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) )
;;
"checkout")
opts_checkout="core ftl web master dev"
COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) )
;;
"debug")
opts_debug="-a"
COMPREPLY=( $(compgen -W "${opts_debug}" -- ${cur}) )
;;
"logging")
opts_logging="on off 'off noflush'"
COMPREPLY=( $(compgen -W "${opts_logging}" -- ${cur}) )
;;
"query")
opts_query="--partial --all"
COMPREPLY=( $(compgen -W "${opts_query}" -- ${cur}) )
;;
"updatePihole"|"-up")
opts_update="--check-only"
COMPREPLY=( $(compgen -W "${opts_update}" -- ${cur}) )
;;
"core"|"admin"|"ftl")
if [[ "$prev2" == "checkout" ]]; then
opts_checkout="master dev"
COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) )
else
return 1
fi
;;
*)
return 1
;;
esac
return 0
}
complete -F _pihole pihole

View File

@@ -1,9 +0,0 @@
#!/bin/bash
#
# Bash completion script for pihole-FTL
#
# This completion script provides tab completion for pihole-FTL CLI flags and commands.
# It uses the `pihole-FTL --complete` command to generate the completion options.
_complete_FTL() { mapfile -t COMPREPLY < <(pihole-FTL --complete "${COMP_WORDS[@]}"); }
complete -F _complete_FTL pihole-FTL

View File

@@ -1,59 +0,0 @@
#!/bin/bash
#
# Bash completion script for pihole
#
_pihole() {
local cur prev prev2 opts opts_lists opts_checkout opts_debug opts_logging opts_query opts_update opts_networkflush
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
prev2="${COMP_WORDS[COMP_CWORD-2]}"
case "${prev}" in
"pihole")
opts="allow allow-regex allow-wild deny checkout debug disable enable flush help logging query repair regex reloaddns reloadlists setpassword status tail uninstall updateGravity updatePihole version wildcard networkflush api"
mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}")
;;
"allow"|"deny"|"wildcard"|"regex"|"allow-regex"|"allow-wild")
opts_lists="\not \--delmode \--quiet \--list \--help"
mapfile -t COMPREPLY < <(compgen -W "${opts_lists}" -- "${cur}")
;;
"checkout")
opts_checkout="core ftl web master dev"
mapfile -t COMPREPLY < <(compgen -W "${opts_checkout}" -- "${cur}")
;;
"debug")
opts_debug="-a"
mapfile -t COMPREPLY < <(compgen -W "${opts_debug}" -- "${cur}")
;;
"logging")
opts_logging="on off 'off noflush'"
mapfile -t COMPREPLY < <(compgen -W "${opts_logging}" -- "${cur}")
;;
"query")
opts_query="--partial --all"
mapfile -t COMPREPLY < <(compgen -W "${opts_query}" -- "${cur}")
;;
"updatePihole"|"-up")
opts_update="--check-only"
mapfile -t COMPREPLY < <(compgen -W "${opts_update}" -- "${cur}")
;;
"networkflush")
opts_networkflush="--arp"
mapfile -t COMPREPLY < <(compgen -W "${opts_networkflush}" -- "${cur}")
;;
"core"|"web"|"ftl")
if [[ "$prev2" == "checkout" ]]; then
opts_checkout="master development"
mapfile -t COMPREPLY < <(compgen -W "${opts_checkout}" -- "${cur}")
else
return 1
fi
;;
*)
return 1
;;
esac
return 0
}
complete -F _pihole pihole

View File

@@ -94,8 +94,8 @@ fresh_install=true
adlistFile="/etc/pihole/adlists.list"
# Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until this script can run
IPV4_ADDRESS=
IPV6_ADDRESS=
IPV4_ADDRESS=${IPV4_ADDRESS}
IPV6_ADDRESS=${IPV6_ADDRESS}
# Give settings their default values. These may be changed by prompts later in the script.
QUERY_LOGGING=
PRIVACY_LEVEL=
@@ -116,11 +116,11 @@ c=70
PIHOLE_META_PACKAGE_CONTROL_APT=$(
cat <<EOM
Package: pihole-meta
Version: 0.6
Version: 0.4
Maintainer: Pi-hole team <adblock@pi-hole.net>
Architecture: all
Description: Pi-hole dependency meta package
Depends: awk,bash-completion,binutils,ca-certificates,cron|cron-daemon,curl,dialog,bind9-dnsutils|dnsutils,dns-root-data,git,grep,iproute2,iputils-ping,jq,libcap2,libcap2-bin,lshw,procps,psmisc,sudo,unzip
Depends: awk,bash-completion,binutils,ca-certificates,cron|cron-daemon,curl,dialog,dnsutils,dns-root-data,git,grep,iproute2,iputils-ping,jq,libcap2,libcap2-bin,lshw,netcat-openbsd,procps,psmisc,sudo,unzip
Section: contrib/metapackages
Priority: optional
EOM
@@ -130,12 +130,12 @@ EOM
PIHOLE_META_PACKAGE_CONTROL_RPM=$(
cat <<EOM
Name: pihole-meta
Version: 0.3
Version: 0.2
Release: 1
License: EUPL
BuildArch: noarch
Summary: Pi-hole dependency meta package
Requires: bash-completion,bind-utils,binutils,ca-certificates,chkconfig,cronie,curl,dialog,findutils,gawk,git,grep,iproute,jq,libcap,lshw,procps-ng,psmisc,sudo,unzip
Requires: bash-completion,bind-utils,binutils,ca-certificates,chkconfig,cronie,curl,dialog,findutils,gawk,git,grep,iproute,jq,libcap,lshw,nmap-ncat,procps-ng,psmisc,sudo,unzip
%description
Pi-hole dependency meta package
%prep
@@ -143,9 +143,6 @@ Pi-hole dependency meta package
%files
%install
%changelog
* Mon Jul 14 2025 Pi-hole Team - 0.3
- Remove nmap-ncat from the list of dependencies
* Wed May 28 2025 Pi-hole Team - 0.2
- Add gawk to the list of dependencies
@@ -154,35 +151,6 @@ Pi-hole dependency meta package
EOM
)
# List of required packages on APK based systems
PIHOLE_META_VERSION_APK=0.2
PIHOLE_META_DEPS_APK=(
bash
bash-completion
bind-tools
binutils
coreutils
cronie
curl
dialog
git
grep
iproute2-minimal # piholeARPTable.sh
iproute2-ss # piholeDebug.sh
jq
libcap
logrotate
lscpu # piholeDebug.sh
lshw # piholeDebug.sh
ncurses
procps-ng
psmisc
shadow
sudo
tzdata
unzip
)
######## Undocumented Flags. Shhh ########
# These are undocumented flags; some of which we can use when repairing an installation
# The runUnattended flag is one example of this
@@ -190,7 +158,7 @@ repair=false
runUnattended=false
# Check arguments for the undocumented flags
for var in "$@"; do
case "${var}" in
case "$var" in
"--repair") repair=true ;;
"--unattended") runUnattended=true ;;
esac
@@ -205,11 +173,13 @@ if [[ -f "${coltable}" ]]; then
else
# Set these values so the installer can still run in color
COL_NC='\e[0m' # No Color
COL_GREEN='\e[1;32m'
COL_RED='\e[1;31m'
TICK="[${COL_GREEN}${COL_NC}]"
CROSS="[${COL_RED}${COL_NC}]"
COL_LIGHT_GREEN='\e[1;32m'
COL_LIGHT_RED='\e[1;31m'
TICK="[${COL_LIGHT_GREEN}${COL_NC}]"
CROSS="[${COL_LIGHT_RED}${COL_NC}]"
INFO="[i]"
# shellcheck disable=SC2034
DONE="${COL_LIGHT_GREEN} done!${COL_NC}"
OVER="\\r\\033[K"
fi
@@ -217,13 +187,13 @@ fi
# This lets users know that it is a Pi-hole, LLC product
show_ascii_berry() {
echo -e "
${COL_GREEN}.;;,.
${COL_LIGHT_GREEN}.;;,.
.ccccc:,.
:cccclll:. ..,,
:ccccclll. ;ooodc
'ccll:;ll .oooodc
.;cll.;;looo:.
${COL_RED}.. ','.
${COL_LIGHT_RED}.. ','.
.',,,,,,'.
.',,,,,,,,,,.
.',,,,,,,,,,,,....
@@ -245,7 +215,7 @@ abort() {
# remove any leftover build directory that may exist
rm -rf /tmp/pihole-meta_*
echo -e "\\n\\n ${COL_RED}Installation was interrupted${COL_NC}\\n"
echo -e "\\n\\n ${COL_LIGHT_RED}Installation was interrupted${COL_NC}\\n"
echo -e "Pi-hole's dependencies might be already installed. If you want to remove them you can try to\\n"
echo -e "a) run 'pihole uninstall' \\n"
echo -e "b) Remove the meta-package 'pihole-meta' manually \\n"
@@ -261,11 +231,13 @@ is_command() {
command -v "${check_command}" >/dev/null 2>&1
}
check_fresh_install() {
# in case of an update (can be a v5 -> v6 or v6 -> v6 update) or repair
if [[ -f "${PI_HOLE_V6_CONFIG}" ]] || [[ -f "/etc/pihole/setupVars.conf" ]]; then
fresh_install=false
fi
is_pid1() {
# Checks to see if the given command runs as PID 1
local is_pid1="$1"
# select PID 1, format output to show only CMD column without header
# quietly grep for a match on the function passed parameter
ps --pid 1 --format comm= | grep -q "${is_pid1}"
}
# Compatibility
@@ -284,6 +256,8 @@ package_manager_detect() {
PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true"
# The command we will use to remove packages (used in the uninstaller)
PKG_REMOVE="${PKG_MANAGER} -y remove --purge"
# Update package cache
update_package_cache || exit 1
# If apt-get is not found, check for rpm.
elif is_command rpm; then
@@ -300,15 +274,7 @@ package_manager_detect() {
PKG_COUNT="${PKG_MANAGER} check-update | grep -E '(.i686|.x86|.noarch|.arm|.src|.riscv64)' | wc -l || true"
# The command we will use to remove packages (used in the uninstaller)
PKG_REMOVE="${PKG_MANAGER} remove -y"
# If neither apt-get or yum/dnf package managers were found, check for apk.
elif is_command apk; then
PKG_MANAGER="apk"
UPDATE_PKG_CACHE="${PKG_MANAGER} update"
PKG_INSTALL="${PKG_MANAGER} add"
PKG_COUNT="${PKG_MANAGER} list --upgradable -q | wc -l"
PKG_REMOVE="${PKG_MANAGER} del"
# If neither apt-get or yum/dnf package managers were found
else
# we cannot install required packages
printf " %b No supported package manager found\\n" "${CROSS}"
@@ -319,20 +285,13 @@ package_manager_detect() {
build_dependency_package(){
# This function will build a package that contains all the dependencies needed for Pi-hole
if is_command apk ; then
local str="APK based system detected. Dependencies will be installed using a virtual package named pihole-meta"
printf " %b %s...\\n" "${INFO}" "${str}"
return 0
fi
# remove any leftover build directory that may exist
rm -rf /tmp/pihole-meta_*
# Create a fresh build directory with random name
# Busybox Compat: `mktemp` long flags unsupported
# -d flag is short form of --directory
local tempdir
tempdir="$(mktemp -d /tmp/pihole-meta_XXXXX)"
tempdir="$(mktemp --directory /tmp/pihole-meta_XXXXX)"
chmod 0755 "${tempdir}"
if is_command apt-get; then
@@ -358,7 +317,7 @@ build_dependency_package(){
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf "%b Error: Building pihole-meta.deb failed. %b\\n" "${COL_RED}" "${COL_NC}"
printf "%b Error: Building pihole-meta.deb failed. %b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
return 1
fi
@@ -391,7 +350,7 @@ build_dependency_package(){
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf "%b Error: Building pihole-meta.rpm failed. %b\\n" "${COL_RED}" "${COL_NC}"
printf "%b Error: Building pihole-meta.rpm failed. %b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
return 1
fi
@@ -533,7 +492,7 @@ getGitFiles() {
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
# Update the repo, returning an error message on failure
update_repo "${directory}" || {
printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_RED}" "${COL_NC}"
printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
}
# If it's not a .git repo,
@@ -542,7 +501,7 @@ getGitFiles() {
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
# Attempt to make the repository, showing an error on failure
make_repo "${directory}" "${remoteRepo}" || {
printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_RED}" "${COL_NC}"
printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
}
fi
@@ -620,17 +579,14 @@ Do you wish to continue with an IPv6-only installation?\\n\\n" \
;;
esac
DNS_SERVERS="${DNS_SERVERS_IPV6_ONLY}"
DNS_SERVERS="$DNS_SERVERS_IPV6_ONLY"
printf " %b Proceeding with IPv6 only installation.\\n" "${INFO}"
}
# Get available interfaces that are UP
get_available_interfaces() {
# There may be more than one so it's all stored in a variable
# The ip command list all interfaces that are in the up state
# The awk command filters out any interfaces that have the LOOPBACK flag set
# while using the characters ": " or "@" as a field separator for awk
availableInterfaces=$(ip --oneline link show up | awk -F ': |@' '!/<.*LOOPBACK.*>/ {print $2}')
availableInterfaces=$(ip --oneline link show up | awk '{print $2}' | grep -v "^lo" | cut -d':' -f1 | cut -d'@' -f1)
}
# A function for displaying the dialogs the user sees when first running the installer
@@ -693,7 +649,6 @@ chooseInterface() {
status="OFF"
done
# Disable check for double quote here as we are passing a string with spaces
# shellcheck disable=SC2086
PIHOLE_INTERFACE=$(dialog --no-shadow --keep-tite --output-fd 1 \
--cancel-label "Exit" --ok-label "Select" \
--radiolist "Choose An Interface (press space to toggle selection)" \
@@ -719,9 +674,9 @@ testIPv6() {
# first will contain fda2 (ULA)
printf -v first "%s" "${1%%:*}"
# value1 will contain 253 which is the decimal value corresponding to 0xFD
value1=$(((0x${first}) / 256))
value1=$(((0x$first) / 256))
# value2 will contain 162 which is the decimal value corresponding to 0xA2
value2=$(((0x${first}) % 256))
value2=$(((0x$first) % 256))
# the ULA test is testing for fc00::/7 according to RFC 4193
if (((value1 & 254) == 252)); then
# echoing result to calling function as return value
@@ -746,7 +701,7 @@ find_IPv6_information() {
# For each address in the array above, determine the type of IPv6 address it is
for i in "${IPV6_ADDRESSES[@]}"; do
# Check if it's ULA, GUA, or LL by using the function created earlier
result=$(testIPv6 "${i}")
result=$(testIPv6 "$i")
# If it's a ULA address, use it and store it as a global variable
[[ "${result}" == "ULA" ]] && ULA_ADDRESS="${i%/*}"
# If it's a GUA address, use it and store it as a global variable
@@ -781,7 +736,7 @@ collect_v4andv6_information() {
printf " %b IPv4 address: %s\\n" "${INFO}" "${IPV4_ADDRESS}"
find_IPv6_information
printf " %b IPv6 address: %s\\n" "${INFO}" "${IPV6_ADDRESS}"
if [ "${IPV4_ADDRESS}" == "" ] && [ "${IPV6_ADDRESS}" != "" ]; then
if [ "$IPV4_ADDRESS" == "" ] && [ "$IPV6_ADDRESS" != "" ]; then
confirm_ipv6_only
fi
}
@@ -801,7 +756,7 @@ valid_ip() {
local regex="^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${portelem}$"
# Evaluate the regex, and return the result
[[ ${ip} =~ ${regex} ]]
[[ $ip =~ ${regex} ]]
stat=$?
return "${stat}"
@@ -836,7 +791,7 @@ setDNS() {
DNSChooseOptions=()
local DNSServerCount=0
# Save the old Internal Field Separator in a variable,
OIFS=${IFS}
OIFS=$IFS
# and set the new one to newline
IFS=$'\n'
# Put the DNS Servers into an array
@@ -861,7 +816,7 @@ setDNS() {
result=$?
case ${result} in
"${DIALOG_CANCEL}" | "${DIALOG_ESC}")
printf " %b Cancel was selected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
@@ -898,13 +853,13 @@ If you want to specify a port other than 53, separate it with a hash.\
result=$?
case ${result} in
"${DIALOG_CANCEL}" | "${DIALOG_ESC}")
printf " %b Cancel was selected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
# Clean user input and replace whitespace with comma.
piholeDNS="${piholeDNS//[[:blank:]]/,}"
piholeDNS=$(sed 's/[, \t]\+/,/g' <<<"${piholeDNS}")
# Separate the user input into the two DNS values (separated by a comma)
printf -v PIHOLE_DNS_1 "%s" "${piholeDNS%%,*}"
@@ -952,7 +907,7 @@ If you want to specify a port other than 53, separate it with a hash.\
DNSSettingsCorrect=False
;;
"${DIALOG_ESC}")
printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
@@ -960,7 +915,7 @@ If you want to specify a port other than 53, separate it with a hash.\
done
else
# Save the old Internal Field Separator in a variable,
OIFS=${IFS}
OIFS=$IFS
# and set the new one to newline
IFS=$'\n'
for DNSServer in ${DNS_SERVERS}; do
@@ -1003,7 +958,7 @@ setLogging() {
;;
"${DIALOG_ESC}")
# User pressed <ESC>
printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
@@ -1028,7 +983,7 @@ setPrivacyLevel() {
printf " %b Using privacy level: %s\\n" "${INFO}" "${PRIVACY_LEVEL}"
;;
"${DIALOG_CANCEL}" | "${DIALOG_ESC}")
printf " %b Cancelled privacy level selection.%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Cancelled privacy level selection.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
@@ -1062,7 +1017,7 @@ chooseBlocklists() {
;;
"${DIALOG_ESC}")
# User pressed <ESC>
printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_RED}" "${COL_NC}"
printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
;;
esac
@@ -1182,14 +1137,13 @@ installScripts() {
install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./automated\ install/uninstall.sh
install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/COL_TABLE
install -o "${USER}" -Dm755 -t "${PI_HOLE_BIN_DIR}" pihole
install -Dm644 ./advanced/bash-completion/pihole.bash /etc/bash_completion.d/pihole
install -Dm644 ./advanced/bash-completion/pihole-ftl.bash /etc/bash_completion.d/pihole-FTL
install -Dm644 ./advanced/bash-completion/pihole /etc/bash_completion.d/pihole
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
# Otherwise, show an error and exit
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf "\\t\\t%bError: Local repo %s not found, exiting installer%b\\n" "${COL_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
printf "\\t\\t%bError: Local repo %s not found, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
return 1
fi
}
@@ -1204,13 +1158,13 @@ installConfigs() {
# Install empty custom.list file if it does not exist
if [[ ! -r "${PI_HOLE_CONFIG_DIR}/hosts/custom.list" ]]; then
if ! install -D -T -o pihole -g pihole -m 660 /dev/null "${PI_HOLE_CONFIG_DIR}/hosts/custom.list" &>/dev/null; then
printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_RED}" "${PI_HOLE_CONFIG_DIR}/hosts"
printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}/hosts"
return 1
fi
fi
# Install pihole-FTL systemd or init.d service, based on whether systemd is the init system or not
if ps -p 1 -o comm= | grep -q systemd; then
if is_pid1 systemd; then
install -T -m 0644 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.systemd" '/etc/systemd/system/pihole-FTL.service'
# Remove init.d service if present
@@ -1222,12 +1176,7 @@ installConfigs() {
# Load final service
systemctl daemon-reload
else
local INIT="service"
if is_command openrc; then
INIT="openrc"
fi
install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.${INIT}" '/etc/init.d/pihole-FTL'
install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.service" '/etc/init.d/pihole-FTL'
fi
install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL-prestart.sh" "${PI_HOLE_INSTALL_DIR}/pihole-FTL-prestart.sh"
install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL-poststop.sh" "${PI_HOLE_INSTALL_DIR}/pihole-FTL-poststop.sh"
@@ -1251,6 +1200,10 @@ install_manpage() {
# if not present, create man8 directory
install -d -m 755 /usr/local/share/man/man8
fi
if [[ ! -d "/usr/local/share/man/man5" ]]; then
# if not present, create man5 directory
install -d -m 755 /usr/local/share/man/man5
fi
# Testing complete, copy the files & update the man db
install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/manpages/pihole.8 /usr/local/share/man/man8/pihole.8
@@ -1279,9 +1232,12 @@ stop_service() {
# Can softfail, as process may not be installed when this is called
local str="Stopping ${1} service"
printf " %b %s..." "${INFO}" "${str}"
if is_command systemctl; then
# If systemd is PID 1,
if is_pid1 systemd; then
# use that to restart the service
systemctl -q stop "${1}" || true
else
# Otherwise, fall back to the service command
service "${1}" stop >/dev/null || true
fi
printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}"
@@ -1292,8 +1248,8 @@ restart_service() {
# Local, named variables
local str="Restarting ${1} service"
printf " %b %s..." "${INFO}" "${str}"
# If systemctl exists,
if is_command systemctl; then
# If systemd is PID 1,
if is_pid1 systemd; then
# use that to restart the service
systemctl -q restart "${1}"
else
@@ -1308,12 +1264,10 @@ enable_service() {
# Local, named variables
local str="Enabling ${1} service to start on reboot"
printf " %b %s..." "${INFO}" "${str}"
# If systemctl exists,
if is_command systemctl; then
# If systemd is PID1,
if is_pid1 systemd; then
# use that to enable the service
systemctl -q enable "${1}"
elif is_command openrc; then
rc-update add "${1}" "${2:-default}" &> /dev/null
else
# Otherwise, use update-rc.d to accomplish this
update-rc.d "${1}" defaults >/dev/null
@@ -1326,13 +1280,10 @@ disable_service() {
# Local, named variables
local str="Disabling ${1} service"
printf " %b %s..." "${INFO}" "${str}"
# If systemctl exists,
if is_command systemctl; then
# If systemd is PID1,
if is_pid1 systemd; then
# use that to disable the service
systemctl -q disable --now "${1}"
elif is_command openrc; then
rc-update del "${1}" "${2:-default}" &> /dev/null
systemctl -q disable "${1}"
else
# Otherwise, use update-rc.d to accomplish this
update-rc.d "${1}" disable >/dev/null
@@ -1341,12 +1292,10 @@ disable_service() {
}
check_service_active() {
# If systemctl exists,
if is_command systemctl; then
# If systemd is PID1,
if is_pid1 systemd; then
# use that to check the status of the service
systemctl -q is-enabled "${1}" 2>/dev/null
elif is_command openrc; then
rc-status default boot | grep -q "${1}"
else
# Otherwise, fall back to service command
service "${1}" status &>/dev/null
@@ -1391,7 +1340,7 @@ update_package_cache() {
UPDATE_PKG_CACHE="apt update"
fi
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b Error: Unable to update package cache. Please try \"%s\"%b\\n" "${COL_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}"
printf " %b Error: Unable to update package cache. Please try \"%s\"%b\\n" "${COL_LIGHT_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}"
return 1
fi
}
@@ -1409,7 +1358,7 @@ notify_package_updates_available() {
printf "%b %b %s... up to date!\\n\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s... %s updates available\\n" "${OVER}" "${TICK}" "${str}" "${updatesToInstall}"
printf " %b %bIt is recommended to update your OS after installing the Pi-hole!%b\\n\\n" "${INFO}" "${COL_GREEN}" "${COL_NC}"
printf " %b %bIt is recommended to update your OS after installing the Pi-hole!%b\\n\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}"
fi
}
@@ -1426,11 +1375,11 @@ install_dependent_packages() {
rm /tmp/pihole-meta.deb
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}"
printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_LIGHT_RED}"
return 1
fi
else
printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_RED}"
printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_LIGHT_RED}"
return 1
fi
# Install Fedora/CentOS packages
@@ -1441,34 +1390,15 @@ install_dependent_packages() {
rm /tmp/pihole-meta.rpm
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}"
printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_LIGHT_RED}"
return 1
fi
else
printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_RED}"
printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_LIGHT_RED}"
return 1
fi
# Install Alpine packages
elif is_command apk; then
local repo_str="Ensuring alpine 'community' repo is enabled."
printf "%b %b %s" "${OVER}" "${INFO}" "${repo_str}"
local pattern='^\s*#(.*/community/?)\s*$'
sed -Ei "s:${pattern}:\1:" /etc/apk/repositories
if grep -Eq "${pattern}" /etc/apk/repositories; then
# Repo still commented out = Failure
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${repo_str}"
else
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${repo_str}"
fi
printf " %b %s..." "${INFO}" "${str}"
if { ${PKG_INSTALL} -q -t "pihole-meta=${PIHOLE_META_VERSION_APK}" "${PIHOLE_META_DEPS_APK[@]}" &>/dev/null; }; then
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b Error: Unable to install Pi-hole dependency package.\\n" "${COL_RED}"
return 1
fi
# If neither apt-get or yum/dnf package managers were found
else
# we cannot install the dependency package
printf " %b No supported package manager found\\n" "${CROSS}"
@@ -1493,15 +1423,6 @@ installCron() {
# Randomize update checker time
sed -i "s/59 17/$((1 + RANDOM % 58)) $((12 + RANDOM % 8))/" /etc/cron.d/pihole
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
# Switch off of busybox cron on alpine
if is_command openrc; then
printf " %b Switching from busybox crond to cronie...\\n" "${INFO}"
stop_service crond
disable_service crond
enable_service cronie
restart_service cronie
fi
}
# Gravity is a very important script as it aggregates all of the domains into a single HOSTS formatted list,
@@ -1551,7 +1472,7 @@ create_pihole_user() {
# then create and add her to the pihole group
local str="Creating user 'pihole'"
printf "%b %b %s..." "${OVER}" "${INFO}" "${str}"
if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then
if useradd -r --no-user-group -g pihole -s /usr/sbin/nologin pihole; then
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
@@ -1566,7 +1487,7 @@ create_pihole_user() {
# create and add pihole user to the pihole group
local str="Creating user 'pihole'"
printf "%b %b %s..." "${OVER}" "${INFO}" "${str}"
if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then
if useradd -r --no-user-group -g pihole -s /usr/sbin/nologin pihole; then
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
else
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
@@ -1702,13 +1623,13 @@ checkSelinux() {
if [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -z "${PIHOLE_SELINUX}" ]]; then
printf " Pi-hole does not provide an SELinux policy as the required changes modify the security of your system.\\n"
printf " Please refer to https://wiki.centos.org/HowTos/SELinux if SELinux is required for your deployment.\\n"
printf " This check can be skipped by setting the environment variable %bPIHOLE_SELINUX%b to %btrue%b\\n" "${COL_RED}" "${COL_NC}" "${COL_RED}" "${COL_NC}"
printf " This check can be skipped by setting the environment variable %bPIHOLE_SELINUX%b to %btrue%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" "${COL_LIGHT_RED}" "${COL_NC}"
printf " e.g: export PIHOLE_SELINUX=true\\n"
printf " By setting this variable to true you acknowledge there may be issues with Pi-hole during or after the install\\n"
printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_RED}" "${COL_NC}"
printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"
exit 1
elif [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -n "${PIHOLE_SELINUX}" ]]; then
printf " %b %bSELinux Enforcing detected%b. PIHOLE_SELINUX env variable set - installer will continue\\n" "${INFO}" "${COL_RED}" "${COL_NC}"
printf " %b %bSELinux Enforcing detected%b. PIHOLE_SELINUX env variable set - installer will continue\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}"
fi
}
@@ -1718,9 +1639,9 @@ check_download_exists() {
status=$(curl --head --silent "https://ftl.pi-hole.net/${1}" | head -n 1)
# Check the status code
if grep -q "200" <<<"${status}"; then
if grep -q "200" <<<"$status"; then
return 0
elif grep -q "404" <<<"${status}"; then
elif grep -q "404" <<<"$status"; then
return 1
fi
@@ -1753,7 +1674,7 @@ get_available_branches() {
# Get reachable remote branches, but store STDERR as STDOUT variable
output=$({ git ls-remote --heads --quiet | cut -d'/' -f3- -; } 2>&1)
# echo status for calling function to capture
echo "${output}"
echo "$output"
return
}
@@ -1786,9 +1707,9 @@ checkout_pull_branch() {
oldbranch="$(git symbolic-ref HEAD)"
str="Switching to branch: '${branch}' from '${oldbranch}'"
printf " %b %s" "${INFO}" "${str}"
printf " %b %s" "${INFO}" "$str"
git checkout "${branch}" --quiet || return 1
printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}"
printf "%b %b %s\\n" "${OVER}" "${TICK}" "$str"
# Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git)
chmod -R a+rX "${directory}"
@@ -1803,22 +1724,16 @@ clone_or_reset_repos() {
# If the user wants to repair/update,
if [[ "${repair}" == true ]]; then
printf " %b Resetting local repos\\n" "${INFO}"
# import getFTLConfigValue from utils.sh
source "/opt/pihole/utils.sh"
# Use the configured Web repo location on repair/update
webInterfaceDir=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome")
# Reset the Core repo
resetRepo ${PI_HOLE_LOCAL_REPO} ||
{
printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
exit 1
}
# Reset the Web repo
resetRepo ${webInterfaceDir} ||
{
printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_RED}" "${webInterfaceDir}" "${COL_NC}"
printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceDir}" "${COL_NC}"
exit 1
}
# Otherwise, a fresh installation is happening
@@ -1826,13 +1741,13 @@ clone_or_reset_repos() {
# so get git files for Core
getGitFiles ${PI_HOLE_LOCAL_REPO} ${piholeGitUrl} ||
{
printf " %b Unable to clone %s into %s, unable to continue%b\\n" "${COL_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
printf " %b Unable to clone %s into %s, unable to continue%b\\n" "${COL_LIGHT_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"
exit 1
}
# get the Web git files
getGitFiles ${webInterfaceDir} ${webInterfaceGitUrl} ||
{
printf " %b Unable to clone %s into ${webInterfaceDir}, exiting installer%b\\n" "${COL_RED}" "${webInterfaceGitUrl}" "${COL_NC}"
printf " %b Unable to clone %s into ${webInterfaceDir}, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceGitUrl}" "${COL_NC}"
exit 1
}
fi
@@ -1882,12 +1797,8 @@ FTLinstall() {
# Before stopping FTL, we download the macvendor database
curl -sSL "https://ftl.pi-hole.net/macvendor.db" -o "${PI_HOLE_CONFIG_DIR}/macvendor.db" || true
# If the binary already exists in /usr/bin, then we need to stop the service
# If the binary does not exist (fresh installs), then we can skip this step.
if [[ -f /usr/bin/pihole-FTL ]]; then
stop_service pihole-FTL >/dev/null
fi
# Stop pihole-FTL service if available
stop_service pihole-FTL >/dev/null
# Install the new version with the correct permissions
install -T -m 0755 "${binary}" /usr/bin/pihole-FTL
@@ -1912,7 +1823,7 @@ FTLinstall() {
return 1
}
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b Error: Download of %s/%s failed (checksum error)%b\\n" "${COL_RED}" "${url}" "${binary}" "${COL_NC}"
printf " %b Error: Download of %s/%s failed (checksum error)%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}"
# Remove temp dir
remove_dir "${tempdir}"
@@ -1926,7 +1837,7 @@ FTLinstall() {
}
printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}"
# The URL could not be found
printf " %b Error: URL %s/%s not found%b\\n" "${COL_RED}" "${url}" "${binary}" "${COL_NC}"
printf " %b Error: URL %s/%s not found%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}"
# Remove temp dir
remove_dir "${tempdir}"
@@ -2001,9 +1912,9 @@ get_binary_name() {
l_binary="pihole-FTL-riscv64"
else
# Something else - we try to use 32bit executable and warn the user
if [[ "${machine}" != "i686" ]]; then
if [[ ! "${machine}" == "i686" ]]; then
printf "%b %b %s...\\n" "${OVER}" "${CROSS}" "${str}"
printf " %b %bNot able to detect architecture (unknown: %s), trying x86 (32bit) executable%b\\n" "${INFO}" "${COL_RED}" "${machine}" "${COL_NC}"
printf " %b %bNot able to detect architecture (unknown: %s), trying x86 (32bit) executable%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${machine}" "${COL_NC}"
printf " %b Contact Pi-hole Support if you experience issues (e.g: FTL not running)\\n" "${INFO}"
else
printf "%b %b Detected 32bit (i686) architecture\\n" "${OVER}" "${TICK}"
@@ -2035,18 +1946,18 @@ FTLcheckUpdate() {
local remoteSha1
local localSha1
if [[ "${ftlBranch}" != "master" ]]; then
if [[ ! "${ftlBranch}" == "master" ]]; then
# This is not the master branch
local path
path="${ftlBranch}/${binary}"
# Check whether or not the binary for this FTL branch actually exists. If not, then there is no update!
local status
if ! check_download_exists "${path}"; then
if ! check_download_exists "$path"; then
local status
status=$?
if [ "${status}" -eq 1 ]; then
printf " %b Branch \"%s\" is not available.\\n" "${INFO}" "${ftlBranch}"
printf " %b Use %bpihole checkout ftl [branchname]%b to switch to a valid branch.\\n" "${INFO}" "${COL_GREEN}" "${COL_NC}"
printf " %b Use %bpihole checkout ftl [branchname]%b to switch to a valid branch.\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}"
elif [ "${status}" -eq 2 ]; then
printf " %b Unable to download from ftl.pi-hole.net. Please check your Internet connection and try again later.\\n" "${CROSS}"
return 3
@@ -2132,11 +2043,6 @@ FTLdetect() {
if FTLcheckUpdate "${1}"; then
FTLinstall "${1}" || return 1
else
case $? in
1) :;; # FTL is up-to-date
*) exit 1;; # 404 (2), other HTTP or curl error (3), unknown (4)
esac
fi
}
@@ -2145,11 +2051,11 @@ make_temporary_log() {
TEMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX)
# Open handle 3 for templog
# https://stackoverflow.com/questions/18460186/writing-outputs-to-log-file-and-console
exec 3>"${TEMPLOG}"
exec 3>"$TEMPLOG"
# Delete templog, but allow for addressing via file handle
# This lets us write to the log without having a temporary file on the drive, which
# is meant to be a security measure so there is not a lingering file on the drive during the install process
rm "${TEMPLOG}"
rm "$TEMPLOG"
}
copy_to_install_log() {
@@ -2264,7 +2170,7 @@ main() {
else
# Otherwise, they do not have enough privileges, so let the user know
printf " %b %s\\n" "${INFO}" "${str}"
printf " %b %bScript called with non-root privileges%b\\n" "${INFO}" "${COL_RED}" "${COL_NC}"
printf " %b %bScript called with non-root privileges%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}"
printf " The Pi-hole requires elevated privileges to install and run\\n"
printf " Please check the installer for any concerns regarding this requirement\\n"
printf " Make sure to download this script from a trusted source\\n\\n"
@@ -2288,7 +2194,7 @@ main() {
# Otherwise, tell the user they need to run the script as root, and bail
printf "%b %b Sudo utility check\\n" "${OVER}" "${CROSS}"
printf " %b Sudo is needed for the Web Interface to run pihole commands\\n\\n" "${INFO}"
printf " %b %bPlease re-run this installer as root${COL_NC}\\n" "${INFO}" "${COL_RED}"
printf " %b %bPlease re-run this installer as root${COL_NC}\\n" "${INFO}" "${COL_LIGHT_RED}"
exit 1
fi
fi
@@ -2299,17 +2205,9 @@ main() {
# Check for availability of either the "service" or "systemctl" commands
check_service_command
# Check if this is a fresh install or an update/repair
check_fresh_install
# Check for supported package managers so that we may install dependencies
package_manager_detect
# Update package cache only on apt based systems
if is_command apt-get; then
update_package_cache || exit 1
fi
# Notify user of package availability
notify_package_updates_available
@@ -2330,7 +2228,10 @@ main() {
exit 1
fi
if [[ "${fresh_install}" == false ]]; then
# in case of an update (can be a v5 -> v6 or v6 -> v6 update) or repair
if [[ -f "${PI_HOLE_V6_CONFIG}" ]] || [[ -f "/etc/pihole/setupVars.conf" ]]; then
# retain settings
fresh_install=false
# if it's running unattended,
if [[ "${runUnattended}" == true ]]; then
printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}"
@@ -2439,7 +2340,7 @@ main() {
if [ -n "${PIHOLE_DNS_1}" ]; then
local string="\"${PIHOLE_DNS_1}\""
[ -n "${PIHOLE_DNS_2}" ] && string+=", \"${PIHOLE_DNS_2}\""
setFTLConfigValue "dns.upstreams" "[ ${string} ]"
setFTLConfigValue "dns.upstreams" "[ $string ]"
fi
if [ -n "${QUERY_LOGGING}" ]; then
@@ -2478,10 +2379,8 @@ main() {
printf " %b If you have not done so already, the above IP should be set to static.\\n" "${INFO}"
printf " %b View the web interface at http://pi.hole:${WEBPORT}/admin or http://%s/admin\\n\\n" "${INFO}" "${IPV4_ADDRESS%/*}:${WEBPORT}"
printf " %b Web Interface password: %b%s%b\\n" "${INFO}" "${COL_GREEN}" "${pw}" "${COL_NC}"
printf " %b Web Interface password: %b%s%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${pw}" "${COL_NC}"
printf " %b This can be changed using 'pihole setpassword'\\n\\n" "${INFO}"
printf " %b To allow your user to use all CLI functions without authentication, refer to\\n" "${INFO}"
printf " our documentation at: https://docs.pi-hole.net/main/post-install/\\n\\n"
# Final dialog message to the user
dialog --no-shadow --keep-tite \
@@ -2490,11 +2389,7 @@ main() {
\\n\\nIPv4: ${IPV4_ADDRESS%/*}\
\\nIPv6: ${IPV6_ADDRESS:-"Not Configured"}\
\\nIf you have not done so already, the above IP should be set to static.\
\\nView the web interface at http://pi.hole:${WEBPORT}/admin or http://${IPV4_ADDRESS%/*}:${WEBPORT}/admin\\n\\nYour Admin Webpage login password is ${pw}\
\\n
\\n
\\nTo allow your user to use all CLI functions without authentication,\
\\nrefer to https://docs.pi-hole.net/main/post-install/" "${r}" "${c}"
\\nView the web interface at http://pi.hole/admin:${WEBPORT} or http://${IPV4_ADDRESS%/*}:${WEBPORT}/admin\\n\\nYour Admin Webpage login password is ${pw}" "${r}" "${c}"
INSTALL_TYPE="Installation"
else
@@ -2503,7 +2398,7 @@ main() {
# Display where the log file is
printf "\\n %b The install log is located at: %s\\n" "${INFO}" "${installLogLoc}"
printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_GREEN}" "${INSTALL_TYPE}" "${COL_NC}"
printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${INSTALL_TYPE}" "${COL_NC}"
if [[ "${INSTALL_TYPE}" == "Update" ]]; then
printf "\\n"

View File

@@ -12,13 +12,15 @@
source "/opt/pihole/COL_TABLE"
# shellcheck source="./advanced/Scripts/utils.sh"
source "/opt/pihole/utils.sh"
# getFTLConfigValue() from utils.sh
ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome")
readonly ADMIN_INTERFACE_DIR
while true; do
read -rp " ${QST} Are you sure you would like to remove ${COL_BOLD}Pi-hole${COL_NC}? [y/N] " answer
read -rp " ${QST} Are you sure you would like to remove ${COL_WHITE}Pi-hole${COL_NC}? [y/N] " answer
case ${answer} in
[Yy]* ) break;;
* ) echo -e "${OVER} ${COL_GREEN}Uninstall has been canceled${COL_NC}"; exit 0;;
* ) echo -e "${OVER} ${COL_LIGHT_GREEN}Uninstall has been canceled${COL_NC}"; exit 0;;
esac
done
@@ -27,200 +29,130 @@ str="Root user check"
if [[ ${EUID} -eq 0 ]]; then
echo -e " ${TICK} ${str}"
else
echo -e " ${CROSS} ${str}
Script called with non-root privileges
The Pi-hole requires elevated privileges to uninstall"
exit 1
# Check if sudo is actually installed
# If it isn't, exit because the uninstall can not complete
if [ -x "$(command -v sudo)" ]; then
export SUDO="sudo"
else
echo -e " ${CROSS} ${str}
Script called with non-root privileges
The Pi-hole requires elevated privileges to uninstall"
exit 1
fi
fi
# Get paths for admin interface, log files and database files,
# to allow deletion where user has specified a non-default location
ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome")
FTL_LOG=$(getFTLConfigValue "files.log.ftl")
DNSMASQ_LOG=$(getFTLConfigValue "files.log.dnsmasq")
WEBSERVER_LOG=$(getFTLConfigValue "files.log.webserver")
PIHOLE_DB=$(getFTLConfigValue "files.database")
GRAVITY_DB=$(getFTLConfigValue "files.gravity")
MACVENDOR_DB=$(getFTLConfigValue "files.macvendor")
PI_HOLE_LOCAL_REPO="/etc/.pihole"
# Setting SKIP_INSTALL="true" to source the installer functions without running them
readonly PI_HOLE_FILES_DIR="/etc/.pihole"
SKIP_INSTALL="true"
# shellcheck source="./automated install/basic-install.sh"
source "${PI_HOLE_LOCAL_REPO}/automated install/basic-install.sh"
# Functions and Variables sources from basic-install:
# package_manager_detect(), disable_service(), stop_service(),
# restart service() and is_command()
# PI_HOLE_CONFIG_DIR PI_HOLE_INSTALL_DIR PI_HOLE_LOCAL_REPO
source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
# stop_service() is defined in basic-install.sh
# package_manager_detect() sourced from basic-install.sh
package_manager_detect
removeMetaPackage() {
# Purge Pi-hole meta package
echo ""
echo -ne " ${INFO} Removing Pi-hole meta package...";
eval "${PKG_REMOVE}" "pihole-meta" &> /dev/null;
eval "${SUDO}" "${PKG_REMOVE}" "pihole-meta" &> /dev/null;
echo -e "${OVER} ${INFO} Removed Pi-hole meta package";
}
removeWebInterface() {
removePiholeFiles() {
# Remove the web interface of Pi-hole
echo -ne " ${INFO} Removing Web Interface..."
rm -rf "${ADMIN_INTERFACE_DIR:-/var/www/html/admin/}" &> /dev/null
${SUDO} rm -rf "${ADMIN_INTERFACE_DIR}" &> /dev/null
echo -e "${OVER} ${TICK} Removed Web Interface"
}
removeFTL() {
# Remove FTL and stop any running FTL service
if is_command "pihole-FTL"; then
# service stop & disable from basic_install.sh
stop_service pihole-FTL
disable_service pihole-FTL
echo -ne " ${INFO} Removing pihole-FTL..."
rm -f /etc/systemd/system/pihole-FTL.service &> /dev/null
if [[ -d '/etc/systemd/system/pihole-FTL.service.d' ]]; then
read -rp " ${QST} FTL service override directory /etc/systemd/system/pihole-FTL.service.d detected. Do you wish to remove this from your system? [y/N] " answer
case $answer in
[yY]*)
echo -ne " ${INFO} Removing /etc/systemd/system/pihole-FTL.service.d..."
rm -R /etc/systemd/system/pihole-FTL.service.d &> /dev/null
echo -e "${OVER} ${INFO} Removed /etc/systemd/system/pihole-FTL.service.d"
;;
*) echo -e " ${INFO} Leaving /etc/systemd/system/pihole-FTL.service.d in place.";;
esac
fi
rm -f /etc/init.d/pihole-FTL &> /dev/null
rm -f /usr/bin/pihole-FTL &> /dev/null
echo -e "${OVER} ${TICK} Removed pihole-FTL"
# Force systemd reload after service files are removed
if is_command "systemctl"; then
echo -ne " ${INFO} Restarting systemd..."
systemctl daemon-reload
echo -e "${OVER} ${TICK} Restarted systemd..."
fi
fi
}
removeCronFiles() {
# Attempt to preserve backwards compatibility with older versions
# to guarantee no additional changes were made to /etc/crontab after
# the installation of pihole, /etc/crontab.pihole should be permanently
# preserved.
if [[ -f /etc/crontab.orig ]]; then
mv /etc/crontab /etc/crontab.pihole
mv /etc/crontab.orig /etc/crontab
restart_service cron
${SUDO} mv /etc/crontab /etc/crontab.pihole
${SUDO} mv /etc/crontab.orig /etc/crontab
${SUDO} service cron restart
echo -e " ${TICK} Restored the default system cron"
echo -e " ${INFO} A backup of the most recent crontab is saved at /etc/crontab.pihole"
fi
# Attempt to preserve backwards compatibility with older versions
if [[ -f /etc/cron.d/pihole ]];then
rm -f /etc/cron.d/pihole &> /dev/null
${SUDO} rm -f /etc/cron.d/pihole &> /dev/null
echo -e " ${TICK} Removed /etc/cron.d/pihole"
fi
}
removePiholeFiles() {
# Remove databases (including user specified non-default paths)
rm -f "${PIHOLE_DB:-/etc/pihole/pihole-FTL.db}" &> /dev/null
rm -f "${GRAVITY_DB:-/etc/pihole/gravity.db}" &> /dev/null
rm -f "${MACVENDOR_DB:-/etc/pihole/macvendor.db}" &> /dev/null
# Remove pihole config, repo and local files
rm -rf "${PI_HOLE_CONFIG_DIR:-/etc/pihole}" &> /dev/null
rm -rf "${PI_HOLE_LOCAL_REPO:-/etc/.pihole}" &> /dev/null
rm -rf "${PI_HOLE_INSTALL_DIR:-/opt/pihole}" &> /dev/null
# Remove log files (including user specified non-default paths)
# and rotated logs
# Explicitly escape spaces, in case of trailing space in path before wildcard
rm -f "$(printf '%q' "${FTL_LOG:-/var/log/pihole/FTL.log}")*" &> /dev/null
rm -f "$(printf '%q' "${DNSMASQ_LOG:-/var/log/pihole/pihole.log}")*" &> /dev/null
rm -f "$(printf '%q' "${WEBSERVER_LOG:-/var/log/pihole/webserver.log}")*" &> /dev/null
# remove any remnant log-files from old versions
rm -rf /var/log/*pihole* &> /dev/null
# remove log directory
rm -rf /var/log/pihole &> /dev/null
# remove the pihole command
rm -f /usr/local/bin/pihole &> /dev/null
# remove Pi-hole's bash completion
rm -f /etc/bash_completion.d/pihole &> /dev/null
rm -f /etc/bash_completion.d/pihole-FTL &> /dev/null
# Remove pihole from sudoers for compatibility with old versions
rm -f /etc/sudoers.d/pihole &> /dev/null
${SUDO} rm -rf /var/log/*pihole* &> /dev/null
${SUDO} rm -rf /var/log/pihole/*pihole* &> /dev/null
${SUDO} rm -rf /etc/pihole/ &> /dev/null
${SUDO} rm -rf /etc/.pihole/ &> /dev/null
${SUDO} rm -rf /opt/pihole/ &> /dev/null
${SUDO} rm -f /usr/local/bin/pihole &> /dev/null
${SUDO} rm -f /etc/bash_completion.d/pihole &> /dev/null
${SUDO} rm -f /etc/sudoers.d/pihole &> /dev/null
echo -e " ${TICK} Removed config files"
}
removeManPage() {
# If the pihole manpage exists, then delete
if [[ -f /usr/local/share/man/man8/pihole.8 ]]; then
rm -f /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 /usr/local/share/man/man5/pihole-FTL.conf.5
# Rebuild man-db if present
if is_command "mandb"; then
mandb -q &>/dev/null
# Restore Resolved
if [[ -e /etc/systemd/resolved.conf.orig ]] || [[ -e /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf ]]; then
${SUDO} cp -p /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf &> /dev/null || true
${SUDO} rm -f /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf
systemctl reload-or-restart systemd-resolved
fi
# Remove FTL
if command -v pihole-FTL &> /dev/null; then
echo -ne " ${INFO} Removing pihole-FTL..."
stop_service pihole-FTL
${SUDO} rm -f /etc/systemd/system/pihole-FTL.service
if [[ -d '/etc/systemd/system/pihole-FTL.service.d' ]]; then
read -rp " ${QST} FTL service override directory /etc/systemd/system/pihole-FTL.service.d detected. Do you wish to remove this from your system? [y/N] " answer
case $answer in
[yY]*)
echo -ne " ${INFO} Removing /etc/systemd/system/pihole-FTL.service.d..."
${SUDO} rm -R /etc/systemd/system/pihole-FTL.service.d
echo -e "${OVER} ${INFO} Removed /etc/systemd/system/pihole-FTL.service.d"
;;
*) echo -e " ${INFO} Leaving /etc/systemd/system/pihole-FTL.service.d in place.";;
esac
fi
${SUDO} rm -f /etc/init.d/pihole-FTL
${SUDO} rm -f /usr/bin/pihole-FTL
echo -e "${OVER} ${TICK} Removed pihole-FTL"
fi
# If the pihole manpage exists, then delete and rebuild man-db
if [[ -f /usr/local/share/man/man8/pihole.8 ]]; then
${SUDO} rm -f /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 /usr/local/share/man/man5/pihole-FTL.conf.5
${SUDO} mandb -q &>/dev/null
echo -e " ${TICK} Removed pihole man page"
fi
}
removeUser() {
# If the pihole user exists, then remove
if id "pihole" &> /dev/null; then
if userdel -r pihole 2> /dev/null; then
if ${SUDO} userdel -r pihole 2> /dev/null; then
echo -e " ${TICK} Removed 'pihole' user"
else
echo -e " ${CROSS} Unable to remove 'pihole' user"
fi
fi
# If the pihole group exists, then remove
if getent group "pihole" &> /dev/null; then
if groupdel pihole 2> /dev/null; then
if ${SUDO} groupdel pihole 2> /dev/null; then
echo -e " ${TICK} Removed 'pihole' group"
else
echo -e " ${CROSS} Unable to remove 'pihole' group"
fi
fi
}
restoreResolved() {
# Restore Resolved from saved configuration, if present
if [[ -e /etc/systemd/resolved.conf.orig ]] || [[ -e /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf ]]; then
cp -p /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf &> /dev/null || true
rm -f /etc/systemd/resolved.conf.d/90-pi-hole-disable-stub-listener.conf &> /dev/null
systemctl reload-or-restart systemd-resolved
fi
}
completionMessage() {
echo -e "\\n We're sorry to see you go, but thanks for checking out Pi-hole!
If you need help, reach out to us on GitHub, Discourse, Reddit or Twitter
Reinstall at any time: ${COL_BOLD}curl -sSL https://install.pi-hole.net | bash${COL_NC}
Reinstall at any time: ${COL_WHITE}curl -sSL https://install.pi-hole.net | bash${COL_NC}
${COL_RED}Please reset the DNS on your router/clients to restore internet connectivity${COL_NC}
${COL_LIGHT_RED}Please reset the DNS on your router/clients to restore internet connectivity${COL_NC}
${INFO} Pi-hole's meta package has been removed, use the 'autoremove' function from your package manager to remove unused dependencies${COL_NC}
${COL_GREEN}Uninstallation Complete! ${COL_NC}"
${COL_LIGHT_GREEN}Uninstallation Complete! ${COL_NC}"
}
######### SCRIPT ###########
# The ordering here allows clean uninstallation with nothing
# removed before anything that depends upon it.
# eg removeFTL relies on scripts removed by removePiholeFiles
# removeUser relies on commands removed by removeMetaPackage
package_manager_detect
removeWebInterface
removeCronFiles
restoreResolved
removeManPage
removeFTL
removeUser
removeMetaPackage
removePiholeFiles
completionMessage

View File

@@ -50,7 +50,7 @@ etag_support=false
# Check gravity temp directory
if [ ! -d "${GRAVITY_TMPDIR}" ] || [ ! -w "${GRAVITY_TMPDIR}" ]; then
echo -e " ${COL_RED}Gravity temporary directory does not exist or is not a writeable directory, falling back to /tmp. ${COL_NC}"
echo -e " ${COL_LIGHT_RED}Gravity temporary directory does not exist or is not a writeable directory, falling back to /tmp. ${COL_NC}"
GRAVITY_TMPDIR="/tmp"
fi
@@ -118,12 +118,9 @@ gravity_swap_databases() {
# Swap databases and remove or conditionally rename old database
# Number of available blocks on disk
# Busybox Compat: `stat` long flags unsupported
# -f flag is short form of --file-system.
# -c flag is short form of --format.
availableBlocks=$(stat -f -c "%a" "${gravityDIR}")
availableBlocks=$(stat -f --format "%a" "${gravityDIR}")
# Number of blocks, used by gravity.db
gravityBlocks=$(stat -c "%b" "${gravityDBfile}")
gravityBlocks=$(stat --format "%b" "${gravityDBfile}")
# Only keep the old database if available disk space is at least twice the size of the existing gravity.db.
# Better be safe than sorry...
oldAvail=false
@@ -611,10 +608,8 @@ compareLists() {
# Download specified URL and perform checks on HTTP status and file content
gravity_DownloadBlocklistFromUrl() {
local url="${1}" adlistID="${2}" saveLocation="${3}" compression="${4}" gravity_type="${5}" domain="${6}"
local listCurlBuffer str httpCode success="" ip customUpstreamResolver=""
local modifiedOptions="" listCurlBuffer str httpCode success="" ip cmd_ext
local file_path permissions ip_addr port blocked=false download=true
# modifiedOptions is an array to store all the options used to check if the adlist has been changed upstream
local modifiedOptions=()
# Create temp file to store content on disk instead of RAM
# We don't use '--suffix' here because not all implementations of mktemp support it, e.g. on Alpine
@@ -631,14 +626,14 @@ gravity_DownloadBlocklistFromUrl() {
# Save HTTP ETag to the specified file. An ETag is a caching related header,
# usually returned in a response. If no ETag is sent by the server, an empty
# file is created and can later be used consistently.
modifiedOptions=("${modifiedOptions[@]}" --etag-save "${saveLocation}".etag)
modifiedOptions="--etag-save ${saveLocation}.etag"
if [[ -f "${saveLocation}.etag" ]]; then
# This option makes a conditional HTTP request for the specific ETag read
# from the given file by sending a custom If-None-Match header using the
# stored ETag. This way, the server will only send the file if it has
# changed since the last request.
modifiedOptions=("${modifiedOptions[@]}" --etag-compare "${saveLocation}".etag)
modifiedOptions="${modifiedOptions} --etag-compare ${saveLocation}.etag"
fi
fi
@@ -651,7 +646,7 @@ gravity_DownloadBlocklistFromUrl() {
# Interstingly, this option is not supported by raw.githubusercontent.com
# URLs, however, it is still supported by many older web servers which may
# not support the HTTP ETag method so we keep it as a fallback.
modifiedOptions=("${modifiedOptions[@]}" -z "${saveLocation}")
modifiedOptions="${modifiedOptions} -z ${saveLocation}"
fi
fi
@@ -717,7 +712,7 @@ gravity_DownloadBlocklistFromUrl() {
fi
echo -e "${OVER} ${CROSS} ${str} ${domain} is blocked by one of your lists. Using DNS server ${upstream} instead"
echo -ne " ${INFO} ${str} Pending..."
customUpstreamResolver="--resolve $domain:$port:$ip"
cmd_ext="--resolve $domain:$port:$ip"
fi
fi
@@ -750,12 +745,14 @@ gravity_DownloadBlocklistFromUrl() {
# Check for allowed protocols
if [[ $url != "http"* && $url != "https"* && $url != "file"* && $url != "ftp"* && $url != "ftps"* && $url != "sftp"* ]]; then
echo -e "${OVER} ${CROSS} ${str} Invalid protocol specified. Ignoring list."
echo -e " Ensure your URL starts with a valid protocol like http:// , https:// or file:// ."
echo -e "Ensure your URL starts with a valid protocol like http:// , https:// or file:// ."
download=false
fi
if [[ "${download}" == true ]]; then
httpCode=$(curl --connect-timeout ${curl_connect_timeout} -s -L ${compression:+${compression}} ${customUpstreamResolver:+${customUpstreamResolver}} "${modifiedOptions[@]}" -w "%{http_code}" "${url}" -o "${listCurlBuffer}" 2>/dev/null)
# See https://github.com/pi-hole/pi-hole/issues/6159 for justification of the below disable directive
# shellcheck disable=SC2086
httpCode=$(curl --connect-timeout ${curl_connect_timeout} -s -L ${compression} ${cmd_ext} ${modifiedOptions} -w "%{http_code}" "${url}" -o "${listCurlBuffer}" 2>/dev/null)
fi
case $url in
@@ -824,13 +821,13 @@ gravity_DownloadBlocklistFromUrl() {
if [[ "${done}" != "true" ]]; then
# Determine if cached list has read permission
if [[ -r "${saveLocation}" ]]; then
echo -e " ${CROSS} List download failed: ${COL_GREEN}using previously cached list${COL_NC}"
echo -e " ${CROSS} List download failed: ${COL_LIGHT_GREEN}using previously cached list${COL_NC}"
# Set list status to "download-failed/cached"
database_adlist_status "${adlistID}" "3"
# Add domains to database table file
pihole-FTL "${gravity_type}" parseList "${saveLocation}" "${gravityTEMPfile}" "${adlistID}"
else
echo -e " ${CROSS} List download failed: ${COL_RED}no cached list available${COL_NC}"
echo -e " ${CROSS} List download failed: ${COL_LIGHT_RED}no cached list available${COL_NC}"
# Manually reset these two numbers because we do not call parseList here
database_adlist_number "${adlistID}" 0 0
database_adlist_status "${adlistID}" "4"
@@ -854,7 +851,7 @@ gravity_Table_Count() {
fi
}
# Output count of denied and allowed domains and regex filters
# Output count of blacklisted domains and regex filters
gravity_ShowCount() {
# Here we use the table "gravity" instead of the view "vw_gravity" for speed.
# It's safe to replace it here, because right after a gravity run both will show the exactly same number of domains.
@@ -867,7 +864,7 @@ gravity_ShowCount() {
# Trap Ctrl-C
gravity_Trap() {
trap '{ echo -e "\\n\\n ${INFO} ${COL_RED}User-abort detected${COL_NC}"; gravity_Cleanup "error"; }' INT
trap '{ echo -e "\\n\\n ${INFO} ${COL_LIGHT_RED}User-abort detected${COL_NC}"; gravity_Cleanup "error"; }' INT
}
# Clean up after Gravity upon exit or cancellation
@@ -947,7 +944,7 @@ database_recovery() {
else
echo -e "${OVER} ${CROSS} ${str} - the following errors happened:"
while IFS= read -r line; do echo " - $line"; done <<<"$result"
echo -e " ${CROSS} Recovery failed. Try \"pihole -g -r recreate\" instead."
echo -e " ${CROSS} Recovery failed. Try \"pihole -r recreate\" instead."
exit 1
fi
echo ""
@@ -1130,7 +1127,7 @@ fi
if [[ "${forceDelete:-}" == true ]]; then
str="Deleting existing list cache"
echo -ne " ${INFO} ${str}..."
echo -ne "${INFO} ${str}..."
rm "${listsCacheDir}/list.*" 2>/dev/null || true
echo -e "${OVER} ${TICK} ${str}"

View File

@@ -105,9 +105,9 @@ Available commands and options:
Flush the Pi-hole log
.br
\fB-r, repair\fR
\fB-r, reconfigure\fR
.br
Repair Pi-hole subsystems
Reconfigure or Repair Pi-hole subsystems
.br
\fB-t, tail\fR [arg]
@@ -268,7 +268,7 @@ Allow-/denylist manipulation
\fBpihole --regex "ad.*\\.example\\.com$"\fR
.br
Adds "ad.*\\.example\\.com$" to the regex denylist.
Adds "ad.*\\.example\\.com$" to the regex blacklist.
Would block all subdomains of example.com which start with "ad"
.br
@@ -317,10 +317,9 @@ Switching Pi-hole subsystem branches
Switch to core development branch
.br
\fBpihole networkflush\fR
\fBpihole arpflush\fR
.br
Flush information stored in Pi-hole's network table
Add '--arp' to additionally flush the ARP table
Flush information stored in Pi-hole's network tables
.br
\fBpihole api stats/summary\fR

104
pihole
View File

@@ -9,7 +9,7 @@
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
PI_HOLE_SCRIPT_DIR="/opt/pihole"
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
# PI_HOLE_BIN_DIR is not readonly here because in some functions (checkout),
# they might get set again when the installer is sourced. This causes an
@@ -20,7 +20,7 @@ readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
# shellcheck source=./advanced/Scripts/COL_TABLE
source "${colfile}"
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source=./advanced/Scripts/utils.sh
source "${utilsfile}"
@@ -96,18 +96,8 @@ flushFunc() {
exit 0
}
# Deprecated function, should be removed in the future
# use networkFlush instead
arpFunc() {
shift
echo -e " ${INFO} The 'arpflush' command is deprecated, use 'networkflush' instead"
"${PI_HOLE_SCRIPT_DIR}"/piholeNetworkFlush.sh "$@"
exit 0
}
networkFlush() {
shift
"${PI_HOLE_SCRIPT_DIR}"/piholeNetworkFlush.sh "$@"
"${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@"
exit 0
}
@@ -157,11 +147,10 @@ uninstallFunc() {
versionFunc() {
exec "${PI_HOLE_SCRIPT_DIR}"/version.sh
exit 0
}
reloadDNS() {
local svcOption svc str output status pid icon FTL_PID_FILE sigrtmin
local svcOption svc str output status pid icon FTL_PID_FILE
svcOption="${1:-reload}"
# get the current path to the pihole-FTL.pid
@@ -180,10 +169,7 @@ reloadDNS() {
str="FTL is not running"
icon="${INFO}"
else
sigrtmin="$(pihole-FTL sigrtmin 2>/dev/null)"
# Make sure sigrtmin is a number, otherwise fallback to RTMIN
[[ "${sigrtmin}" =~ ^[0-9]+$ ]] || unset sigrtmin
svc="kill -${sigrtmin:-RTMIN} ${pid}"
svc="kill -RTMIN ${pid}"
str="Reloading DNS lists"
icon="${TICK}"
fi
@@ -252,7 +238,7 @@ Time:
fi
if [[ ${error} == true ]];then
echo -e " ${COL_RED}Unknown format for blocking timer!${COL_NC}"
echo -e " ${COL_LIGHT_RED}Unknown format for blocking timer!${COL_NC}"
echo -e " Try 'pihole disable --help' for more information."
exit 1
fi
@@ -278,7 +264,6 @@ Time:
LogoutAPI
echo -e "${OVER} ${TICK} ${str}"
exit 0
}
piholeLogging() {
@@ -308,7 +293,7 @@ Options:
echo -e " ${INFO} Enabling logging..."
local str="Logging has been enabled!"
else
echo -e " ${COL_RED}Invalid option${COL_NC}
echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}
Try 'pihole logging --help' for more information."
exit 1
fi
@@ -404,14 +389,14 @@ tailFunc() {
echo -e " ${INFO} Press Ctrl-C to exit"
# Get logfile path
LOGFILE=$(getFTLConfigValue files.log.dnsmasq)
readonly LOGFILE
LOGFILE=$(getFTLConfigValue files.log.dnsmasq)
# Strip date from each line
# Color blocklist/denylist/wildcard entries as red
# Color A/AAAA/DHCP strings as white
# Color everything else as gray
tail -f $LOGFILE | grep --line-buffered -- "${1}" | sed -E \
tail -f $LOGFILE | grep --line-buffered "${1}" | sed -E \
-e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \
-e "s,(.*(denied |gravity blocked ).*),${COL_RED}&${COL_NC}," \
-e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \
@@ -534,8 +519,7 @@ Options:
reloadlists Update the lists WITHOUT flushing the cache or restarting the DNS server
checkout Switch Pi-hole subsystems to a different GitHub branch
Add '-h' for more info on checkout usage
networkflush Flush information stored in Pi-hole's network tables
Add '--arp' to additionally flush the ARP table ";
arpflush Flush information stored in Pi-hole's network tables";
exit 0
}
@@ -544,7 +528,7 @@ if [[ $# = 0 ]]; then
fi
# functions that do not require sudo power
need_root=
need_root=1
case "${1}" in
"-h" | "help" | "--help" ) helpFunc;;
"-v" | "version" ) versionFunc;;
@@ -552,32 +536,31 @@ case "${1}" in
"-q" | "query" ) queryFunc "$@";;
"status" ) statusFunc "$2";;
"tricorder" ) tricorderFunc;;
"allow" | "allowlist" ) listFunc "$@";;
"deny" | "denylist" ) listFunc "$@";;
"--wild" | "wildcard" ) listFunc "$@";;
"--regex" | "regex" ) listFunc "$@";;
"--allow-regex" | "allow-regex" ) listFunc "$@";;
"--allow-wild" | "allow-wild" ) listFunc "$@";;
"enable" ) piholeEnable true "$2";;
"disable" ) piholeEnable false "$2";;
"api" ) shift; apiFunc "$@"; exit 0;;
# we need to add all arguments that require sudo power to not trigger the * argument
"-f" | "flush" ) need_root=true;;
"-up" | "updatePihole" ) need_root=true;;
"-r" | "repair" ) need_root=true;;
"-l" | "logging" ) need_root=true;;
"uninstall" ) need_root=true;;
"-d" | "debug" ) need_root=true;;
"-g" | "updateGravity" ) need_root=true;;
"reloaddns" ) need_root=true;;
"reloadlists" ) need_root=true;;
"setpassword" ) need_root=true;;
"checkout" ) need_root=true;;
"updatechecker" ) need_root=true;;
"arpflush" ) need_root=true;; # Deprecated, use networkflush instead
"networkflush" ) need_root=true;;
"-t" | "tail" ) need_root=true;;
"allow" | "allowlist" ) need_root=0;;
"deny" | "denylist" ) need_root=0;;
"--wild" | "wildcard" ) need_root=0;;
"--regex" | "regex" ) need_root=0;;
"--allow-regex" | "allow-regex" ) need_root=0;;
"--allow-wild" | "allow-wild" ) need_root=0;;
"-f" | "flush" ) ;;
"-up" | "updatePihole" ) ;;
"-r" | "repair" ) ;;
"-l" | "logging" ) ;;
"uninstall" ) ;;
"enable" ) need_root=0;;
"disable" ) need_root=0;;
"-d" | "debug" ) ;;
"-g" | "updateGravity" ) ;;
"reloaddns" ) ;;
"reloadlists" ) ;;
"setpassword" ) ;;
"checkout" ) ;;
"updatechecker" ) ;;
"arpflush" ) ;;
"-t" | "tail" ) ;;
"api" ) need_root=0;;
* ) helpFunc;;
esac
@@ -587,17 +570,22 @@ if [[ -z ${USER} ]]; then
USER=$(whoami)
fi
# Check if the current user is not root and if the command
# Check if the current user is neither root nor pihole and if the command
# requires root. If so, exit with an error message.
# Add an exception for the user "pihole" to allow the webserver running gravity
if [[ ( $EUID -ne 0 && ${USER} != "pihole" ) && -n "${need_root}" ]]; then
echo -e " ${CROSS} This Pi-hole command requires root privileges, try:"
if [[ $EUID -ne 0 && ${USER} != "pihole" && need_root -eq 1 ]];then
echo -e " ${CROSS} The Pi-hole command requires root privileges, try:"
echo -e " ${COL_GREEN}sudo pihole $*${COL_NC}"
exit 1
fi
# Handle redirecting to specific functions based on arguments
case "${1}" in
"allow" | "allowlist" ) listFunc "$@";;
"deny" | "denylist" ) listFunc "$@";;
"--wild" | "wildcard" ) listFunc "$@";;
"--regex" | "regex" ) listFunc "$@";;
"--allow-regex" | "allow-regex" ) listFunc "$@";;
"--allow-wild" | "allow-wild" ) listFunc "$@";;
"-d" | "debug" ) debugFunc "$@";;
"-f" | "flush" ) flushFunc "$@";;
"-up" | "updatePihole" ) updatePiholeFunc "$@";;
@@ -605,13 +593,15 @@ case "${1}" in
"-g" | "updateGravity" ) updateGravityFunc "$@";;
"-l" | "logging" ) piholeLogging "$@";;
"uninstall" ) uninstallFunc;;
"enable" ) piholeEnable true "$2";;
"disable" ) piholeEnable false "$2";;
"reloaddns" ) reloadDNS "reload";;
"reloadlists" ) reloadDNS "reload-lists";;
"setpassword" ) SetWebPassword "$@";;
"checkout" ) piholeCheckoutFunc "$@";;
"updatechecker" ) shift; updateCheckFunc "$@";;
"arpflush" ) arpFunc "$@";; # Deprecated, use networkflush instead
"networkflush" ) networkFlush "$@";;
"arpflush" ) arpFunc "$@";;
"-t" | "tail" ) tailFunc "$2";;
"api" ) shift; apiFunc "$@";;
* ) helpFunc;;
esac

0
test/__init__.py Normal file
View File

View File

@@ -1,18 +0,0 @@
FROM alpine:3.21
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole
RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories
RUN apk --no-cache add bash coreutils curl git jq openrc shadow
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV SKIP_INSTALL=true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

View File

@@ -1,18 +0,0 @@
FROM alpine:3.22
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole
RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories
RUN apk --no-cache add bash coreutils curl git jq openrc shadow
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV SKIP_INSTALL=true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

View File

@@ -1,18 +0,0 @@
FROM alpine:3.23
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole
RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories
RUN apk --no-cache add bash coreutils curl git jq openrc shadow
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV SKIP_INSTALL=true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

View File

@@ -1,7 +1,7 @@
FROM quay.io/centos/centos:stream10
# Disable SELinux
RUN echo "SELINUX=disabled" > /etc/selinux/config
RUN yum install -y --allowerasing curl git initscripts
RUN yum install -y --allowerasing curl git
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole

View File

@@ -1,7 +1,7 @@
FROM quay.io/centos/centos:stream9
# Disable SELinux
RUN echo "SELINUX=disabled" > /etc/selinux/config
RUN yum install -y --allowerasing curl git initscripts
RUN yum install -y --allowerasing curl git
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole

View File

@@ -1,16 +0,0 @@
FROM buildpack-deps:trixie-scm
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV SKIP_INSTALL=true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

View File

@@ -1,5 +1,5 @@
FROM fedora:40
RUN dnf install -y git initscripts
RUN dnf install -y git
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole

View File

@@ -1,5 +1,5 @@
FROM fedora:41
RUN dnf install -y git initscripts
RUN dnf install -y git
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole

View File

@@ -1,5 +1,5 @@
FROM fedora:42
RUN dnf install -y git initscripts
RUN dnf install -y git gawk
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole

View File

@@ -1,17 +0,0 @@
FROM fedora:43
RUN dnf install -y git initscripts
ENV GITDIR=/etc/.pihole
ENV SCRIPTDIR=/opt/pihole
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $GITDIR/advanced/Scripts/COL_TABLE $SCRIPTDIR/
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV SKIP_INSTALL=true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

219
test/conftest.py Normal file
View File

@@ -0,0 +1,219 @@
import pytest
import testinfra
import testinfra.backend.docker
import subprocess
from textwrap import dedent
IMAGE = "pytest_pihole:test_container"
tick_box = "[✓]"
cross_box = "[✗]"
info_box = "[i]"
# Monkeypatch sh to bash, if they ever support non hard code /bin/sh this can go away
# https://github.com/pytest-dev/pytest-testinfra/blob/master/testinfra/backend/docker.py
def run_bash(self, command, *args, **kwargs):
cmd = self.get_command(command, *args)
if self.user is not None:
out = self.run_local(
"docker exec -u %s %s /bin/bash -c %s", self.user, self.name, cmd
)
else:
out = self.run_local("docker exec %s /bin/bash -c %s", self.name, cmd)
out.command = self.encode(cmd)
return out
testinfra.backend.docker.DockerBackend.run = run_bash
@pytest.fixture
def host():
# run a container
docker_id = (
subprocess.check_output(["docker", "run", "-t", "-d", "--cap-add=ALL", IMAGE])
.decode()
.strip()
)
# return a testinfra connection to the container
docker_host = testinfra.get_host("docker://" + docker_id)
yield docker_host
# at the end of the test suite, destroy the container
subprocess.check_call(["docker", "rm", "-f", docker_id])
# Helper functions
def mock_command(script, args, container):
"""
Allows for setup of commands we don't really want to have to run for real
in unit tests
"""
full_script_path = "/usr/local/bin/{}".format(script)
mock_script = dedent(
r"""\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1" in""".format(
script=script
)
)
for k, v in args.items():
case = dedent(
"""
{arg})
echo {res}
exit {retcode}
;;""".format(
arg=k, res=v[0], retcode=v[1]
)
)
mock_script += case
mock_script += dedent(
"""
esac"""
)
container.run(
"""
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}""".format(
script=full_script_path, content=mock_script, scriptlog=script
)
)
def mock_command_passthrough(script, args, container):
"""
Per other mock_command* functions, allows intercepting of commands we don't want to run for real
in unit tests, however also allows only specific arguments to be mocked. Anything not defined will
be passed through to the actual command.
Example use-case: mocking `git pull` but still allowing `git clone` to work as intended
"""
orig_script_path = container.check_output("command -v {}".format(script))
full_script_path = "/usr/local/bin/{}".format(script)
mock_script = dedent(
r"""\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1" in""".format(
script=script
)
)
for k, v in args.items():
case = dedent(
"""
{arg})
echo {res}
exit {retcode}
;;""".format(
arg=k, res=v[0], retcode=v[1]
)
)
mock_script += case
mock_script += dedent(
r"""
*)
{orig_script_path} "\$@"
;;""".format(
orig_script_path=orig_script_path
)
)
mock_script += dedent(
"""
esac"""
)
container.run(
"""
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}""".format(
script=full_script_path, content=mock_script, scriptlog=script
)
)
def mock_command_run(script, args, container):
"""
Allows for setup of commands we don't really want to have to run for real
in unit tests
"""
full_script_path = "/usr/local/bin/{}".format(script)
mock_script = dedent(
r"""\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1 \$2" in""".format(
script=script
)
)
for k, v in args.items():
case = dedent(
"""
\"{arg}\")
echo {res}
exit {retcode}
;;""".format(
arg=k, res=v[0], retcode=v[1]
)
)
mock_script += case
mock_script += dedent(
"""
esac"""
)
container.run(
"""
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}""".format(
script=full_script_path, content=mock_script, scriptlog=script
)
)
def mock_command_2(script, args, container):
"""
Allows for setup of commands we don't really want to have to run for real
in unit tests
"""
full_script_path = "/usr/local/bin/{}".format(script)
mock_script = dedent(
r"""\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1 \$2" in""".format(
script=script
)
)
for k, v in args.items():
case = dedent(
"""
\"{arg}\")
echo \"{res}\"
exit {retcode}
;;""".format(
arg=k, res=v[0], retcode=v[1]
)
)
mock_script += case
mock_script += dedent(
"""
esac"""
)
container.run(
"""
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}""".format(
script=full_script_path, content=mock_script, scriptlog=script
)
)
def run_script(Pihole, script):
result = Pihole.run(script)
assert result.rc == 0
return result

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env bash
# Mock command helpers for BATS tests.
#
# These are the BATS equivalents of the mock_command* functions in conftest.py.
# Each function writes a bash case-statement script to /usr/local/bin/<name>
# inside the container, allowing tests to intercept command invocations.
#
# Usage:
# mock_command CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...]
# mock_command_2 CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...]
# mock_command_passthrough CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [...]
#
# mock_command: matches on $1 (first argument); unquoted case pattern
# mock_command_2: matches on "$1 $2" (first two args joined); quoted pattern
# mock_command_passthrough: like mock_command but falls through to real binary
#
# Use '*' as ARG for a catch-all case (only works in mock_command and
# mock_command_passthrough; in mock_command_2 it matches the literal string '*').
#
# Content is transferred to the container via base64 to avoid quoting issues.
_write_mock_to_container() {
local container="$1" script_name="$2" script_content="$3"
# base64 alphabet is [A-Za-z0-9+/=] — safe to single-quote in the shell
local encoded
encoded=$(printf '%s' "$script_content" | base64 | tr -d '\n')
docker exec "$container" bash -c \
"printf '%s' '${encoded}' | base64 -d > /usr/local/bin/${script_name} && chmod +x /usr/local/bin/${script_name} && rm -f /var/log/${script_name}"
}
# mock_command — matches on $1
mock_command() {
local container="$1" script_name="$2"
shift 2
local script
script='#!/bin/bash -e'$'\n'
script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n'
script+='case "$1" in'$'\n'
while (( $# >= 3 )); do
local arg="$1" output="$2" rc="$3"
shift 3
script+=" ${arg})"$'\n'
script+=" echo ${output}"$'\n'
script+=" exit ${rc}"$'\n'
script+=" ;;"$'\n'
done
script+='esac'$'\n'
_write_mock_to_container "$container" "$script_name" "$script"
}
# mock_command_2 — matches on "$1 $2" (quoted pattern, quoted echo output)
mock_command_2() {
local container="$1" script_name="$2"
shift 2
local script
script='#!/bin/bash -e'$'\n'
script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n'
script+='case "$1 $2" in'$'\n'
while (( $# >= 3 )); do
local arg="$1" output="$2" rc="$3"
shift 3
script+=" \"${arg}\")"$'\n'
script+=" echo \"${output}\""$'\n'
script+=" exit ${rc}"$'\n'
script+=" ;;"$'\n'
done
script+='esac'$'\n'
_write_mock_to_container "$container" "$script_name" "$script"
}
# mock_command_passthrough — matches on $1; falls through to real binary for
# unmatched arguments
mock_command_passthrough() {
local container="$1" script_name="$2"
shift 2
# Find the real binary path before we shadow it
local orig_path
orig_path=$(docker exec "$container" bash -c "command -v ${script_name}")
local script
script='#!/bin/bash -e'$'\n'
script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n'
script+='case "$1" in'$'\n'
while (( $# >= 3 )); do
local arg="$1" output="$2" rc="$3"
shift 3
script+=" ${arg})"$'\n'
script+=" echo ${output}"$'\n'
script+=" exit ${rc}"$'\n'
script+=" ;;"$'\n'
done
script+=' *)'$'\n'
script+=" ${orig_path} \"\$@\""$'\n'
script+=' ;;'$'\n'
script+='esac'$'\n'
_write_mock_to_container "$container" "$script_name" "$script"
}

6
test/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
pyyaml == 6.0.2
pytest == 8.3.5
pytest-xdist == 3.6.1
pytest-testinfra == 10.2.2
tox == 4.26.0
pytest-clarity == 1.0.1

View File

@@ -1,91 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ---------------------------------------------------------------------------
# Distro selection
# ---------------------------------------------------------------------------
if [[ -z "${DISTRO:-}" ]]; then
echo "Error: DISTRO is required."
echo "Example: DISTRO=debian_12 bash test/run.sh"
echo ""
echo "Available distros:"
ls _*.Dockerfile | sed 's/^_//;s/\.Dockerfile$//' | sort
exit 1
fi
DOCKERFILE="_${DISTRO}.Dockerfile"
if [[ ! -f "$DOCKERFILE" ]]; then
echo "Error: Dockerfile not found: $DOCKERFILE"
exit 1
fi
# Determine distro family to select which test files to run.
# rhel: CentOS/Fedora — includes SELinux tests
# alpine: Alpine Linux
# debian: Debian/Ubuntu (default)
distro_family() {
case "$1" in
centos_* | fedora_*) echo "rhel" ;;
alpine_*) echo "alpine" ;;
*) echo "debian" ;;
esac
}
DISTRO_FAMILY=$(distro_family "$DISTRO")
# ---------------------------------------------------------------------------
# Build the test image
# ---------------------------------------------------------------------------
IMAGE_TAG="pihole_test:${DISTRO}"
docker buildx build \
--load \
--progress plain \
-f "$DOCKERFILE" \
-t "$IMAGE_TAG" \
../
# ---------------------------------------------------------------------------
# Install BATS and helper libraries (on-demand, not committed)
# ---------------------------------------------------------------------------
mkdir -p libs
if [[ ! -d libs/bats ]]; then
echo "Cloning bats-core..."
git clone --depth=1 --quiet https://github.com/bats-core/bats-core libs/bats
fi
if [[ ! -d libs/bats-support ]]; then
echo "Cloning bats-support..."
git clone --depth=1 --quiet https://github.com/bats-core/bats-support libs/bats-support
fi
if [[ ! -d libs/bats-assert ]]; then
echo "Cloning bats-assert..."
git clone --depth=1 --quiet https://github.com/bats-core/bats-assert libs/bats-assert
fi
BATS="${BATS:-libs/bats/bin/bats}"
# ---------------------------------------------------------------------------
# Run tests
# ---------------------------------------------------------------------------
export IMAGE_TAG DISTRO DISTRO_FAMILY
TEST_FILES=(
test_automated_install.bats
test_ftl.bats
test_network.bats
test_utils.bats
)
[[ "$DISTRO_FAMILY" == "rhel" ]] && TEST_FILES+=(test_selinux.bats)
# Use pretty output only when stdout is a real terminal; fall back to TAP in CI.
# Parallelise across files with --jobs when GNU parallel is available.
BATS_FLAGS=()
[[ -t 1 ]] && BATS_FLAGS+=("-p")
command -v parallel > /dev/null 2>&1 && BATS_FLAGS+=("--jobs" "$(nproc)")
"$BATS" "${BATS_FLAGS[@]}" "${TEST_FILES[@]}"

7
test/setup.py Normal file
View File

@@ -0,0 +1,7 @@
from setuptools import setup
setup(
py_modules=[],
setup_requires=["pytest-runner"],
tests_require=["pytest"],
)

View File

@@ -0,0 +1,506 @@
import pytest
from textwrap import dedent
import re
from .conftest import (
tick_box,
info_box,
cross_box,
mock_command,
mock_command_run,
mock_command_2,
mock_command_passthrough,
run_script,
)
FTL_BRANCH = "development"
def test_supported_package_manager(host):
"""
confirm installer exits when no supported package manager found
"""
# break supported package managers
host.run("rm -rf /usr/bin/apt-get")
host.run("rm -rf /usr/bin/rpm")
package_manager_detect = host.run(
"""
source /opt/pihole/basic-install.sh
package_manager_detect
"""
)
expected_stdout = cross_box + " No supported package manager found"
assert expected_stdout in package_manager_detect.stdout
# assert package_manager_detect.rc == 1
def test_selinux_not_detected(host):
"""
confirms installer continues when SELinux configuration file does not exist
"""
check_selinux = host.run(
"""
rm -f /etc/selinux/config
source /opt/pihole/basic-install.sh
checkSelinux
"""
)
expected_stdout = info_box + " SELinux not detected"
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0
def get_directories_recursive(host, directory):
if directory is None:
return directory
# returns all non-hidden subdirs of 'directory'
dirs_raw = host.run("find {} -type d -not -path '*/.*'".format(directory))
dirs = list(filter(bool, dirs_raw.stdout.splitlines()))
return dirs
def test_installPihole_fresh_install_readableFiles(host):
"""
confirms all necessary files are readable by pihole user
"""
# dialog returns Cancel for user prompt
mock_command("dialog", {"*": ("", "0")}, host)
# mock git pull
mock_command_passthrough("git", {"pull": ("", "0")}, host)
# mock PID 1 to pretend to be systemd
mock_command_2(
"ps",
{
"--pid 1": ("systemd", "0"),
},
host,
)
# mock systemctl to not start FTL
mock_command_2(
"systemctl",
{
"enable pihole-FTL": ("", "0"),
"restart pihole-FTL": ("", "0"),
"start pihole-FTL": ("", "0"),
"stop pihole-FTL": ("", "0"),
"*": ('echo "systemctl call with $@"', "0"),
},
host,
)
# try to install man
host.run("command -v apt-get > /dev/null && apt-get install -qq man")
host.run("command -v dnf > /dev/null && dnf install -y man")
host.run("command -v yum > /dev/null && yum install -y man")
# Workaround to get FTLv6 installed until it reaches master branch
host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch')
install = host.run(
"""
export TERM=xterm
export DEBIAN_FRONTEND=noninteractive
umask 0027
runUnattended=true
fresh_install=false
source /opt/pihole/basic-install.sh > /dev/null
runUnattended=true
fresh_install=false
main
/opt/pihole/pihole-FTL-prestart.sh
"""
)
assert 0 == install.rc
maninstalled = True
if (info_box + " man not installed") in install.stdout:
maninstalled = False
if (info_box + " man pages not installed") in install.stdout:
maninstalled = False
piholeuser = "pihole"
exit_status_success = 0
test_cmd = 'su --shell /bin/bash --command "test -{0} {1}" -p {2}'
# check files in /etc/pihole for read, write and execute permission
check_etc = test_cmd.format("r", "/etc/pihole", piholeuser)
actual_rc = host.run(check_etc).rc
assert exit_status_success == actual_rc
check_etc = test_cmd.format("x", "/etc/pihole", piholeuser)
actual_rc = host.run(check_etc).rc
assert exit_status_success == actual_rc
# readable and writable dhcp.leases
check_leases = test_cmd.format("r", "/etc/pihole/dhcp.leases", piholeuser)
actual_rc = host.run(check_leases).rc
assert exit_status_success == actual_rc
check_leases = test_cmd.format("w", "/etc/pihole/dhcp.leases", piholeuser)
actual_rc = host.run(check_leases).rc
# readable install.log
check_install = test_cmd.format("r", "/etc/pihole/install.log", piholeuser)
actual_rc = host.run(check_install).rc
assert exit_status_success == actual_rc
# readable versions
check_localversion = test_cmd.format("r", "/etc/pihole/versions", piholeuser)
actual_rc = host.run(check_localversion).rc
assert exit_status_success == actual_rc
# readable macvendor.db
check_macvendor = test_cmd.format("r", "/etc/pihole/macvendor.db", piholeuser)
actual_rc = host.run(check_macvendor).rc
assert exit_status_success == actual_rc
# check readable and executable manpages
if maninstalled is True:
check_man = test_cmd.format("x", "/usr/local/share/man", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format("r", "/usr/local/share/man", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format("x", "/usr/local/share/man/man8", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format("r", "/usr/local/share/man/man8", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format("x", "/usr/local/share/man/man5", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format("r", "/usr/local/share/man/man5", piholeuser)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
check_man = test_cmd.format(
"r", "/usr/local/share/man/man8/pihole.8", piholeuser
)
actual_rc = host.run(check_man).rc
assert exit_status_success == actual_rc
# check not readable cron file
check_sudo = test_cmd.format("x", "/etc/cron.d/", piholeuser)
actual_rc = host.run(check_sudo).rc
assert exit_status_success == actual_rc
check_sudo = test_cmd.format("r", "/etc/cron.d/", piholeuser)
actual_rc = host.run(check_sudo).rc
assert exit_status_success == actual_rc
check_sudo = test_cmd.format("r", "/etc/cron.d/pihole", piholeuser)
actual_rc = host.run(check_sudo).rc
assert exit_status_success == actual_rc
directories = get_directories_recursive(host, "/etc/.pihole/")
for directory in directories:
check_pihole = test_cmd.format("r", directory, piholeuser)
actual_rc = host.run(check_pihole).rc
check_pihole = test_cmd.format("x", directory, piholeuser)
actual_rc = host.run(check_pihole).rc
findfiles = 'find "{}" -maxdepth 1 -type f -exec echo {{}} \\;;'
filelist = host.run(findfiles.format(directory))
files = list(filter(bool, filelist.stdout.splitlines()))
for file in files:
check_pihole = test_cmd.format("r", file, piholeuser)
actual_rc = host.run(check_pihole).rc
def test_update_package_cache_success_no_errors(host):
"""
confirms package cache was updated without any errors
"""
updateCache = host.run(
"""
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
"""
)
expected_stdout = tick_box + " Update local cache of available packages"
assert expected_stdout in updateCache.stdout
assert "error" not in updateCache.stdout.lower()
def test_update_package_cache_failure_no_errors(host):
"""
confirms package cache was not updated
"""
mock_command("apt-get", {"update": ("", "1")}, host)
updateCache = host.run(
"""
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
"""
)
expected_stdout = cross_box + " Update local cache of available packages"
assert expected_stdout in updateCache.stdout
assert "Error: Unable to update package cache." in updateCache.stdout
@pytest.mark.parametrize(
"arch,detected_string,supported",
[
("aarch64", "AArch64 (64 Bit ARM)", True),
("armv6", "ARMv6", True),
("armv7l", "ARMv7 (or newer)", True),
("armv7", "ARMv7 (or newer)", True),
("armv8a", "ARMv7 (or newer)", True),
("x86_64", "x86_64", True),
("riscv64", "riscv64", True),
("mips", "mips", False),
],
)
def test_FTL_detect_no_errors(host, arch, detected_string, supported):
"""
confirms only correct package is downloaded for FTL engine
"""
# mock uname to return passed platform
mock_command("uname", {"-m": (arch, "0")}, host)
# mock readelf to respond with passed CPU architecture
mock_command_2(
"readelf",
{
"-A /bin/sh": ("Tag_CPU_arch: " + arch, "0"),
"-A /usr/bin/sh": ("Tag_CPU_arch: " + arch, "0"),
"-A /usr/sbin/sh": ("Tag_CPU_arch: " + arch, "0"),
},
host,
)
host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch')
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=$(get_binary_name)
binary="pihole-FTL${funcOutput##*pihole-FTL}"
theRest="${funcOutput%pihole-FTL*}"
FTLdetect "${binary}" "${theRest}"
"""
)
if supported:
expected_stdout = info_box + " FTL Checks..."
assert expected_stdout in detectPlatform.stdout
expected_stdout = tick_box + " Detected " + detected_string + " architecture"
assert expected_stdout in detectPlatform.stdout
expected_stdout = tick_box + " Downloading and Installing FTL"
assert expected_stdout in detectPlatform.stdout
else:
expected_stdout = (
"Not able to detect architecture (unknown: " + detected_string + ")"
)
assert expected_stdout in detectPlatform.stdout
def test_FTL_development_binary_installed_and_responsive_no_errors(host):
"""
confirms FTL development binary is copied and functional in installed location
"""
host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch')
host.run(
"""
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=$(get_binary_name)
binary="pihole-FTL${funcOutput##*pihole-FTL}"
theRest="${funcOutput%pihole-FTL*}"
FTLdetect "${binary}" "${theRest}"
"""
)
version_check = host.run(
"""
VERSION=$(pihole-FTL version)
echo ${VERSION:0:1}
"""
)
expected_stdout = "v"
assert expected_stdout in version_check.stdout
def test_IPv6_only_link_local(host):
"""
confirms IPv6 blocking is disabled for Link-local address
"""
# mock ip -6 address to return Link-local address
mock_command_2(
"ip",
{"-6 address": ("inet6 fe80::d210:52fa:fe00:7ad7/64 scope link", "0")},
host,
)
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
find_IPv6_information
"""
)
expected_stdout = "Unable to find IPv6 ULA/GUA address"
assert expected_stdout in detectPlatform.stdout
def test_IPv6_only_ULA(host):
"""
confirms IPv6 blocking is enabled for ULA addresses
"""
# mock ip -6 address to return ULA address
mock_command_2(
"ip",
{
"-6 address": (
"inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global",
"0",
)
},
host,
)
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
find_IPv6_information
"""
)
expected_stdout = "Found IPv6 ULA address"
assert expected_stdout in detectPlatform.stdout
def test_IPv6_only_GUA(host):
"""
confirms IPv6 blocking is enabled for GUA addresses
"""
# mock ip -6 address to return GUA address
mock_command_2(
"ip",
{
"-6 address": (
"inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global",
"0",
)
},
host,
)
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
find_IPv6_information
"""
)
expected_stdout = "Found IPv6 GUA address"
assert expected_stdout in detectPlatform.stdout
def test_IPv6_GUA_ULA_test(host):
"""
confirms IPv6 blocking is enabled for GUA and ULA addresses
"""
# mock ip -6 address to return GUA and ULA addresses
mock_command_2(
"ip",
{
"-6 address": (
"inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n"
"inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global",
"0",
)
},
host,
)
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
find_IPv6_information
"""
)
expected_stdout = "Found IPv6 ULA address"
assert expected_stdout in detectPlatform.stdout
def test_IPv6_ULA_GUA_test(host):
"""
confirms IPv6 blocking is enabled for GUA and ULA addresses
"""
# mock ip -6 address to return ULA and GUA addresses
mock_command_2(
"ip",
{
"-6 address": (
"inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n"
"inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global",
"0",
)
},
host,
)
detectPlatform = host.run(
"""
source /opt/pihole/basic-install.sh
find_IPv6_information
"""
)
expected_stdout = "Found IPv6 ULA address"
assert expected_stdout in detectPlatform.stdout
def test_validate_ip(host):
"""
Tests valid_ip for various IP addresses
"""
def test_address(addr, success=True):
output = host.run(
"""
source /opt/pihole/basic-install.sh
valid_ip "{addr}"
""".format(
addr=addr
)
)
assert output.rc == 0 if success else 1
test_address("192.168.1.1")
test_address("127.0.0.1")
test_address("255.255.255.255")
test_address("255.255.255.256", False)
test_address("255.255.256.255", False)
test_address("255.256.255.255", False)
test_address("256.255.255.255", False)
test_address("1092.168.1.1", False)
test_address("not an IP", False)
test_address("8.8.8.8#", False)
test_address("8.8.8.8#0")
test_address("8.8.8.8#1")
test_address("8.8.8.8#42")
test_address("8.8.8.8#888")
test_address("8.8.8.8#1337")
test_address("8.8.8.8#65535")
test_address("8.8.8.8#65536", False)
test_address("8.8.8.8#-1", False)
test_address("00.0.0.0", False)
test_address("010.0.0.0", False)
test_address("001.0.0.0", False)
test_address("0.0.0.0#00", False)
test_address("0.0.0.0#01", False)
test_address("0.0.0.0#001", False)
test_address("0.0.0.0#0001", False)
test_address("0.0.0.0#00001", False)
def test_package_manager_has_pihole_deps(host):
"""Confirms OS is able to install the required packages for Pi-hole"""
mock_command("dialog", {"*": ("", "0")}, host)
output = host.run(
"""
source /opt/pihole/basic-install.sh
package_manager_detect
build_dependency_package
install_dependent_packages
"""
)
assert "No package" not in output.stdout
assert output.rc == 0
def test_meta_package_uninstall(host):
"""Confirms OS is able to install and uninstall the Pi-hole meta package"""
mock_command("dialog", {"*": ("", "0")}, host)
install = host.run(
"""
source /opt/pihole/basic-install.sh
package_manager_detect
build_dependency_package
install_dependent_packages
"""
)
assert install.rc == 0
uninstall = host.run(
"""
source /opt/pihole/uninstall.sh
removeMetaPackage
"""
)
assert uninstall.rc == 0

59
test/test_any_utils.py Normal file
View File

@@ -0,0 +1,59 @@
def test_key_val_replacement_works(host):
"""Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file"""
host.run(
"""
source /opt/pihole/utils.sh
addOrEditKeyValPair "./testoutput" "KEY_ONE" "value1"
addOrEditKeyValPair "./testoutput" "KEY_TWO" "value2"
addOrEditKeyValPair "./testoutput" "KEY_ONE" "value3"
addOrEditKeyValPair "./testoutput" "KEY_FOUR" "value4"
"""
)
output = host.run(
"""
cat ./testoutput
"""
)
expected_stdout = "KEY_ONE=value3\nKEY_TWO=value2\nKEY_FOUR=value4\n"
assert expected_stdout == output.stdout
def test_getFTLPID_default(host):
"""Confirms getFTLPID returns the default value if FTL is not running"""
output = host.run(
"""
source /opt/pihole/utils.sh
getFTLPID
"""
)
expected_stdout = "-1\n"
assert expected_stdout == output.stdout
def test_setFTLConfigValue_getFTLConfigValue(host):
"""
Confirms getFTLConfigValue works (also assumes setFTLConfigValue works)
Requires FTL to be installed, so we do that first
(taken from test_FTL_development_binary_installed_and_responsive_no_errors)
"""
host.run(
"""
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=$(get_binary_name)
echo "development" > /etc/pihole/ftlbranch
binary="pihole-FTL${funcOutput##*pihole-FTL}"
theRest="${funcOutput%pihole-FTL*}"
FTLdetect "${binary}" "${theRest}"
"""
)
output = host.run(
"""
source /opt/pihole/utils.sh
setFTLConfigValue "dns.upstreams" '["9.9.9.9"]' > /dev/null
getFTLConfigValue "dns.upstreams"
"""
)
assert "[ 9.9.9.9 ]" in output.stdout

View File

@@ -1,188 +0,0 @@
#!/usr/bin/env bats
# Core installer tests — package manager, cache, fresh install, dependencies
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'helpers/mocks'
TICK="[✓]"
CROSS="[✗]"
INFO="[i]"
FTL_BRANCH="development"
CID=""
setup() {
CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG")
}
teardown() {
if [[ -n "$CID" ]]; then
docker rm -f "$CID" > /dev/null 2>&1 || true
fi
}
# ---------------------------------------------------------------------------
@test "installer exits when no supported package manager found" {
docker exec "$CID" bash -c "rm -rf /usr/bin/apt-get /usr/bin/rpm /sbin/apk"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
package_manager_detect
"
assert_output --partial "${CROSS} No supported package manager found"
}
@test "installer continues when SELinux config file does not exist" {
run docker exec "$CID" bash -c "
rm -f /etc/selinux/config
source /opt/pihole/basic-install.sh
checkSelinux
"
assert_output --partial "${INFO} SELinux not detected"
assert_success
}
@test "fresh install: all necessary files are readable by pihole user" {
mock_command "$CID" dialog "*" "" "0"
mock_command_passthrough "$CID" git "pull" "" "0"
mock_command_2 "$CID" systemctl \
"enable pihole-FTL" "" "0" \
"restart pihole-FTL" "" "0" \
"start pihole-FTL" "" "0"
mock_command_2 "$CID" rc-service \
"pihole-FTL enable" "" "0" \
"pihole-FTL restart" "" "0" \
"pihole-FTL start" "" "0"
# Install man pages if available (best-effort)
docker exec "$CID" bash -c "command -v apt-get > /dev/null && apt-get install -qq man" || true
docker exec "$CID" bash -c "command -v dnf > /dev/null && dnf install -y man" || true
docker exec "$CID" bash -c "command -v yum > /dev/null && yum install -y man" || true
docker exec "$CID" bash -c "command -v apk > /dev/null && apk add mandoc man-pages" || true
docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch"
run docker exec "$CID" bash -c "
export TERM=xterm
export DEBIAN_FRONTEND=noninteractive
umask 0027
runUnattended=true
source /opt/pihole/basic-install.sh > /dev/null
runUnattended=true
main
/opt/pihole/pihole-FTL-prestart.sh
"
assert_success
# Detect whether man was installed
local maninstalled=true
if [[ "$output" == *"${INFO} man not installed"* ]] || [[ "$output" == *"${INFO} man pages not installed"* ]]; then
maninstalled=false
fi
local piholeuser="pihole"
_check_perm() { docker exec "$CID" bash -c "su -s /bin/bash -c 'test -${1} ${2}' -p ${piholeuser}"; }
# /etc/pihole
run _check_perm r /etc/pihole; assert_success
run _check_perm x /etc/pihole; assert_success
# /etc/pihole/dhcp.leases
run _check_perm r /etc/pihole/dhcp.leases; assert_success
# /etc/pihole/install.log
run _check_perm r /etc/pihole/install.log; assert_success
# /etc/pihole/versions
run _check_perm r /etc/pihole/versions; assert_success
# /etc/pihole/macvendor.db
run _check_perm r /etc/pihole/macvendor.db; assert_success
# /etc/init.d/pihole-FTL
run _check_perm x /etc/init.d/pihole-FTL; assert_success
run _check_perm r /etc/init.d/pihole-FTL; assert_success
# man pages (if installed)
if [[ "$maninstalled" == "true" ]]; then
run _check_perm x /usr/local/share/man; assert_success
run _check_perm r /usr/local/share/man; assert_success
run _check_perm x /usr/local/share/man/man8; assert_success
run _check_perm r /usr/local/share/man/man8; assert_success
run _check_perm r /usr/local/share/man/man8/pihole.8; assert_success
fi
# /etc/cron.d
run _check_perm x /etc/cron.d/; assert_success
run _check_perm r /etc/cron.d/; assert_success
run _check_perm r /etc/cron.d/pihole; assert_success
# All files and directories under /etc/.pihole/
local dirs
dirs=$(docker exec "$CID" bash -c "find /etc/.pihole/ -type d -not -path '*/.*'" 2>/dev/null || true)
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
run _check_perm r "$dir"; assert_success
run _check_perm x "$dir"; assert_success
local files
files=$(docker exec "$CID" bash -c "find '${dir}' -maxdepth 1 -type f -exec echo {} \\;" 2>/dev/null || true)
while IFS= read -r file; do
[[ -z "$file" ]] && continue
run _check_perm r "$file"; assert_success
done <<< "$files"
done <<< "$dirs"
}
@test "package cache update succeeds without errors" {
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
"
assert_output --partial "${TICK} Update local cache of available packages"
refute_output --partial "error"
}
@test "package cache update reports failure correctly" {
mock_command "$CID" apt-get "update" "" "1"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
"
assert_output --partial "${CROSS} Update local cache of available packages"
assert_output --partial "Error: Unable to update package cache."
}
@test "OS can install required Pi-hole dependency packages" {
mock_command "$CID" dialog "*" "" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
build_dependency_package
install_dependent_packages
"
refute_output --partial "No package"
assert_success
}
@test "OS can install and uninstall the Pi-hole meta package" {
mock_command "$CID" dialog "*" "" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
package_manager_detect
update_package_cache
build_dependency_package
install_dependent_packages
"
assert_success
run docker exec "$CID" bash -c "
source /opt/pihole/uninstall.sh
removeMetaPackage
"
assert_success
}

View File

@@ -0,0 +1,75 @@
from .conftest import (
tick_box,
cross_box,
mock_command,
)
def mock_selinux_config(state, host):
"""
Creates a mock SELinux config file with expected content
"""
# validate state string
valid_states = ["enforcing", "permissive", "disabled"]
assert state in valid_states
# getenforce returns the running state of SELinux
mock_command("getenforce", {"*": (state.capitalize(), "0")}, host)
# create mock configuration with desired content
host.run(
"""
mkdir /etc/selinux
echo "SELINUX={state}" > /etc/selinux/config
""".format(
state=state.lower()
)
)
def test_selinux_enforcing_exit(host):
"""
confirms installer prompts to exit when SELinux is Enforcing by default
"""
mock_selinux_config("enforcing", host)
check_selinux = host.run(
"""
source /opt/pihole/basic-install.sh
checkSelinux
"""
)
expected_stdout = cross_box + " Current SELinux: enforcing"
assert expected_stdout in check_selinux.stdout
expected_stdout = "SELinux Enforcing detected, exiting installer"
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 1
def test_selinux_permissive(host):
"""
confirms installer continues when SELinux is Permissive
"""
mock_selinux_config("permissive", host)
check_selinux = host.run(
"""
source /opt/pihole/basic-install.sh
checkSelinux
"""
)
expected_stdout = tick_box + " Current SELinux: permissive"
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0
def test_selinux_disabled(host):
"""
confirms installer continues when SELinux is Disabled
"""
mock_selinux_config("disabled", host)
check_selinux = host.run(
"""
source /opt/pihole/basic-install.sh
checkSelinux
"""
)
expected_stdout = tick_box + " Current SELinux: disabled"
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env bats
# FTL architecture detection and binary installation tests
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'helpers/mocks'
TICK="[✓]"
INFO="[i]"
FTL_BRANCH="development"
CID=""
setup() {
CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG")
}
teardown() {
if [[ -n "$CID" ]]; then
docker rm -f "$CID" > /dev/null 2>&1 || true
fi
}
# ---------------------------------------------------------------------------
# FTL architecture detection — one @test per arch (replaces parametrize)
# ---------------------------------------------------------------------------
_test_ftl_arch() {
local arch="$1" detected_string="$2" supported="$3"
mock_command "$CID" uname "-m" "$arch" "0"
mock_command_2 "$CID" readelf \
"-A /bin/sh" "Tag_CPU_arch: ${arch}" "0" \
"-A /usr/bin/sh" "Tag_CPU_arch: ${arch}" "0" \
"-A /usr/sbin/sh" "Tag_CPU_arch: ${arch}" "0"
docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=\$(get_binary_name)
binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\"
theRest=\"\${funcOutput%pihole-FTL*}\"
FTLdetect \"\${binary}\" \"\${theRest}\"
"
if [[ "$supported" == "true" ]]; then
assert_output --partial "${INFO} FTL Checks..."
assert_output --partial "${TICK} Detected ${detected_string} architecture"
assert_output --partial "${TICK} Downloading and Installing FTL"
else
assert_output --partial "Not able to detect architecture (unknown: ${detected_string})"
fi
}
@test "FTL detects aarch64 architecture" {
_test_ftl_arch "aarch64" "AArch64 (64 Bit ARM)" "true"
}
@test "FTL detects ARMv6 architecture" {
_test_ftl_arch "armv6" "ARMv6" "true"
}
@test "FTL detects ARMv7l architecture" {
_test_ftl_arch "armv7l" "ARMv7 (or newer)" "true"
}
@test "FTL detects ARMv7 architecture" {
_test_ftl_arch "armv7" "ARMv7 (or newer)" "true"
}
@test "FTL detects ARMv8a architecture" {
_test_ftl_arch "armv8a" "ARMv7 (or newer)" "true"
}
@test "FTL detects x86_64 architecture" {
_test_ftl_arch "x86_64" "x86_64" "true"
}
@test "FTL detects riscv64 architecture" {
_test_ftl_arch "riscv64" "riscv64" "true"
}
@test "FTL reports unsupported architecture" {
_test_ftl_arch "mips" "mips" "false"
}
@test "FTL development binary is installed and responsive" {
docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch"
docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=\$(get_binary_name)
binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\"
theRest=\"\${funcOutput%pihole-FTL*}\"
FTLdetect \"\${binary}\" \"\${theRest}\"
"
run docker exec "$CID" bash -c '
VERSION=$(pihole-FTL version)
echo "${VERSION:0:1}"
'
assert_output --partial "v"
}

View File

@@ -1,116 +0,0 @@
#!/usr/bin/env bats
# Network detection tests — IPv6 address detection and IP validation
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'helpers/mocks'
CID=""
setup() {
CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG")
}
teardown() {
if [[ -n "$CID" ]]; then
docker rm -f "$CID" > /dev/null 2>&1 || true
fi
}
# ---------------------------------------------------------------------------
# IPv6 detection
# ---------------------------------------------------------------------------
@test "IPv6 link-local only: blocking disabled" {
mock_command_2 "$CID" ip \
"-6 address" "inet6 fe80::d210:52fa:fe00:7ad7/64 scope link" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
find_IPv6_information
"
assert_output --partial "Unable to find IPv6 ULA/GUA address"
}
@test "IPv6 ULA only: blocking enabled" {
mock_command_2 "$CID" ip \
"-6 address" "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
find_IPv6_information
"
assert_output --partial "Found IPv6 ULA address"
}
@test "IPv6 GUA only: blocking enabled" {
mock_command_2 "$CID" ip \
"-6 address" "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
find_IPv6_information
"
assert_output --partial "Found IPv6 GUA address"
}
@test "IPv6 GUA + ULA: ULA takes precedence" {
mock_command_2 "$CID" ip \
"-6 address" "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global
inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
find_IPv6_information
"
assert_output --partial "Found IPv6 ULA address"
}
@test "IPv6 ULA + GUA: ULA takes precedence" {
mock_command_2 "$CID" ip \
"-6 address" "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global
inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global" "0"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
find_IPv6_information
"
assert_output --partial "Found IPv6 ULA address"
}
# ---------------------------------------------------------------------------
# IP address validation
# ---------------------------------------------------------------------------
@test "valid_ip accepts and rejects addresses correctly" {
_valid() {
run docker exec "$CID" bash -c "source /opt/pihole/basic-install.sh; valid_ip '${1}'"
assert_success
}
_invalid() {
run docker exec "$CID" bash -c "source /opt/pihole/basic-install.sh; valid_ip '${1}'"
assert_failure
}
_valid "192.168.1.1"
_valid "127.0.0.1"
_valid "255.255.255.255"
_invalid "255.255.255.256"
_invalid "255.255.256.255"
_invalid "255.256.255.255"
_invalid "256.255.255.255"
_invalid "1092.168.1.1"
_invalid "not an IP"
_invalid "8.8.8.8#"
_valid "8.8.8.8#0"
_valid "8.8.8.8#1"
_valid "8.8.8.8#42"
_valid "8.8.8.8#888"
_valid "8.8.8.8#1337"
_valid "8.8.8.8#65535"
_invalid "8.8.8.8#65536"
_invalid "8.8.8.8#-1"
_invalid "00.0.0.0"
_invalid "010.0.0.0"
_invalid "001.0.0.0"
_invalid "0.0.0.0#00"
_invalid "0.0.0.0#01"
_invalid "0.0.0.0#001"
_invalid "0.0.0.0#0001"
_invalid "0.0.0.0#00001"
}

View File

@@ -1,71 +0,0 @@
#!/usr/bin/env bats
# Tests for SELinux handling in basic-install.sh.
# Translated from test_centos_fedora_common_support.py.
# Only runs on rhel family (CentOS/Fedora) — selected by run.sh.
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'helpers/mocks'
TICK="[✓]"
CROSS="[✗]"
CID=""
setup() {
CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG")
}
teardown() {
if [[ -n "$CID" ]]; then
docker rm -f "$CID" > /dev/null 2>&1 || true
fi
}
# ---------------------------------------------------------------------------
# Helper: write a mock SELinux config with the given state
# ---------------------------------------------------------------------------
_mock_selinux_config() {
local state="$1" # enforcing, permissive, or disabled
local capitalized
capitalized=$(echo "${state}" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
mock_command "$CID" getenforce "*" "$capitalized" "0"
docker exec "$CID" bash -c "
mkdir -p /etc/selinux
echo 'SELINUX=${state}' > /etc/selinux/config
"
}
# ---------------------------------------------------------------------------
@test "SELinux enforcing: installer exits with error" {
_mock_selinux_config "enforcing"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
checkSelinux
"
assert_output --partial "${CROSS} Current SELinux: enforcing"
assert_output --partial "SELinux Enforcing detected, exiting installer"
assert_failure
}
@test "SELinux permissive: installer continues" {
_mock_selinux_config "permissive"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
checkSelinux
"
assert_output --partial "${TICK} Current SELinux: permissive"
assert_success
}
@test "SELinux disabled: installer continues" {
_mock_selinux_config "disabled"
run docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
checkSelinux
"
assert_output --partial "${TICK} Current SELinux: disabled"
assert_success
}

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env bats
# Tests for utils.sh — translated from test_any_utils.py
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
CID=""
setup() {
CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG")
}
teardown() {
if [[ -n "$CID" ]]; then
docker rm -f "$CID" > /dev/null 2>&1 || true
fi
}
# ---------------------------------------------------------------------------
@test "addOrEditKeyValPair adds and replaces key-value pairs correctly" {
docker exec "$CID" bash -c "
source /opt/pihole/utils.sh
addOrEditKeyValPair './testoutput' 'KEY_ONE' 'value1'
addOrEditKeyValPair './testoutput' 'KEY_TWO' 'value2'
addOrEditKeyValPair './testoutput' 'KEY_ONE' 'value3'
addOrEditKeyValPair './testoutput' 'KEY_FOUR' 'value4'
"
run docker exec "$CID" bash -c "cat ./testoutput"
assert_output "KEY_ONE=value3
KEY_TWO=value2
KEY_FOUR=value4"
}
@test "getFTLPID returns -1 when FTL is not running" {
run docker exec "$CID" bash -c "
source /opt/pihole/utils.sh
getFTLPID
"
assert_output "-1"
}
@test "setFTLConfigValue and getFTLConfigValue round-trip" {
# FTL must be installed for this test
docker exec "$CID" bash -c "
source /opt/pihole/basic-install.sh
create_pihole_user
funcOutput=\$(get_binary_name)
echo 'development' > /etc/pihole/ftlbranch
binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\"
theRest=\"\${funcOutput%pihole-FTL*}\"
FTLdetect \"\${binary}\" \"\${theRest}\"
"
run docker exec "$CID" bash -c "
source /opt/pihole/utils.sh
setFTLConfigValue 'dns.upstreams' '[\"9.9.9.9\"]' > /dev/null
getFTLConfigValue 'dns.upstreams'
"
assert_output --partial "[ 9.9.9.9 ]"
}

10
test/tox.centos_10.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _centos_10.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py

10
test/tox.centos_9.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _centos_9.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py

10
test/tox.debian_11.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _debian_11.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py

10
test/tox.debian_12.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _debian_12.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py

10
test/tox.fedora_40.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _fedora_40.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py

10
test/tox.fedora_41.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _fedora_41.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py

10
test/tox.fedora_42.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _fedora_42.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py

10
test/tox.ubuntu_20.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _ubuntu_20.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py

10
test/tox.ubuntu_22.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _ubuntu_22.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py

10
test/tox.ubuntu_24.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py3
[testenv:py3]
allowlist_externals = docker
deps = -rrequirements.txt
setenv =
COLUMNS=120
commands = docker buildx build --load --progress plain -f _ubuntu_24.Dockerfile -t pytest_pihole:test_container ../
pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py