diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 77aacbec..ee923724 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,16 +25,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml index f169ab6f..5dca98ba 100644 --- a/.github/workflows/merge-conflict.yml +++ b/.github/workflows/merge-conflict.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check if PRs are have merge conflicts - uses: eps1lon/actions-label-merge-conflict@v3.0.3 + uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 #v3.0.3 with: dirtyLabel: "PR: Merge Conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 34ffb64e..1e044ec9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: issues: write steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - name: Remove 'stale' label run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }} env: diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml index 6952dcab..6dfcbe99 100644 --- a/.github/workflows/stale_pr.yml +++ b/.github/workflows/stale_pr.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Do not automatically mark PR/issue as stale diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml index 7df1a32d..058bbcab 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -33,7 +33,7 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2e8f951..6482e242 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: fetch-depth: 0 # Differential ShellCheck requires full git history @@ -31,25 +31,25 @@ jobs: [[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!" - name: Differential ShellCheck - uses: redhat-plumbers-in-action/differential-shellcheck@v5 + uses: redhat-plumbers-in-action/differential-shellcheck@d965e66ec0b3b2f821f75c8eff9b12442d9a7d1e #v5.5.6 with: severity: warning display-engine: sarif-fmt - name: Spell-Checking - uses: codespell-project/actions-codespell@master + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 #v2.2 with: ignore_words_file: .codespellignore - name: Get editorconfig-checker - uses: editorconfig-checker/action-editorconfig-checker@main # tag v1.0.0 is really out of date + uses: editorconfig-checker/action-editorconfig-checker@4b6cd6190d435e7e084fb35e36a096e98506f7b9 #v2.1.0 - name: Run editorconfig-checker run: editorconfig-checker - name: Check python code formatting with black - uses: psf/black@stable + uses: psf/black@6305bf1ae645ab7541be4f5028a86239316178eb #26.1.0 with: src: "./test" options: "--check --diff --color" @@ -65,6 +65,7 @@ jobs: [ debian_11, debian_12, + debian_13, ubuntu_20, ubuntu_22, ubuntu_24, @@ -73,15 +74,19 @@ 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@v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 with: python-version: "3.13" diff --git a/.shellcheckrc b/.shellcheckrc index 8e0b8387..c4711a8f 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -1,2 +1,6 @@ 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 diff --git a/advanced/Scripts/api.sh b/advanced/Scripts/api.sh index e5eacd41..7589d3ff 100755 --- a/advanced/Scripts/api.sh +++ b/advanced/Scripts/api.sh @@ -150,7 +150,6 @@ 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 @@ -161,6 +160,14 @@ 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' @@ -183,13 +190,20 @@ 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 the error message from the session response - sessionError=$(echo "${sessionResponse}"| jq --raw-output .error.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') if [ "${1}" = "verbose" ]; then if [ "${validSession}" = true ]; then @@ -353,12 +367,9 @@ apiFunc() { if [ "${verbosity}" = "verbose" ]; then echo "Data:" fi - - if command -v jq >/dev/null && echo "${data}" | jq . >/dev/null 2>&1; then - echo "${data}" | jq . - else - 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}" # Delete the session LogoutAPI "${verbosity}" diff --git a/advanced/Scripts/database_migration/gravity-db.sh b/advanced/Scripts/database_migration/gravity-db.sh index 41593368..5cf4cc34 100755 --- a/advanced/Scripts/database_migration/gravity-db.sh +++ b/advanced/Scripts/database_migration/gravity-db.sh @@ -150,4 +150,10 @@ 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 } diff --git a/advanced/Scripts/database_migration/gravity/19_to_20.sql b/advanced/Scripts/database_migration/gravity/19_to_20.sql new file mode 100644 index 00000000..1867615d --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/19_to_20.sql @@ -0,0 +1,43 @@ +.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; diff --git a/advanced/Scripts/piholeARPTable.sh b/advanced/Scripts/piholeARPTable.sh deleted file mode 100755 index c62acdbc..00000000 --- a/advanced/Scripts/piholeARPTable.sh +++ /dev/null @@ -1,83 +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. -# -# 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 - -readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" -utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" -# shellcheck source=./advanced/Scripts/utils.sh -source "${utilsfile}" - -# 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=$(service pihole-FTL stop 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=$(service pihole-FTL restart 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 diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index a6df46f2..deb07172 100755 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -41,6 +41,22 @@ warning1() { } checkout() { + + local skipFTL additionalFlag + skipFTL=false + # Check arguments + for var in "$@"; do + case "$var" in + "--skipFTL") skipFTL=true ;; + esac + done + + if [ "${skipFTL}" == true ]; then + additionalFlag="--skipFTL" + else + additionalFlag="" + fi + local corebranches local webbranches @@ -235,7 +251,7 @@ checkout() { # Force updating everything if [[ ! "${1}" == "web" && ! "${1}" == "ftl" ]]; then echo -e " ${INFO} Running installer to upgrade your installation" - if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then + if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended ${additionalFlag}; then exit 0 else echo -e " ${COL_RED} Error: Unable to complete update, please contact support${COL_NC}" diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index 2b903e50..964fff9b 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -375,22 +375,6 @@ 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 - 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}"; @@ -593,18 +577,21 @@ 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 - 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}') + 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]}" + )" # Check if the right services are using the right ports - if [[ ${ports_configured[*]} =~ $(echo "${port_number}" | rev | cut -d: -f1 | rev) ]]; then + if [[ ${ports_configured[*]} =~ ${port_number##*:} ]]; 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 @@ -672,7 +659,7 @@ dig_at() { local record_type="A" fi - # Find a random blocked url that has not been whitelisted and is not ABP style. + # Find a random blocked url that has not been allowlisted 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 @@ -722,7 +709,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}")"; then + if local_dig="$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${local_address}" "${record_type}" -p "$(get_ftl_conf_value "dns.port")")"; then # If it can, show success if [[ "${local_dig}" == *"status: NOERROR"* ]]; then local_dig="NOERROR" @@ -778,7 +765,7 @@ process_status(){ : else # non-Docker system - if service "${i}" status | grep -E 'is\srunning' &> /dev/null; then + if service "${i}" status | grep -q -E 'is\srunning|started'; then status_of_process="active" else status_of_process="inactive" @@ -816,42 +803,27 @@ 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 - # 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 + # Process the file, strip out comments and blank lines + local processed + processed=$(sed -e 's/^\s*#.*$//' -e '/^$/d' "${filename}") - # 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 + 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" } parse_file() { @@ -924,38 +896,38 @@ list_files_in_dir() { fi # Store the files found in an array - mapfile -t files_found < <(ls "${dir_to_parse}") + local files_found=("${dir_to_parse}"/*) # For each file in the array, for each_file in "${files_found[@]}"; do - if [[ -d "${dir_to_parse}/${each_file}" ]]; then + if [[ -d "${each_file}" ]]; then # If it's a directory, do nothing : - 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 [[ "${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}" == "${DNSMASQ_D_DIRECTORY}" ]]; then # in case of the dnsmasq directory include all files in the debug output - log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" - make_array_from_file "${dir_to_parse}/${each_file}" + log_write "\\n${COL_GREEN}$(ls -lhd "${each_file}")${COL_NC}" + make_array_from_file "${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 [[ "${dir_to_parse}/${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then + if [[ "${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then # display the filename - log_write "\\n${COL_GREEN}$(ls -lhd "${dir_to_parse}"/"${each_file}")${COL_NC}" + log_write "\\n${COL_GREEN}$(ls -lhd "${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 "${dir_to_parse}/${each_file}" in + case "${each_file}" in # If it's Web server log, give the first and last 25 lines - "${PIHOLE_WEBSERVER_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 25 + "${PIHOLE_WEBSERVER_LOG}") head_tail_log "${each_file}" 25 ;; # Same for the FTL log - "${PIHOLE_FTL_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 35 + "${PIHOLE_FTL_LOG}") head_tail_log "${each_file}" 35 ;; # parse the file into an array in case we ever need to analyze it line-by-line - *) make_array_from_file "${dir_to_parse}/${each_file}"; + *) make_array_from_file "${each_file}"; esac else # Otherwise, do nothing since it's not a file needed for Pi-hole so we don't care about it @@ -991,6 +963,7 @@ 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 @@ -999,14 +972,14 @@ head_tail_log() { IFS=$'\r\n' local log_head=() mapfile -t log_head < <(head -n "${qty}" "${filename}") - log_write " ${COL_CYAN}-----head of $(basename "${filename}")------${COL_NC}" + log_write " ${COL_CYAN}-----head of ${filebasename}------${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 $(basename "${filename}")------${COL_NC}" + log_write " ${COL_CYAN}-----tail of ${filebasename}------${COL_NC}" for tail_line in "${log_tail[@]}"; do log_write " ${tail_line}" done @@ -1033,6 +1006,24 @@ 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 @@ -1080,15 +1071,15 @@ check_dhcp_servers() { } show_groups() { - 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_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_adlists() { - 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_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_domainlist() { - 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_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_clients() { diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index ca70f31b..ac28aed9 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -86,6 +86,7 @@ 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 @@ -115,4 +116,3 @@ else echo -e "${OVER} ${TICK} Deleted ${deleted} queries from long-term query database" fi fi - diff --git a/advanced/Scripts/piholeNetworkFlush.sh b/advanced/Scripts/piholeNetworkFlush.sh new file mode 100755 index 00000000..a8721476 --- /dev/null +++ b/advanced/Scripts/piholeNetworkFlush.sh @@ -0,0 +1,84 @@ +#!/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 + +readonly 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 + diff --git a/advanced/Scripts/query.sh b/advanced/Scripts/query.sh index 18c018dc..d0dfd6e9 100755 --- a/advanced/Scripts/query.sh +++ b/advanced/Scripts/query.sh @@ -37,19 +37,16 @@ Options: } GenerateOutput() { - 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 + local counts data num_gravity num_lists search_type_str + local gravity_data_csv lists_data_csv line url type color data="${1}" - # 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) + # 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') if [ "${partial}" = true ]; then search_type_str="partially" @@ -62,7 +59,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 "${lists_data}" | jq --raw-output '.[] | [.domain, .type] | join(",")') + lists_data_csv=$(printf %s "${data}" | jq --raw-output '.search.domains | map([.domain, .type] | join(",")) | join("\n")') # 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 @@ -71,11 +68,11 @@ GenerateOutput() { fi # Results from gravity - printf "%s\n\n" "Found ${num_gravity} adlists ${search_type_str} matching '${COL_BLUE}${domain}${COL_NC}'." + printf "%s\n\n" "Found ${num_gravity} lists ${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,domain,domain,...." string + # Convert the data to a csv, each line is a "URL,type,domain,domain,...." string # not using jq's @csv here as it quotes each value individually - gravity_data_csv=$(printf %s "${gravity_data}" | jq --raw-output '.[] | [.address, .type, .domains[]] | join(",")') + gravity_data_csv=$(printf %s "${data}" | jq --raw-output '.search.gravity | group_by(.address,.type) | map([.[0].address, .[0].type, (.[] | .domain)] | join(",")) | join("\n")') # Generate line-by-line output for each csv line echo "${gravity_data_csv}" | while read -r line; do @@ -97,15 +94,8 @@ GenerateOutput() { # cut off type, leaving "domain,domain,...." line=${line#*,} - # 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 + # Replace commas with newlines and format output + echo "${line}" | sed 's/,/\n/g' | sed "s/^/ - ${COL_GREEN}/" | sed "s/$/${COL_NC}/" printf "\n\n" done fi diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index 4e0d973e..67ac9693 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -149,31 +149,37 @@ main() { echo -e " ${INFO} Web Interface:\\t${COL_GREEN}up to date${COL_NC}" fi - local funcOutput - funcOutput=$(get_binary_name) #Store output of get_binary_name here - local binary - binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) - if FTLcheckUpdate "${binary}" &>/dev/null; then - FTL_update=true - echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" + if FTLcheckUpdate "${binary}" &>/dev/null; then + FTL_update=true + echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" + else + case $? in + 1) + echo -e " ${INFO} FTL:\\t\\t${COL_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 + ;; + 3) + echo -e " ${INFO} FTL:\\t\\t${COL_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}" + exit 1 + esac + FTL_update=false + fi else - case $? in - 1) - echo -e " ${INFO} FTL:\\t\\t${COL_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 - ;; - 3) - echo -e " ${INFO} FTL:\\t\\t${COL_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}" - exit 1 - esac + echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}--skipFTL set - update check skipped${COL_NC}" FTL_update=false fi @@ -222,7 +228,14 @@ main() { fi if [[ "${FTL_update}" == true || "${core_update}" == true ]]; then - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --repair --unattended || \ + local addionalFlag + + if [[ ${skipFTL} == true ]]; then + addionalFlag="--skipFTL" + else + addionalFlag="" + fi + ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --repair --unattended ${addionalFlag} || \ echo -e "${basicError}" && exit 1 fi @@ -242,8 +255,15 @@ main() { exit 0 } -if [[ "$1" == "--check-only" ]]; then - CHECK_ONLY=true -fi +CHECK_ONLY=false +skipFTL=false + +# Check arguments +for var in "$@"; do + case "$var" in + "--check-only") CHECK_ONLY=true ;; + "--skipFTL") skipFTL=true ;; + esac +done main diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh index d4a6957c..d000a6db 100755 --- a/advanced/Scripts/utils.sh +++ b/advanced/Scripts/utils.sh @@ -73,7 +73,9 @@ getFTLPID() { # Example getFTLConfigValue dns.piholePTR ####################### getFTLConfigValue(){ - pihole-FTL --config -q "${1}" + # Pipe to cat to avoid pihole-FTL assuming this is an interactive command + # returning colored output. + pihole-FTL --config -q "${1}" | cat } ####################### @@ -86,9 +88,17 @@ getFTLConfigValue(){ # setFTLConfigValue dns.upstreams '[ "8.8.8.8" , "8.8.4.4" ]' ####################### setFTLConfigValue(){ - 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 + 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 } diff --git a/advanced/Templates/gravity.db.sql b/advanced/Templates/gravity.db.sql index 0187e4e6..9aad6113 100644 --- a/advanced/Templates/gravity.db.sql +++ b/advanced/Templates/gravity.db.sql @@ -66,7 +66,7 @@ CREATE TABLE info value TEXT NOT NULL ); -INSERT INTO "info" VALUES('version','19'); +INSERT INTO "info" VALUES('version','20'); /* 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_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +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 @@ -119,7 +119,7 @@ CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_gr AND domainlist.type = 0 ORDER BY domainlist.id; -CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +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 @@ -127,7 +127,7 @@ CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_gr AND domainlist.type = 1 ORDER BY domainlist.id; -CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +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 @@ -135,7 +135,7 @@ CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist AND domainlist.type = 2 ORDER BY domainlist.id; -CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id +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 diff --git a/advanced/Templates/pihole-FTL-prestart.sh b/advanced/Templates/pihole-FTL-prestart.sh index 579309d3..095c0d2d 100755 --- a/advanced/Templates/pihole-FTL-prestart.sh +++ b/advanced/Templates/pihole-FTL-prestart.sh @@ -8,12 +8,20 @@ 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) @@ -28,7 +36,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 /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 "${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 /etc/pihole/dhcp.leases ] || install -m 644 -o pihole -g pihole /dev/null /etc/pihole/dhcp.leases diff --git a/advanced/Templates/pihole-FTL.openrc b/advanced/Templates/pihole-FTL.openrc new file mode 100644 index 00000000..2207273e --- /dev/null +++ b/advanced/Templates/pihole-FTL.openrc @@ -0,0 +1,40 @@ +#!/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 $? +} diff --git a/advanced/Templates/pihole-FTL.systemd b/advanced/Templates/pihole-FTL.systemd index fcbb8d8d..29470c5a 100644 --- a/advanced/Templates/pihole-FTL.systemd +++ b/advanced/Templates/pihole-FTL.systemd @@ -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 -ExecStartPre=/opt/pihole/pihole-FTL-prestart.sh +# Run prestart with elevated permissions +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 diff --git a/advanced/bash-completion/pihole b/advanced/bash-completion/pihole deleted file mode 100644 index cf99ab73..00000000 --- a/advanced/bash-completion/pihole +++ /dev/null @@ -1,51 +0,0 @@ -_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 diff --git a/advanced/bash-completion/pihole-ftl.bash b/advanced/bash-completion/pihole-ftl.bash new file mode 100644 index 00000000..d652f007 --- /dev/null +++ b/advanced/bash-completion/pihole-ftl.bash @@ -0,0 +1,9 @@ +#!/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 diff --git a/advanced/bash-completion/pihole.bash b/advanced/bash-completion/pihole.bash new file mode 100644 index 00000000..7f6c7ab4 --- /dev/null +++ b/advanced/bash-completion/pihole.bash @@ -0,0 +1,59 @@ +#!/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 diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index ac467dda..2d504c1c 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -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=${IPV4_ADDRESS} -IPV6_ADDRESS=${IPV6_ADDRESS} +IPV4_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 < Architecture: all Description: Pi-hole dependency meta package -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 +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 Section: contrib/metapackages Priority: optional EOM @@ -130,12 +130,12 @@ EOM PIHOLE_META_PACKAGE_CONTROL_RPM=$( cat < /dev/null else # Otherwise, use update-rc.d to accomplish this update-rc.d "${1}" defaults >/dev/null @@ -1277,7 +1342,10 @@ disable_service() { # If systemctl exists, if is_command systemctl; then # use that to disable the service - systemctl -q disable "${1}" + systemctl -q disable --now "${1}" + elif is_command openrc; then + rc-update del "${1}" "${2:-default}" &> /dev/null + else # Otherwise, use update-rc.d to accomplish this update-rc.d "${1}" disable >/dev/null @@ -1290,6 +1358,8 @@ check_service_active() { if is_command systemctl; 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,8 +1461,27 @@ install_dependent_packages() { printf " %b Error: Unable to find Pi-hole dependency package.\\n" "${COL_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}" - # If neither apt-get or yum/dnf package managers were found + 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 else # we cannot install the dependency package printf " %b No supported package manager found\\n" "${CROSS}" @@ -1417,6 +1506,15 @@ 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, @@ -1466,7 +1564,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 /usr/sbin/nologin pihole; then + if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" @@ -1481,7 +1579,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 /usr/sbin/nologin pihole; then + if useradd -r --no-user-group -g pihole -s "$(command -v nologin)" pihole; then printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" else printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" @@ -1633,9 +1731,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 @@ -1668,7 +1766,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 } @@ -1701,9 +1799,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}" @@ -1718,6 +1816,12 @@ 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} || { @@ -1791,8 +1895,12 @@ 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 - # Stop pihole-FTL service if available - stop_service pihole-FTL >/dev/null + + # 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 # Install the new version with the correct permissions install -T -m 0755 "${binary}" /usr/bin/pihole-FTL @@ -1906,7 +2014,7 @@ 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 Contact Pi-hole Support if you experience issues (e.g: FTL not running)\\n" "${INFO}" @@ -1940,14 +2048,14 @@ 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 status=$? if [ "${status}" -eq 1 ]; then printf " %b Branch \"%s\" is not available.\\n" "${INFO}" "${ftlBranch}" @@ -2050,11 +2158,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() { @@ -2227,21 +2335,18 @@ main() { # Check if there is a usable FTL binary available on this architecture - do # this early on as FTL is a hard dependency for Pi-hole - local funcOutput - funcOutput=$(get_binary_name) #Store output of get_binary_name here - # Abort early if this processor is not supported (get_binary_name returns empty string) - if [[ "${funcOutput}" == "" ]]; then - printf " %b Upgrade/install aborted\\n" "${CROSS}" "${DISTRO_NAME}" - exit 1 - fi - - if [[ "${fresh_install}" == false ]]; then - # if it's running unattended, - if [[ "${runUnattended}" == true ]]; then - printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" - # also disable debconf-apt-progress dialogs - export DEBIAN_FRONTEND="noninteractive" + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + # Get the binary name for the current architecture + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + # Abort early if this processor is not supported (get_binary_name returns empty string) + if [[ "${funcOutput}" == "" ]]; then + printf " %b Upgrade/install aborted\\n" "${CROSS}" "${DISTRO_NAME}" + exit 1 fi + else + printf " %b %b--skipFTL set - skipping architecture check%b\\n" "${INFO}" "${COL_YELLOW}" "${COL_NC}" fi if [[ "${fresh_install}" == true ]]; then @@ -2274,13 +2379,18 @@ main() { create_pihole_user # Download and install FTL - local binary - binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) - local theRest - theRest="${funcOutput%pihole-FTL*}" # Print the rest of get_binary_name's output to display (cut out from first instance of "pihole-FTL") - if ! FTLdetect "${binary}" "${theRest}"; then - printf " %b FTL Engine not installed\\n" "${CROSS}" - exit 1 + # Allow the user to skip this check if they are using a self-compiled FTL binary from an unsupported architecture + if [ "${skipFTL}" != true ]; then + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + local theRest + theRest="${funcOutput%pihole-FTL*}" # Print the rest of get_binary_name's output to display (cut out from first instance of "pihole-FTL") + if ! FTLdetect "${binary}" "${theRest}"; then + printf " %b FTL Engine not installed\\n" "${CROSS}" + exit 1 + fi + else + printf " %b %b--skipFTL set - skipping FTL binary installation%b\\n" "${INFO}" "${COL_YELLOW}" "${COL_NC}" fi # Install and log everything to a file @@ -2344,7 +2454,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 @@ -2395,7 +2505,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/admin:${WEBPORT} or http://${IPV4_ADDRESS%/*}:${WEBPORT}/admin\\n\\nYour Admin Webpage login password is ${pw}\ +\\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,\ diff --git a/automated install/uninstall.sh b/automated install/uninstall.sh index e8dec36a..6d307de4 100755 --- a/automated install/uninstall.sh +++ b/automated install/uninstall.sh @@ -12,9 +12,7 @@ source "/opt/pihole/COL_TABLE" # shellcheck source="./advanced/Scripts/utils.sh" source "/opt/pihole/utils.sh" - -ADMIN_INTERFACE_DIR=$(getFTLConfigValue "webserver.paths.webroot")$(getFTLConfigValue "webserver.paths.webhome") -readonly ADMIN_INTERFACE_DIR +# getFTLConfigValue() from utils.sh while true; do read -rp " ${QST} Are you sure you would like to remove ${COL_BOLD}Pi-hole${COL_NC}? [y/N] " answer @@ -29,125 +27,179 @@ str="Root user check" if [[ ${EUID} -eq 0 ]]; then echo -e " ${TICK} ${str}" else - # 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 + echo -e " ${CROSS} ${str} + Script called with non-root privileges + The Pi-hole requires elevated privileges to uninstall" + exit 1 fi -readonly PI_HOLE_FILES_DIR="/etc/.pihole" +# 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 SKIP_INSTALL="true" # shellcheck source="./automated install/basic-install.sh" -source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" - -# package_manager_detect() sourced from basic-install.sh -package_manager_detect - +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 removeMetaPackage() { # Purge Pi-hole meta package echo "" echo -ne " ${INFO} Removing Pi-hole meta package..."; - eval "${SUDO}" "${PKG_REMOVE}" "pihole-meta" &> /dev/null; + eval "${PKG_REMOVE}" "pihole-meta" &> /dev/null; echo -e "${OVER} ${INFO} Removed Pi-hole meta package"; - } -removePiholeFiles() { +removeWebInterface() { # Remove the web interface of Pi-hole echo -ne " ${INFO} Removing Web Interface..." - ${SUDO} rm -rf "${ADMIN_INTERFACE_DIR}" &> /dev/null + rm -rf "${ADMIN_INTERFACE_DIR:-/var/www/html/admin/}" &> /dev/null echo -e "${OVER} ${TICK} Removed Web Interface" +} - # 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 - ${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" - fi +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 - # Attempt to preserve backwards compatibility with older versions - if [[ -f /etc/cron.d/pihole ]];then - ${SUDO} rm -f /etc/cron.d/pihole &> /dev/null - echo -e " ${TICK} Removed /etc/cron.d/pihole" - fi - - ${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" - - # 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..." - if [[ -x "$(command -v systemctl)" ]]; then - systemctl stop pihole-FTL - else - service pihole-FTL stop - fi - ${SUDO} rm -f /etc/systemd/system/pihole-FTL.service + 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..." - ${SUDO} rm -R /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 - ${SUDO} rm -f /etc/init.d/pihole-FTL - ${SUDO} rm -f /usr/bin/pihole-FTL + 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 + 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 - # If the pihole manpage exists, then delete and rebuild man-db + # Attempt to preserve backwards compatibility with older versions + if [[ -f /etc/cron.d/pihole ]];then + 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 + + echo -e " ${TICK} Removed config files" +} + +removeManPage() { + # If the pihole manpage exists, then delete 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 + 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 + fi echo -e " ${TICK} Removed pihole man page" fi +} +removeUser() { # If the pihole user exists, then remove if id "pihole" &> /dev/null; then - if ${SUDO} userdel -r pihole 2> /dev/null; then + if 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 ${SUDO} groupdel pihole 2> /dev/null; then + if 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} @@ -158,5 +210,17 @@ removePiholeFiles() { } ######### 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 diff --git a/gravity.sh b/gravity.sh index da5aecfa..b772dc61 100755 --- a/gravity.sh +++ b/gravity.sh @@ -118,9 +118,12 @@ gravity_swap_databases() { # Swap databases and remove or conditionally rename old database # Number of available blocks on disk - availableBlocks=$(stat -f --format "%a" "${gravityDIR}") + # 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}") # Number of blocks, used by gravity.db - gravityBlocks=$(stat --format "%b" "${gravityDBfile}") + gravityBlocks=$(stat -c "%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 @@ -609,7 +612,7 @@ compareLists() { gravity_DownloadBlocklistFromUrl() { local url="${1}" adlistID="${2}" saveLocation="${3}" compression="${4}" gravity_type="${5}" domain="${6}" local listCurlBuffer str httpCode success="" ip customUpstreamResolver="" - local file_path permissions ip_addr port blocked=false download=true + local file_path 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=() @@ -718,36 +721,47 @@ gravity_DownloadBlocklistFromUrl() { fi fi - # If we are going to "download" a local file, we first check if the target - # file has a+r permission. We explicitly check for all+read because we want - # to make sure that the file is readable by everyone and not just the user - # running the script. - if [[ $url == "file://"* ]]; then + # If we "download" a local file (file://), verify read access before using it. + # When running as root (e.g., via pihole -g), check that the 'pihole' user can read the file + # to match the effective runtime user of FTL; otherwise, check the current user's read access + # (e.g., in Docker or when invoked by a non-root user). The target must + # resolve to a regular file and be readable by the evaluated user. + if [[ "${url}" == "file:/"* ]]; then # Get the file path - file_path=$(echo "$url" | cut -d'/' -f3-) + file_path=$(echo "${url}" | cut -d'/' -f3-) # Check if the file exists and is a regular file (i.e. not a socket, fifo, tty, block). Might still be a symlink. - if [[ ! -f $file_path ]]; then - # Output that the file does not exist - echo -e "${OVER} ${CROSS} ${file_path} does not exist" - download=false - else - # Check if the file or a file referenced by the symlink has a+r permissions - permissions=$(stat -L -c "%a" "$file_path") - if [[ $permissions == *4 || $permissions == *5 || $permissions == *6 || $permissions == *7 ]]; then - # Output that we are using the local file - echo -e "${OVER} ${INFO} Using local file ${file_path}" - else - # Output that the file does not have the correct permissions - echo -e "${OVER} ${CROSS} Cannot read file (file needs to have a+r permission)" + if [[ ! -f ${file_path} ]]; then + # Output that the file does not exist + echo -e "${OVER} ${CROSS} ${file_path} does not exist" download=false - fi + else + if [ "$(id -un)" == "root" ]; then + # If we are root, we need to check if the pihole user has read permission + # otherwise, we might read files that the pihole user should not be able to read + if sudo -u pihole test -r "${file_path}"; then + echo -e "${OVER} ${INFO} Using local file ${file_path}" + else + echo -e "${OVER} ${CROSS} Cannot read file (user 'pihole' lacks read permission)" + download=false + fi + else + # If we are not root, we just check if the current user has read permission + if [[ -r "${file_path}" ]]; then + # Output that we are using the local file + echo -e "${OVER} ${INFO} Using local file ${file_path}" + else + # Output that the file is not readable by the current user + echo -e "${OVER} ${CROSS} Cannot read file (current user '$(id -un)' lacks read permission)" + download=false + fi + fi fi fi # 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 @@ -855,7 +869,7 @@ gravity_Table_Count() { fi } -# Output count of blacklisted domains and regex filters +# Output count of denied and allowed 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. @@ -948,7 +962,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 -r recreate\" instead." + echo -e " ${CROSS} Recovery failed. Try \"pihole -g -r recreate\" instead." exit 1 fi echo "" @@ -1131,7 +1145,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}" diff --git a/manpages/pihole.8 b/manpages/pihole.8 index e0c38828..ac3146ba 100644 --- a/manpages/pihole.8 +++ b/manpages/pihole.8 @@ -105,9 +105,9 @@ Available commands and options: Flush the Pi-hole log .br -\fB-r, reconfigure\fR +\fB-r, repair\fR .br - Reconfigure or Repair Pi-hole subsystems + 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 blacklist. + Adds "ad.*\\.example\\.com$" to the regex denylist. Would block all subdomains of example.com which start with "ad" .br @@ -317,9 +317,10 @@ Switching Pi-hole subsystem branches Switch to core development branch .br -\fBpihole arpflush\fR +\fBpihole networkflush\fR .br - Flush information stored in Pi-hole's network tables + Flush information stored in Pi-hole's network table + Add '--arp' to additionally flush the ARP table .br \fBpihole api stats/summary\fR diff --git a/pihole b/pihole index 7b5e3eb8..e5d3172e 100755 --- a/pihole +++ b/pihole @@ -96,8 +96,18 @@ flushFunc() { exit 0 } +# Deprecated function, should be removed in the future +# use networkFlush instead arpFunc() { - "${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@" + 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 "$@" exit 0 } @@ -115,7 +125,22 @@ repairPiholeFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else - /etc/.pihole/automated\ install/basic-install.sh --repair + local skipFTL additionalFlag + skipFTL=false + # Check arguments + for var in "$@"; do + case "$var" in + "--skipFTL") skipFTL=true ;; + esac + done + + if [ "${skipFTL}" == true ]; then + additionalFlag="--skipFTL" + else + additionalFlag="" + fi + + /etc/.pihole/automated\ install/basic-install.sh --repair ${additionalFlag} exit 0; fi } @@ -147,10 +172,11 @@ uninstallFunc() { versionFunc() { exec "${PI_HOLE_SCRIPT_DIR}"/version.sh + exit 0 } reloadDNS() { - local svcOption svc str output status pid icon FTL_PID_FILE + local svcOption svc str output status pid icon FTL_PID_FILE sigrtmin svcOption="${1:-reload}" # get the current path to the pihole-FTL.pid @@ -169,7 +195,10 @@ reloadDNS() { str="FTL is not running" icon="${INFO}" else - svc="kill -RTMIN ${pid}" + 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}" str="Reloading DNS lists" icon="${TICK}" fi @@ -264,6 +293,7 @@ Time: LogoutAPI echo -e "${OVER} ${TICK} ${str}" + exit 0 } piholeLogging() { @@ -519,7 +549,8 @@ 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 - arpflush Flush information stored in Pi-hole's network tables"; + networkflush Flush information stored in Pi-hole's network tables + Add '--arp' to additionally flush the ARP table "; exit 0 } @@ -528,7 +559,7 @@ if [[ $# = 0 ]]; then fi # functions that do not require sudo power -need_root=1 +need_root= case "${1}" in "-h" | "help" | "--help" ) helpFunc;; "-v" | "version" ) versionFunc;; @@ -536,31 +567,32 @@ 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 - "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" ) need_root=0;; - "reloaddns" ) ;; - "reloadlists" ) ;; - "setpassword" ) ;; - "checkout" ) ;; - "updatechecker" ) ;; - "arpflush" ) ;; - "-t" | "tail" ) ;; - "api" ) need_root=0;; + "-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;; * ) helpFunc;; esac @@ -572,36 +604,29 @@ fi # Check if the current user is not root and if the command # requires root. If so, exit with an error message. -if [[ $EUID -ne 0 && need_root -eq 1 ]];then - echo -e " ${CROSS} The Pi-hole command requires root privileges, try:" +# 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:" 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 "$@";; - "-r" | "repair" ) repairPiholeFunc;; + "-r" | "repair" ) repairPiholeFunc "$@";; "-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 "$@";; + "arpflush" ) arpFunc "$@";; # Deprecated, use networkflush instead + "networkflush" ) networkFlush "$@";; "-t" | "tail" ) tailFunc "$2";; - "api" ) shift; apiFunc "$@";; * ) helpFunc;; esac diff --git a/test/_alpine_3_21.Dockerfile b/test/_alpine_3_21.Dockerfile new file mode 100644 index 00000000..d7b88f20 --- /dev/null +++ b/test/_alpine_3_21.Dockerfile @@ -0,0 +1,18 @@ +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 && \ diff --git a/test/_alpine_3_22.Dockerfile b/test/_alpine_3_22.Dockerfile new file mode 100644 index 00000000..25beb4e0 --- /dev/null +++ b/test/_alpine_3_22.Dockerfile @@ -0,0 +1,18 @@ +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 && \ diff --git a/test/_alpine_3_23.Dockerfile b/test/_alpine_3_23.Dockerfile new file mode 100644 index 00000000..2cb34137 --- /dev/null +++ b/test/_alpine_3_23.Dockerfile @@ -0,0 +1,18 @@ +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 && \ diff --git a/test/_debian_13.Dockerfile b/test/_debian_13.Dockerfile new file mode 100644 index 00000000..cfff2235 --- /dev/null +++ b/test/_debian_13.Dockerfile @@ -0,0 +1,16 @@ +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 && \ diff --git a/test/_fedora_43.Dockerfile b/test/_fedora_43.Dockerfile new file mode 100644 index 00000000..85f06ff8 --- /dev/null +++ b/test/_fedora_43.Dockerfile @@ -0,0 +1,17 @@ +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 && \ diff --git a/test/conftest.py b/test/conftest.py index dcf49790..d4c763e7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -51,29 +51,19 @@ def mock_command(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format( - script=script - ) - ) + case "\$1" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" {arg}) echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -94,37 +84,23 @@ def mock_command_passthrough(script, args, container): """ orig_script_path = container.check_output("command -v {}".format(script)) full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format( - script=script - ) - ) + case "\$1" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" {arg}) echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - r""" + mock_script += dedent(r""" *) {orig_script_path} "\$@" - ;;""".format( - orig_script_path=orig_script_path - ) - ) - mock_script += dedent( - """ - esac""" - ) + ;;""".format(orig_script_path=orig_script_path)) + mock_script += dedent(""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -141,29 +117,19 @@ def mock_command_run(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format( - script=script - ) - ) + case "\$1 \$2" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" \"{arg}\") echo {res} exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(r""" + esac""") container.run( """ cat < {script}\n{content}\nEOF @@ -180,29 +146,19 @@ def mock_command_2(script, args, container): in unit tests """ full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent( - r"""\ + mock_script = dedent(r"""\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format( - script=script - ) - ) + case "\$1 \$2" in""".format(script=script)) for k, v in args.items(): - case = dedent( - """ + case = dedent(""" \"{arg}\") echo \"{res}\" exit {retcode} - ;;""".format( - arg=k, res=v[0], retcode=v[1] - ) - ) + ;;""".format(arg=k, res=v[0], retcode=v[1])) mock_script += case - mock_script += dedent( - """ - esac""" - ) + mock_script += dedent(r""" + esac""") container.run( """ cat < {script}\n{content}\nEOF diff --git a/test/requirements.txt b/test/requirements.txt index 92f78840..61f18c4e 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,6 @@ -pyyaml == 6.0.2 -pytest == 8.4.1 +pyyaml == 6.0.3 +pytest == 9.0.2 pytest-xdist == 3.8.0 pytest-testinfra == 10.2.2 -tox == 4.27.0 +tox == 4.35.0 pytest-clarity == 1.0.1 diff --git a/test/test_any_automated_install.py b/test/test_any_automated_install.py index cf4b454d..aa48fd32 100644 --- a/test/test_any_automated_install.py +++ b/test/test_any_automated_install.py @@ -6,10 +6,8 @@ from .conftest import ( info_box, cross_box, mock_command, - mock_command_run, mock_command_2, mock_command_passthrough, - run_script, ) FTL_BRANCH = "development" @@ -22,12 +20,11 @@ def test_supported_package_manager(host): # break supported package managers host.run("rm -rf /usr/bin/apt-get") host.run("rm -rf /usr/bin/rpm") - package_manager_detect = host.run( - """ + host.run("rm -rf /sbin/apk") + 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 @@ -37,13 +34,11 @@ def test_selinux_not_detected(host): """ confirms installer continues when SELinux configuration file does not exist """ - check_selinux = host.run( - """ + 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 @@ -77,14 +72,24 @@ def test_installPihole_fresh_install_readableFiles(host): }, host, ) + mock_command_2( + "rc-service", + { + "rc-service pihole-FTL enable": ("", "0"), + "rc-service pihole-FTL restart": ("", "0"), + "rc-service pihole-FTL start": ("", "0"), + "*": ('echo "rc-service 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") + host.run("command -v apk > /dev/null && apk add mandoc man-pages") # Workaround to get FTLv6 installed until it reaches master branch host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - install = host.run( - """ + install = host.run(""" export TERM=xterm export DEBIAN_FRONTEND=noninteractive umask 0027 @@ -93,8 +98,7 @@ def test_installPihole_fresh_install_readableFiles(host): runUnattended=true main /opt/pihole/pihole-FTL-prestart.sh - """ - ) + """) assert 0 == install.rc maninstalled = True if (info_box + " man not installed") in install.stdout: @@ -103,7 +107,7 @@ def test_installPihole_fresh_install_readableFiles(host): maninstalled = False piholeuser = "pihole" exit_status_success = 0 - test_cmd = 'su --shell /bin/bash --command "test -{0} {1}" -p {2}' + test_cmd = 'su -s /bin/bash -c "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 @@ -150,12 +154,6 @@ def test_installPihole_fresh_install_readableFiles(host): 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 ) @@ -189,13 +187,11 @@ def test_update_package_cache_success_no_errors(host): """ confirms package cache was updated without any errors """ - updateCache = host.run( - """ + 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() @@ -206,13 +202,11 @@ 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( - """ + 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 @@ -248,16 +242,14 @@ def test_FTL_detect_no_errors(host, arch, detected_string, supported): host, ) host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - detectPlatform = host.run( - """ + 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 @@ -277,22 +269,18 @@ 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( - """ + 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_check = host.run(""" VERSION=$(pihole-FTL version) echo ${VERSION:0:1} - """ - ) + """) expected_stdout = "v" assert expected_stdout in version_check.stdout @@ -307,12 +295,10 @@ def test_IPv6_only_link_local(host): {"-6 address": ("inet6 fe80::d210:52fa:fe00:7ad7/64 scope link", "0")}, host, ) - detectPlatform = host.run( - """ + 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 @@ -332,12 +318,10 @@ def test_IPv6_only_ULA(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -357,12 +341,10 @@ def test_IPv6_only_GUA(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 GUA address" assert expected_stdout in detectPlatform.stdout @@ -383,12 +365,10 @@ def test_IPv6_GUA_ULA_test(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -409,12 +389,10 @@ def test_IPv6_ULA_GUA_test(host): }, host, ) - detectPlatform = host.run( - """ + detectPlatform = host.run(""" source /opt/pihole/basic-install.sh find_IPv6_information - """ - ) + """) expected_stdout = "Found IPv6 ULA address" assert expected_stdout in detectPlatform.stdout @@ -425,14 +403,10 @@ def test_validate_ip(host): """ def test_address(addr, success=True): - output = host.run( - """ + output = host.run(""" source /opt/pihole/basic-install.sh valid_ip "{addr}" - """.format( - addr=addr - ) - ) + """.format(addr=addr)) assert output.rc == 0 if success else 1 @@ -467,15 +441,13 @@ def test_validate_ip(host): 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( - """ + output = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache build_dependency_package install_dependent_packages - """ - ) + """) assert "No package" not in output.stdout assert output.rc == 0 @@ -484,21 +456,17 @@ def test_package_manager_has_pihole_deps(host): 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( - """ + install = host.run(""" source /opt/pihole/basic-install.sh package_manager_detect update_package_cache build_dependency_package install_dependent_packages - """ - ) + """) assert install.rc == 0 - uninstall = host.run( - """ + uninstall = host.run(""" source /opt/pihole/uninstall.sh removeMetaPackage - """ - ) + """) assert uninstall.rc == 0 diff --git a/test/test_any_utils.py b/test/test_any_utils.py index 0f9ae6d2..43e637f3 100644 --- a/test/test_any_utils.py +++ b/test/test_any_utils.py @@ -1,31 +1,25 @@ def test_key_val_replacement_works(host): """Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file""" - host.run( - """ + 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( - """ + """) + 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( - """ + output = host.run(""" source /opt/pihole/utils.sh getFTLPID - """ - ) + """) expected_stdout = "-1\n" assert expected_stdout == output.stdout @@ -36,8 +30,7 @@ def test_setFTLConfigValue_getFTLConfigValue(host): Requires FTL to be installed, so we do that first (taken from test_FTL_development_binary_installed_and_responsive_no_errors) """ - host.run( - """ + host.run(""" source /opt/pihole/basic-install.sh create_pihole_user funcOutput=$(get_binary_name) @@ -45,15 +38,12 @@ def test_setFTLConfigValue_getFTLConfigValue(host): binary="pihole-FTL${funcOutput##*pihole-FTL}" theRest="${funcOutput%pihole-FTL*}" FTLdetect "${binary}" "${theRest}" - """ - ) + """) - output = host.run( - """ + 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 diff --git a/test/test_centos_fedora_common_support.py b/test/test_centos_fedora_common_support.py index 7e0bae4e..a892db87 100644 --- a/test/test_centos_fedora_common_support.py +++ b/test/test_centos_fedora_common_support.py @@ -15,14 +15,10 @@ def mock_selinux_config(state, host): # getenforce returns the running state of SELinux mock_command("getenforce", {"*": (state.capitalize(), "0")}, host) # create mock configuration with desired content - host.run( - """ + host.run(""" mkdir /etc/selinux echo "SELINUX={state}" > /etc/selinux/config - """.format( - state=state.lower() - ) - ) + """.format(state=state.lower())) def test_selinux_enforcing_exit(host): @@ -30,12 +26,10 @@ 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( - """ + 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" @@ -48,12 +42,10 @@ def test_selinux_permissive(host): confirms installer continues when SELinux is Permissive """ mock_selinux_config("permissive", host) - check_selinux = host.run( - """ + 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 @@ -64,12 +56,10 @@ def test_selinux_disabled(host): confirms installer continues when SELinux is Disabled """ mock_selinux_config("disabled", host) - check_selinux = host.run( - """ + 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 diff --git a/test/tox.alpine_3_21.ini b/test/tox.alpine_3_21.ini new file mode 100644 index 00000000..b0465f6c --- /dev/null +++ b/test/tox.alpine_3_21.ini @@ -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 _alpine_3_21.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.alpine_3_22.ini b/test/tox.alpine_3_22.ini new file mode 100644 index 00000000..38f66c4f --- /dev/null +++ b/test/tox.alpine_3_22.ini @@ -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 _alpine_3_22.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.alpine_3_23.ini b/test/tox.alpine_3_23.ini new file mode 100644 index 00000000..d7208064 --- /dev/null +++ b/test/tox.alpine_3_23.ini @@ -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 _alpine_3_23.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.debian_13.ini b/test/tox.debian_13.ini new file mode 100644 index 00000000..dcfbf816 --- /dev/null +++ b/test/tox.debian_13.ini @@ -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_13.Dockerfile -t pytest_pihole:test_container ../ + pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.fedora_43.ini b/test/tox.fedora_43.ini new file mode 100644 index 00000000..efbb0471 --- /dev/null +++ b/test/tox.fedora_43.ini @@ -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_43.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