first commit
This commit is contained in:
137
ceres-backup.sh
Executable file
137
ceres-backup.sh
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
# ceres-backup.sh – Snapshot-freundliches Mirror-Backup auf Synology NAS
|
||||
# Spiegeln statt Tarballs; nur Portainer-DB als einzelne Datei (fixer Name).
|
||||
set -euo pipefail
|
||||
|
||||
# ── NAS-Ziel ───────────────────────────────────────────────────────
|
||||
NAS_USER="backup-master"
|
||||
NAS_HOST="192.168.178.74"
|
||||
NAS_PORT=10022
|
||||
NAS_BASE="/volume1/ceres-raspi5/mirror"
|
||||
|
||||
# ── Optionen / Tools ───────────────────────────────────────────────
|
||||
RSYNC_SSH=(-e "ssh -p ${NAS_PORT}")
|
||||
RSYNC_OPTS=(-aHAX --delete --numeric-ids)
|
||||
DATE="$(date +%F)"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
# Prüfe Abhängigkeiten
|
||||
for bin in curl jq rsync ssh; do
|
||||
command -v "$bin" >/dev/null 2>&1 || { echo "Fehlt: $bin"; exit 1; }
|
||||
done
|
||||
|
||||
# Heimatverzeichnis fest (systemd hat oft kein USER/SUDO_USER gesetzt)
|
||||
REAL_USER="${REAL_USER:-pdavidt}"
|
||||
HOME_DIR="${HOME_DIR:-/home/pdavidt}"
|
||||
[ -d "$HOME_DIR" ] || { echo "HOME_DIR nicht gefunden: $HOME_DIR"; exit 1; }
|
||||
|
||||
# Remote-Verzeichnis sicher anlegen
|
||||
ensure_remote_dir() {
|
||||
local remote_dir="$1"
|
||||
ssh -p "${NAS_PORT}" "${NAS_USER}@${NAS_HOST}" "mkdir -p ${remote_dir}"
|
||||
}
|
||||
|
||||
# Rsync-Mirror-Helfer
|
||||
mirror_dir() {
|
||||
local src="$1"
|
||||
local dest_rel="$2" # relativ zu NAS_BASE
|
||||
ensure_remote_dir "${NAS_BASE}/${dest_rel}"
|
||||
rsync "${RSYNC_OPTS[@]}" "${RSYNC_SSH[@]}" "${src%/}/" \
|
||||
"${NAS_USER}@${NAS_HOST}:${NAS_BASE}/${dest_rel}/"
|
||||
}
|
||||
|
||||
mirror_files() {
|
||||
# mehrere Dateien in dasselbe Ziel spiegeln (kein trailing slash)
|
||||
local dest_rel="$1"; shift
|
||||
ensure_remote_dir "${NAS_BASE}/${dest_rel}"
|
||||
rsync "${RSYNC_OPTS[@]}" "${RSYNC_SSH[@]}" "$@" \
|
||||
"${NAS_USER}@${NAS_HOST}:${NAS_BASE}/${dest_rel}/"
|
||||
}
|
||||
|
||||
echo "[*] Backup gestartet: ${DATE}"
|
||||
|
||||
# ── 1) Portainer-Backup via API (Token, JSON-POST) → fixer Dateiname ─────────
|
||||
PORTAINER_URL="http://127.0.0.1:9000" # lokal ist robuster als Traefik
|
||||
PORTAINER_TOKEN="$(cat /root/.portainer-token 2>/dev/null || true)"
|
||||
# optional: Passphrase aus Datei (falls du das Backup verschlüsseln willst)
|
||||
PORTAINER_BACKUP_PASS="$(cat /root/.portainer-backup-pass 2>/dev/null || true)"
|
||||
OUT="${TMP_DIR}/portainer-backup.tar.gz"
|
||||
|
||||
if [ -z "$PORTAINER_TOKEN" ]; then
|
||||
echo "WARN: /root/.portainer-token fehlt – Portainer-Backup übersprungen."
|
||||
else
|
||||
echo "[*] Portainer-Backup via API (Token, JSON POST)…"
|
||||
JSON_BODY='{}'
|
||||
[ -n "$PORTAINER_BACKUP_PASS" ] && JSON_BODY=$(printf '{"password":"%s"}' "$PORTAINER_BACKUP_PASS")
|
||||
|
||||
if curl -fsS -X POST \
|
||||
-H "X-API-Key: ${PORTAINER_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${JSON_BODY}" \
|
||||
-o "${OUT}" \
|
||||
"${PORTAINER_URL}/api/backup"
|
||||
then
|
||||
if file "${OUT}" | grep -qi 'gzip compressed data'; then
|
||||
ensure_remote_dir "${NAS_BASE}/portainer"
|
||||
rsync "${RSYNC_OPTS[@]}" "${RSYNC_SSH[@]}" \
|
||||
"${OUT}" "${NAS_USER}@${NAS_HOST}:${NAS_BASE}/portainer/portainer-backup.tar.gz"
|
||||
echo "[OK] Portainer-Backup gespeichert."
|
||||
else
|
||||
echo "ERROR: Portainer-Backup ist keine gzip-Datei – Antwort (erste 300 B):"
|
||||
head -c 300 "${OUT}" | sed -e 's/[^[:print:]\t]/./g'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Portainer-Backup-Request fehlgeschlagen."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Pi-hole Teleporter (neue Syntax via pihole-FTL) ───────────────
|
||||
echo "[*] Pi-hole Teleporter-Backup…"
|
||||
|
||||
cd "${TMP_DIR}"
|
||||
OUTFILE=$(sudo pihole-FTL --teleporter | tail -n1)
|
||||
|
||||
if [ ! -s "${TMP_DIR}/${OUTFILE}" ]; then
|
||||
echo "ERROR: Teleporter-Backup wurde nicht erstellt."; exit 1
|
||||
fi
|
||||
|
||||
ensure_remote_dir "${NAS_BASE}/pihole"
|
||||
rsync "${RSYNC_OPTS[@]}" "${RSYNC_SSH[@]}" \
|
||||
"${TMP_DIR}/${OUTFILE}" \
|
||||
"${NAS_USER}@${NAS_HOST}:${NAS_BASE}/pihole/pihole-teleporter.tar.gz"
|
||||
|
||||
echo "[OK] Pi-hole Teleporter gesichert als ${OUTFILE} → NAS"
|
||||
|
||||
# ── 3) Homepage ────────────────────────────────────────────────────
|
||||
if [ -d "${HOME_DIR}/homepage" ]; then
|
||||
echo "[*] Homepage spiegeln…"
|
||||
mirror_dir "${HOME_DIR}/homepage" "homepage"
|
||||
else
|
||||
echo "INFO: ${HOME_DIR}/homepage nicht gefunden – übersprungen."
|
||||
fi
|
||||
|
||||
# ── 4) Traefik (inkl. acme.json) ───────────────────────────────────
|
||||
if [ -d "${HOME_DIR}/traefik" ]; then
|
||||
echo "[*] Traefik spiegeln…"
|
||||
mirror_dir "${HOME_DIR}/traefik" "traefik"
|
||||
else
|
||||
echo "INFO: ${HOME_DIR}/traefik nicht gefunden – übersprungen."
|
||||
fi
|
||||
|
||||
# ── 5) WireGuard (lokal) ───────────────────────────────────────────
|
||||
echo "[*] WireGuard spiegeln…"
|
||||
mirror_dir "/etc/wireguard" "wireguard"
|
||||
|
||||
# ── 6) Wichtige Systemdateien ──────────────────────────────────────
|
||||
echo "[*] Systemdateien spiegeln…"
|
||||
# Einzeldateien
|
||||
mirror_files "system/etc" /etc/hostname /etc/hosts /etc/fstab /etc/sysctl.conf
|
||||
# Verzeichnisse
|
||||
mirror_dir "/etc/ssh" "system/etc/ssh"
|
||||
mirror_dir "/etc/systemd/system" "system/systemd"
|
||||
mirror_dir "/usr/local/bin" "system/usr-local-bin"
|
||||
|
||||
echo "[OK] Backup fertig: ${DATE}"
|
||||
117
cloudflare-ddns.sh
Executable file
117
cloudflare-ddns.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cloudflare DDNS – Multi-Record (A), mehrere Zonen
|
||||
# Pfadvorschlag: /usr/local/bin/cloudflare-ddns.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Konfiguration ──────────────────────────────────────────────────────────────
|
||||
# !!! Trage hier deinen echten Token ein
|
||||
CF_API_TOKEN="UsC5_INInUKoanrSIITOxZQMso3uQUZPMUIMQW9t"
|
||||
|
||||
# Zonen
|
||||
CF_ZONE_ID_DE="3059e7e1c29c962544c82e941f60fcb6"
|
||||
CF_ZONE_ID_CLOUD="836d573a41c5dc1a0a1712332d4facb0"
|
||||
|
||||
# DNS-Eintrag 1: nas.davidt.de
|
||||
CF_RECORD_ID_1="d0cbc0b4294b6c1e76dafb8e5309893d"
|
||||
CF_RECORD_NAME_1="nas.davidt.de"
|
||||
CF_RECORD_ZONE_1="$CF_ZONE_ID_DE"
|
||||
|
||||
# DNS-Eintrag 2: vpn.davidt.de
|
||||
#CF_RECORD_ID_2="430898a21e676d72b6ab90f23cda74cd"
|
||||
#CF_RECORD_NAME_2="vpn.davidt.de"
|
||||
#CF_RECORD_ZONE_2="$CF_ZONE_ID_DE"
|
||||
|
||||
# DNS-Eintrag 3: mc.davidt.cloud
|
||||
CF_RECORD_ID_3="4a964d71eda3de8d1646bf1263d29cce"
|
||||
CF_RECORD_NAME_3="mc.davidt.cloud"
|
||||
CF_RECORD_ZONE_3="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 4: remote.davidt.cloud
|
||||
CF_RECORD_ID_4="5dff7541531c24a151b00c6b7f7bd66c"
|
||||
CF_RECORD_NAME_4="remote.davidt.cloud"
|
||||
CF_RECORD_ZONE_4="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 5: mc2.davidt.cloud
|
||||
CF_RECORD_ID_5="d0e1b3c84fe55bcaa21509f16218503b"
|
||||
CF_RECORD_NAME_5="mc2.davidt.cloud"
|
||||
CF_RECORD_ZONE_5="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 6: matrix.davidt.cloud
|
||||
CF_RECORD_ID_6="10df7a1dd299ae2317a6719d74964453"
|
||||
CF_RECORD_NAME_6="matrix.davidt.cloud"
|
||||
CF_RECORD_ZONE_6="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 7: mas.davidt.cloud
|
||||
CF_RECORD_ID_7="f0b8cae60639c13bb01955aee8ac9cf6"
|
||||
CF_RECORD_NAME_7="mas.davidt.cloud"
|
||||
CF_RECORD_ZONE_7="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 8: pad.davidt.cloud
|
||||
#CF_RECORD_ID_8="075a76da5c40392584eaab0ff567e770"
|
||||
#CF_RECORD_NAME_8="pad.davidt.cloud"
|
||||
#CF_RECORD_ZONE_8="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# DNS-Eintrag 9: sandbox-pad.davidt.cloud
|
||||
#CF_RECORD_ID_9="d93837978a456b39fcd40b17a3216b2a"
|
||||
#CF_RECORD_NAME_9="sandbox-pad.davidt.cloud"
|
||||
#CF_RECORD_ZONE_9="$CF_ZONE_ID_CLOUD"
|
||||
|
||||
# IPv6 verwenden?
|
||||
USE_IPV6=false
|
||||
|
||||
# ── aktuelle öffentliche IP ermitteln ─────────────────────────────────────────
|
||||
if [[ "$USE_IPV6" == "true" ]]; then
|
||||
CURRENT_IP="$(curl -fsS --max-time 5 https://ifconfig.co/ip || true)"
|
||||
else
|
||||
CURRENT_IP="$(curl -fsS --max-time 5 https://ipv4.icanhazip.com || true)"
|
||||
fi
|
||||
CURRENT_IP="${CURRENT_IP//$'\n'/}" # trim newline
|
||||
|
||||
if [[ -z "$CURRENT_IP" ]]; then
|
||||
echo "✗ Konnte öffentliche IP nicht ermitteln."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IP_FILE="/tmp/cf_last_ip.txt"
|
||||
LAST_IP=""
|
||||
[[ -f "$IP_FILE" ]] && LAST_IP="$(cat "$IP_FILE")"
|
||||
|
||||
# ── nur updaten, wenn sich die IP geändert hat ────────────────────────────────
|
||||
if [[ "$CURRENT_IP" != "$LAST_IP" ]]; then
|
||||
echo "IP hat sich geändert: ${LAST_IP:-<none>} -> $CURRENT_IP"
|
||||
|
||||
for i in {1..9}; do
|
||||
CF_RECORD_ID_VAR="CF_RECORD_ID_${i}"
|
||||
CF_RECORD_NAME_VAR="CF_RECORD_NAME_${i}"
|
||||
CF_RECORD_ZONE_VAR="CF_RECORD_ZONE_${i}"
|
||||
|
||||
RECORD_ID="${!CF_RECORD_ID_VAR:-}"
|
||||
RECORD_NAME="${!CF_RECORD_NAME_VAR:-}"
|
||||
ZONE_ID="${!CF_RECORD_ZONE_VAR:-}"
|
||||
|
||||
[[ -z "$RECORD_ID" || -z "$RECORD_NAME" || -z "$ZONE_ID" ]] && continue
|
||||
|
||||
echo "→ Aktualisiere ${RECORD_NAME}"
|
||||
|
||||
RESPONSE="$(
|
||||
curl -fsS -X PUT \
|
||||
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"content\":\"${CURRENT_IP}\",\"ttl\":120,\"proxied\":false}" \
|
||||
|| true
|
||||
)"
|
||||
|
||||
if grep -q '"success":true' <<<"$RESPONSE"; then
|
||||
echo "✓ ${RECORD_NAME} erfolgreich aktualisiert."
|
||||
else
|
||||
echo "✗ Fehler bei ${RECORD_NAME}:"
|
||||
echo "$RESPONSE"
|
||||
fi
|
||||
done
|
||||
|
||||
printf "%s" "$CURRENT_IP" > "$IP_FILE"
|
||||
else
|
||||
echo "IP unverändert. Kein Update nötig."
|
||||
fi
|
||||
607
pihole
Executable file
607
pihole
Executable file
@@ -0,0 +1,607 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Pi-hole: A black hole for Internet advertisements
|
||||
# (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
||||
# Network-wide ad blocking via your own hardware.
|
||||
#
|
||||
# Controller for all pihole scripts and functions.
|
||||
#
|
||||
# This file is copyright under the latest version of the EUPL.
|
||||
# Please see LICENSE file for your rights under this license.
|
||||
|
||||
PI_HOLE_SCRIPT_DIR="/opt/pihole"
|
||||
|
||||
# PI_HOLE_BIN_DIR is not readonly here because in some functions (checkout),
|
||||
# they might get set again when the installer is sourced. This causes an
|
||||
# error due to modifying a readonly variable.
|
||||
PI_HOLE_BIN_DIR="/usr/local/bin"
|
||||
|
||||
readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
|
||||
# shellcheck source=./advanced/Scripts/COL_TABLE
|
||||
source "${colfile}"
|
||||
|
||||
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
||||
# shellcheck source=./advanced/Scripts/utils.sh
|
||||
source "${utilsfile}"
|
||||
|
||||
# Source api functions
|
||||
readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
|
||||
# shellcheck source=./advanced/Scripts/api.sh
|
||||
source "${apifile}"
|
||||
|
||||
versionsfile="/etc/pihole/versions"
|
||||
if [ -f "${versionsfile}" ]; then
|
||||
# Only source versionsfile if the file exits
|
||||
# fixes a warning during installation where versionsfile does not exist yet
|
||||
# but gravity calls `pihole -status` and thereby sourcing the file
|
||||
# shellcheck source=/dev/null
|
||||
source "${versionsfile}"
|
||||
fi
|
||||
|
||||
# TODO: We can probably remove the reliance on this function too, just tell people to pihole-FTL --config webserver.api.password "password"
|
||||
SetWebPassword() {
|
||||
if [ -n "$2" ] ; then
|
||||
readonly PASSWORD="$2"
|
||||
readonly CONFIRM="${PASSWORD}"
|
||||
else
|
||||
# Prevents a bug if the user presses Ctrl+C and it continues to hide the text typed.
|
||||
# So we reset the terminal via stty if the user does press Ctrl+C
|
||||
trap '{ echo -e "\nNot changed" ; stty sane ; exit 1; }' INT
|
||||
read -s -r -p "Enter New Password (Blank for no password): " PASSWORD
|
||||
echo ""
|
||||
|
||||
if [ "${PASSWORD}" == "" ]; then
|
||||
setFTLConfigValue "webserver.api.password" ""
|
||||
echo -e " ${TICK} Password Removed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read -s -r -p "Confirm Password: " CONFIRM
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "${PASSWORD}" == "${CONFIRM}" ] ; then
|
||||
# pihole-FTL will automatically hash the password
|
||||
setFTLConfigValue "webserver.api.password" "${PASSWORD}"
|
||||
echo -e " ${TICK} New password set"
|
||||
else
|
||||
echo -e " ${CROSS} Passwords don't match. Your password has not been changed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
listFunc() {
|
||||
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
debugFunc() {
|
||||
local automated
|
||||
local check_database_integrity
|
||||
# Pull off the `debug` leaving passed call augmentation flags in $1
|
||||
shift
|
||||
|
||||
for value in "$@"; do
|
||||
[[ "$value" == *"-a"* ]] && automated="true"
|
||||
[[ "$value" == *"-c"* ]] && check_database_integrity="true"
|
||||
[[ "$value" == *"--check_database"* ]] && check_database_integrity="true"
|
||||
done
|
||||
|
||||
AUTOMATED=${automated:-} CHECK_DATABASE=${check_database_integrity:-} "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh
|
||||
exit 0
|
||||
}
|
||||
|
||||
flushFunc() {
|
||||
"${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
arpFunc() {
|
||||
"${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
updatePiholeFunc() {
|
||||
if [ -n "${DOCKER_VERSION}" ]; then
|
||||
unsupportedFunc
|
||||
else
|
||||
shift
|
||||
"${PI_HOLE_SCRIPT_DIR}"/update.sh "$@"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
repairPiholeFunc() {
|
||||
if [ -n "${DOCKER_VERSION}" ]; then
|
||||
unsupportedFunc
|
||||
else
|
||||
/etc/.pihole/automated\ install/basic-install.sh --repair
|
||||
exit 0;
|
||||
fi
|
||||
}
|
||||
|
||||
updateGravityFunc() {
|
||||
exec "${PI_HOLE_SCRIPT_DIR}"/gravity.sh "$@"
|
||||
}
|
||||
|
||||
queryFunc() {
|
||||
shift
|
||||
"${PI_HOLE_SCRIPT_DIR}"/query.sh "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
chronometerFunc() {
|
||||
echo "Chronometer is gone, use PADD (https://github.com/pi-hole/PADD)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
uninstallFunc() {
|
||||
if [ -n "${DOCKER_VERSION}" ]; then
|
||||
unsupportedFunc
|
||||
else
|
||||
"${PI_HOLE_SCRIPT_DIR}"/uninstall.sh
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
versionFunc() {
|
||||
exec "${PI_HOLE_SCRIPT_DIR}"/version.sh
|
||||
}
|
||||
|
||||
reloadDNS() {
|
||||
local svcOption svc str output status pid icon FTL_PID_FILE
|
||||
svcOption="${1:-reload}"
|
||||
|
||||
# get the current path to the pihole-FTL.pid
|
||||
FTL_PID_FILE="$(getFTLConfigValue files.pid)"
|
||||
|
||||
# Determine if we should reload or restart
|
||||
if [[ "${svcOption}" =~ "reload-lists" ]]; then
|
||||
# Reloading of the lists has been requested
|
||||
# Note 1: This will NOT re-read any *.conf files
|
||||
# Note 2: We cannot use killall here as it does
|
||||
# not know about real-time signals
|
||||
|
||||
pid="$(getFTLPID ${FTL_PID_FILE})"
|
||||
if [[ "$pid" -eq "-1" ]]; then
|
||||
svc="true"
|
||||
str="FTL is not running"
|
||||
icon="${INFO}"
|
||||
else
|
||||
svc="kill -RTMIN ${pid}"
|
||||
str="Reloading DNS lists"
|
||||
icon="${TICK}"
|
||||
fi
|
||||
elif [[ "${svcOption}" =~ "reload" ]]; then
|
||||
# Reloading of the DNS cache has been requested
|
||||
# Note: This will NOT re-read any *.conf files
|
||||
pid="$(getFTLPID ${FTL_PID_FILE})"
|
||||
if [[ "$pid" -eq "-1" ]]; then
|
||||
svc="true"
|
||||
str="FTL is not running"
|
||||
icon="${INFO}"
|
||||
else
|
||||
svc="kill -HUP ${pid}"
|
||||
str="Flushing DNS cache"
|
||||
icon="${TICK}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print output to Terminal, but not to Web Admin
|
||||
[[ -t 1 ]] && echo -ne " ${INFO} ${str}..."
|
||||
|
||||
output=$( { ${svc}; } 2>&1 )
|
||||
status="$?"
|
||||
|
||||
if [[ "${status}" -eq 0 ]]; then
|
||||
[[ -t 1 ]] && echo -e "${OVER} ${icon} ${str}"
|
||||
return 0
|
||||
else
|
||||
[[ ! -t 1 ]] && local OVER=""
|
||||
echo -e "${OVER} ${CROSS} ${output}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
piholeEnable() {
|
||||
if [[ "${2}" == "-h" ]] || [[ "${2}" == "--help" ]]; then
|
||||
echo "Usage: pihole enable/disable [time]
|
||||
Example: 'pihole enable', or 'pihole disable 5m'
|
||||
En- or disable Pi-hole subsystems
|
||||
|
||||
Time:
|
||||
#s En-/disable Pi-hole functionality for # second(s)
|
||||
#m En-/disable Pi-hole functionality for # minute(s)"
|
||||
exit 0
|
||||
|
||||
fi
|
||||
|
||||
# Get timer
|
||||
local tt="null"
|
||||
if [[ $# -gt 1 ]]; then
|
||||
local error=false
|
||||
if [[ "${2}" == *"s" ]]; then
|
||||
tt=${2%"s"}
|
||||
if [[ ! "${tt}" =~ ^-?[0-9]+$ ]];then
|
||||
local error=true
|
||||
fi
|
||||
elif [[ "${2}" == *"m" ]]; then
|
||||
tt=${2%"m"}
|
||||
if [[ "${tt}" =~ ^-?[0-9]+$ ]];then
|
||||
tt=$((${tt}*60))
|
||||
else
|
||||
local error=true
|
||||
fi
|
||||
elif [[ -n "${2}" ]]; then
|
||||
local error=true
|
||||
fi
|
||||
|
||||
if [[ ${error} == true ]];then
|
||||
echo -e " ${COL_RED}Unknown format for blocking timer!${COL_NC}"
|
||||
echo -e " Try 'pihole disable --help' for more information."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Authenticate with the API
|
||||
LoginAPI
|
||||
|
||||
# Send the request
|
||||
data=$(PostFTLData "dns/blocking" "{ \"blocking\": ${1}, \"timer\": ${tt} }")
|
||||
|
||||
# Check the response
|
||||
local extra timer
|
||||
extra=" forever"
|
||||
timer="$(echo "${data}"| jq --raw-output '.timer' )"
|
||||
if [[ "${timer}" != "null" ]]; then
|
||||
extra=" for ${timer}s"
|
||||
fi
|
||||
local str
|
||||
str="Pi-hole $(echo "${data}" | jq --raw-output '.blocking')${extra}"
|
||||
|
||||
# Logout from the API
|
||||
LogoutAPI
|
||||
|
||||
echo -e "${OVER} ${TICK} ${str}"
|
||||
}
|
||||
|
||||
piholeLogging() {
|
||||
shift
|
||||
if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then
|
||||
echo "Usage: pihole logging [options]
|
||||
Example: 'pihole logging on'
|
||||
Specify whether the Pi-hole log should be used
|
||||
|
||||
Options:
|
||||
on Enable the Pi-hole log at /var/log/pihole/pihole.log
|
||||
off Disable and flush the Pi-hole log at /var/log/pihole/pihole.log
|
||||
off noflush Disable the Pi-hole log at /var/log/pihole/pihole.log"
|
||||
exit 0
|
||||
elif [[ "${1}" == "off" ]]; then
|
||||
# Disable logging
|
||||
setFTLConfigValue dns.queryLogging false
|
||||
if [[ "${2}" != "noflush" ]]; then
|
||||
# Flush logs
|
||||
"${PI_HOLE_BIN_DIR}"/pihole -f
|
||||
fi
|
||||
echo -e " ${INFO} Disabling logging..."
|
||||
local str="Logging has been disabled!"
|
||||
elif [[ "${1}" == "on" ]]; then
|
||||
# Enable logging
|
||||
setFTLConfigValue dns.queryLogging true
|
||||
echo -e " ${INFO} Enabling logging..."
|
||||
local str="Logging has been enabled!"
|
||||
else
|
||||
echo -e " ${COL_RED}Invalid option${COL_NC}
|
||||
Try 'pihole logging --help' for more information."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${OVER} ${TICK} ${str}"
|
||||
}
|
||||
|
||||
analyze_ports() {
|
||||
local lv4 lv6 port=${1}
|
||||
# FTL is listening at least on at least one port when this
|
||||
# function is getting called
|
||||
# Check individual address family/protocol combinations
|
||||
# For a healthy Pi-hole, they should all be up (nothing printed)
|
||||
lv4="$(ss --ipv4 --listening --numeric --tcp --udp src :${port})"
|
||||
if grep -q "udp " <<< "${lv4}"; then
|
||||
echo -e " ${TICK} UDP (IPv4)"
|
||||
else
|
||||
echo -e " ${CROSS} UDP (IPv4)"
|
||||
fi
|
||||
if grep -q "tcp " <<< "${lv4}"; then
|
||||
echo -e " ${TICK} TCP (IPv4)"
|
||||
else
|
||||
echo -e " ${CROSS} TCP (IPv4)"
|
||||
fi
|
||||
lv6="$(ss --ipv6 --listening --numeric --tcp --udp src :${port})"
|
||||
if grep -q "udp " <<< "${lv6}"; then
|
||||
echo -e " ${TICK} UDP (IPv6)"
|
||||
else
|
||||
echo -e " ${CROSS} UDP (IPv6)"
|
||||
fi
|
||||
if grep -q "tcp " <<< "${lv6}"; then
|
||||
echo -e " ${TICK} TCP (IPv6)"
|
||||
else
|
||||
echo -e " ${CROSS} TCP (IPv6)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
statusFunc() {
|
||||
# Determine if there is pihole-FTL service is listening
|
||||
local pid port ftl_pid_file block_status
|
||||
|
||||
ftl_pid_file="$(getFTLConfigValue files.pid)"
|
||||
|
||||
pid="$(getFTLPID ${ftl_pid_file})"
|
||||
|
||||
if [[ "$pid" -eq "-1" ]]; then
|
||||
case "${1}" in
|
||||
"web") echo "-1";;
|
||||
*) echo -e " ${CROSS} DNS service is NOT running";;
|
||||
esac
|
||||
exit 0
|
||||
else
|
||||
# get the DNS port pihole-FTL is listening on
|
||||
port="$(getFTLConfigValue dns.port)"
|
||||
if [[ "${port}" == "0" ]]; then
|
||||
case "${1}" in
|
||||
"web") echo "-1";;
|
||||
*) echo -e " ${CROSS} DNS service is NOT listening";;
|
||||
esac
|
||||
exit 0
|
||||
else
|
||||
if [[ "${1}" != "web" ]]; then
|
||||
echo -e " ${TICK} FTL is listening on port ${port}"
|
||||
analyze_ports "${port}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine if Pi-hole's blocking is enabled
|
||||
block_status=$(getFTLConfigValue dns.blocking.active)
|
||||
if [ ${block_status} == "true" ]; then
|
||||
case "${1}" in
|
||||
"web") echo "$port";;
|
||||
*) echo -e " ${TICK} Pi-hole blocking is enabled";;
|
||||
esac
|
||||
else
|
||||
case "${1}" in
|
||||
"web") echo 0;;
|
||||
*) echo -e " ${CROSS} Pi-hole blocking is disabled";;
|
||||
esac
|
||||
fi
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
tailFunc() {
|
||||
# Warn user if Pi-hole's logging is disabled
|
||||
local logging_enabled
|
||||
logging_enabled=$(getFTLConfigValue dns.queryLogging)
|
||||
if [[ "${logging_enabled}" != "true" ]]; then
|
||||
echo " ${CROSS} Warning: Query logging is disabled"
|
||||
fi
|
||||
echo -e " ${INFO} Press Ctrl-C to exit"
|
||||
|
||||
# Get logfile path
|
||||
LOGFILE=$(getFTLConfigValue files.log.dnsmasq)
|
||||
readonly LOGFILE
|
||||
|
||||
# Strip date from each line
|
||||
# Color blocklist/denylist/wildcard entries as red
|
||||
# Color A/AAAA/DHCP strings as white
|
||||
# Color everything else as gray
|
||||
tail -f $LOGFILE | grep --line-buffered -- "${1}" | sed -E \
|
||||
-e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \
|
||||
-e "s,(.*(denied |gravity blocked ).*),${COL_RED}&${COL_NC}," \
|
||||
-e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \
|
||||
-e "s,.*,${COL_GRAY}&${COL_NC},"
|
||||
exit 0
|
||||
}
|
||||
|
||||
piholeCheckoutFunc() {
|
||||
if [ -n "${DOCKER_VERSION}" ]; then
|
||||
echo -e "${CROSS} Function not supported in Docker images"
|
||||
echo "Please build a custom image following the steps at"
|
||||
echo "https://github.com/pi-hole/docker-pi-hole?tab=readme-ov-file#building-the-image-locally"
|
||||
exit 0
|
||||
else
|
||||
if [[ "$2" == "-h" ]] || [[ "$2" == "--help" ]]; then
|
||||
echo "Switch Pi-hole subsystems to a different GitHub branch
|
||||
Usage: ${COL_GREEN}pihole checkout${COL_NC} ${COL_YELLOW}shortcut${COL_NC}
|
||||
or ${COL_GREEN}pihole checkout${COL_NC} ${COL_PURPLE}repo${COL_NC} ${COL_CYAN}branch${COL_NC}
|
||||
|
||||
Example: ${COL_GREEN}pihole checkout${COL_NC} ${COL_YELLOW}master${COL_NC}
|
||||
or ${COL_GREEN}pihole checkout${COL_NC} ${COL_PURPLE}ftl ${COL_CYAN}development${COL_NC}
|
||||
|
||||
Shortcuts:
|
||||
${COL_YELLOW}master${COL_NC} Update all subsystems to the latest stable release
|
||||
${COL_YELLOW}dev${COL_NC} Update all subsystems to the latest development release
|
||||
|
||||
Individual components:
|
||||
${COL_PURPLE}core${COL_NC} ${COL_CYAN}branch${COL_NC} Change the branch of Pi-hole's core subsystem
|
||||
${COL_PURPLE}web${COL_NC} ${COL_CYAN}branch${COL_NC} Change the branch of the web interface subsystem
|
||||
${COL_PURPLE}ftl${COL_NC} ${COL_CYAN}branch${COL_NC} Change the branch of Pi-hole's FTL subsystem"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#shellcheck source=./advanced/Scripts/piholeCheckout.sh
|
||||
source "${PI_HOLE_SCRIPT_DIR}"/piholeCheckout.sh
|
||||
shift
|
||||
checkout "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
tricorderFunc() {
|
||||
local tricorder_token
|
||||
if [[ ! -p "/dev/stdin" ]]; then
|
||||
echo -e " ${INFO} Please do not call Tricorder directly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tricorder_token=$(curl --silent --fail --show-error --upload-file "-" https://tricorder.pi-hole.net/upload < /dev/stdin 2>&1)
|
||||
if [[ "${tricorder_token}" != "https://tricorder.pi-hole.net/"* ]]; then
|
||||
echo -e "${CROSS} uploading failed, contact Pi-hole support for assistance."
|
||||
# Log curl error (if available)
|
||||
if [ -n "${tricorder_token}" ]; then
|
||||
echo -e "${INFO} Error message: ${COL_RED}${tricorder_token}${COL_NC}\\n"
|
||||
tricorder_token=""
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
echo "Upload successful, your token is: ${COL_GREEN}${tricorder_token}${COL_NC}"
|
||||
exit 0
|
||||
}
|
||||
|
||||
updateCheckFunc() {
|
||||
"${PI_HOLE_SCRIPT_DIR}"/updatecheck.sh "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
unsupportedFunc(){
|
||||
echo "Function not supported in Docker images"
|
||||
exit 0
|
||||
}
|
||||
|
||||
helpFunc() {
|
||||
echo "Usage: pihole [options]
|
||||
Example: 'pihole allow -h'
|
||||
Add '-h' after specific commands for more information on usage
|
||||
|
||||
Domain Options:
|
||||
allow, allowlist Allow domain(s)
|
||||
deny, denylist Deny domain(s)
|
||||
--regex, regex Regex deny domains(s)
|
||||
--allow-regex Regex allow domains(s)
|
||||
--wild, wildcard Wildcard deny domain(s)
|
||||
--allow-wild Wildcard allow domain(s)
|
||||
Add '-h' for more info on allow/deny usage
|
||||
|
||||
Debugging Options:
|
||||
-d, debug Start a debugging session
|
||||
Add '-c' or '--check-database' to include a Pi-hole database integrity check
|
||||
Add '-a' to automatically upload the log to tricorder.pi-hole.net
|
||||
-f, flush Flush the Pi-hole log
|
||||
-r, repair Repair Pi-hole subsystems
|
||||
-t, tail [arg] View the live output of the Pi-hole log.
|
||||
Add an optional argument to filter the log
|
||||
(regular expressions are supported)
|
||||
api <endpoint> Query the Pi-hole API at <endpoint>
|
||||
Precede <endpoint> with 'verbose' option to show authentication and status messages
|
||||
|
||||
|
||||
Options:
|
||||
setpassword [pwd] Set the password for the web interface
|
||||
Without optional argument, password is read interactively.
|
||||
When specifying a password directly, enclose it in single quotes.
|
||||
-g, updateGravity Update the list of ad-serving domains
|
||||
-h, --help, help Show this help dialog
|
||||
-l, logging Specify whether the Pi-hole log should be used
|
||||
Add '-h' for more info on logging usage
|
||||
-q, query Query the adlists for a specified domain
|
||||
Add '-h' for more info on query usage
|
||||
-up, updatePihole Update Pi-hole subsystems
|
||||
Add '--check-only' to exit script before update is performed.
|
||||
-v, version Show installed versions of Pi-hole, Web Interface & FTL
|
||||
uninstall Uninstall Pi-hole from your system
|
||||
status Display the running status of Pi-hole subsystems
|
||||
enable Enable Pi-hole subsystems
|
||||
Add '-h' for more info on enable usage
|
||||
disable Disable Pi-hole subsystems
|
||||
Add '-h' for more info on disable usage
|
||||
reloaddns Update the lists and flush the cache without restarting the DNS server
|
||||
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";
|
||||
exit 0
|
||||
}
|
||||
|
||||
if [[ $# = 0 ]]; then
|
||||
helpFunc
|
||||
fi
|
||||
|
||||
# functions that do not require sudo power
|
||||
need_root=1
|
||||
case "${1}" in
|
||||
"-h" | "help" | "--help" ) helpFunc;;
|
||||
"-v" | "version" ) versionFunc;;
|
||||
"-c" | "chronometer" ) chronometerFunc "$@";;
|
||||
"-q" | "query" ) queryFunc "$@";;
|
||||
"status" ) statusFunc "$2";;
|
||||
"tricorder" ) tricorderFunc;;
|
||||
|
||||
# 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;;
|
||||
* ) helpFunc;;
|
||||
esac
|
||||
|
||||
# In the case of alpine running in a container, the USER variable appears to be blank
|
||||
# which prevents the next trap from working correctly. Set it by running whoami
|
||||
if [[ -z ${USER} ]]; then
|
||||
USER=$(whoami)
|
||||
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:"
|
||||
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;;
|
||||
"-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 "$@";;
|
||||
"-t" | "tail" ) tailFunc "$2";;
|
||||
"api" ) shift; apiFunc "$@";;
|
||||
* ) helpFunc;;
|
||||
esac
|
||||
189
vps-healthcheck.sh
Executable file
189
vps-healthcheck.sh
Executable file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
# Healthcheck v2 (modular) – Core + optionale Module (z. B. Mailcow)
|
||||
|
||||
# --- Helpers / Debug ---
|
||||
DEBUG="${DEBUG:-0}"
|
||||
NO_PUSH="${NO_PUSH:-0}"
|
||||
log() { [ "$DEBUG" = "1" ] && echo "[dbg] $*" >&2; }
|
||||
|
||||
alerts=()
|
||||
add_alert(){ alerts+=("$1"); }
|
||||
|
||||
# --- Defaults, falls nicht via Env gesetzt ---
|
||||
KUMA_BASE_URL="${KUMA_BASE_URL:-https://status.example/api/push}"
|
||||
KUMA_PUSH_ID="${KUMA_PUSH_ID:-}"
|
||||
|
||||
LOAD_PER_CORE_MAX="${LOAD_PER_CORE_MAX:-1.50}"
|
||||
MEM_USED_MAX="${MEM_USED_MAX:-90}"
|
||||
DISK_USED_MAX="${DISK_USED_MAX:-85}"
|
||||
INODE_USED_MAX="${INODE_USED_MAX:-90}"
|
||||
MOUNTS=(${MOUNTS:-/ /var /opt /var/lib/docker})
|
||||
|
||||
SWAP_USED_MAX="${SWAP_USED_MAX:-60}"
|
||||
SWAP_SI_MIN="${SWAP_SI_MIN:-50}"
|
||||
SWAP_SO_MIN="${SWAP_SO_MIN:-50}"
|
||||
MEM_PRESSURE_FOR_SWAP_ALERT="${MEM_PRESSURE_FOR_SWAP_ALERT:-95}"
|
||||
|
||||
# Mailcow-Module
|
||||
ENABLE_MAILCOW="${ENABLE_MAILCOW:-0}"
|
||||
COMPOSE_PROJECT_LABEL="${COMPOSE_PROJECT_LABEL:-mailcowdockerized}"
|
||||
MAILCOW_POSTFIX="${MAILCOW_POSTFIX:-postfix-mailcow}"
|
||||
QUEUE_MAX="${QUEUE_MAX:-100}"
|
||||
|
||||
# --- Echtes Netz-Ping (ICMP/TCP) ---
|
||||
PING_MODE="${PING_MODE:-icmp}" # icmp | tcp | off
|
||||
PING_TARGET="${PING_TARGET:-$(echo "${KUMA_BASE_URL}" | awk -F/ '{print $3}')}"
|
||||
PING_COUNT="${PING_COUNT:-3}"
|
||||
PING_TIMEOUT="${PING_TIMEOUT:-1}" # Sek. pro Reply
|
||||
PING_DEADLINE="${PING_DEADLINE:-3}" # Sek. Gesamt
|
||||
PING_FAMILY="${PING_FAMILY:-auto}" # 4 | 6 | auto
|
||||
PING_TCP_URL="${PING_TCP_URL:-https://$(echo "${KUMA_BASE_URL}" | awk -F/ '{print $3}')/}"
|
||||
PING_TCP_TIMEOUT="${PING_TCP_TIMEOUT:-2}"
|
||||
|
||||
# --- Sanitizer für Zahlen aus Env (entfernt Inline-Kommentare/Einheiten) ---
|
||||
strip_comment() { echo "${1%%#*}"; }
|
||||
num_int() { strip_comment "$1" | tr -cd '0-9'; }
|
||||
num_float() { strip_comment "$1" | tr -cd '0-9.'; }
|
||||
|
||||
LOAD_PER_CORE_MAX=$(num_float "${LOAD_PER_CORE_MAX:-1.50}")
|
||||
MEM_USED_MAX=$(num_int "${MEM_USED_MAX:-90}")
|
||||
DISK_USED_MAX=$(num_int "${DISK_USED_MAX:-85}")
|
||||
INODE_USED_MAX=$(num_int "${INODE_USED_MAX:-90}")
|
||||
|
||||
SWAP_USED_MAX=$(num_int "${SWAP_USED_MAX:-60}")
|
||||
SWAP_SI_MIN=$(num_int "${SWAP_SI_MIN:-50}")
|
||||
SWAP_SO_MIN=$(num_int "${SWAP_SO_MIN:-50}")
|
||||
MEM_PRESSURE_FOR_SWAP_ALERT=$(num_int "${MEM_PRESSURE_FOR_SWAP_ALERT:-95}")
|
||||
|
||||
# --- Sanity ---
|
||||
if [ -z "${KUMA_PUSH_ID}" ]; then
|
||||
echo "ERROR: KUMA_PUSH_ID ist leer. Bitte in /etc/vps-healthcheck.env setzen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Mess-Funktion: echter Netz-Ping in ms ---
|
||||
measure_ping() {
|
||||
local ms=""
|
||||
if [ "${PING_MODE}" = "icmp" ] && command -v ping >/dev/null 2>&1 && [ -n "${PING_TARGET}" ]; then
|
||||
local fam=""
|
||||
[ "$PING_FAMILY" = "4" ] && fam="-4"
|
||||
[ "$PING_FAMILY" = "6" ] && fam="-6"
|
||||
local out
|
||||
out=$(LANG=C ping -n $fam -c "$PING_COUNT" -W "$PING_TIMEOUT" -w "$PING_DEADLINE" "$PING_TARGET" 2>&1 || true)
|
||||
# iputils: rtt min/avg/max/mdev = a/b/c/d ms -> avg
|
||||
ms=$(echo "$out" | sed -n 's/.*= \([0-9.]\+\)\/\([0-9.]\+\)\/\([0-9.]\+\)\/\([0-9.]\+\) .*$/\2/p' | tail -n1)
|
||||
# busybox: round-trip min/avg/max = a/b/c ms -> avg
|
||||
[ -z "$ms" ] && ms=$(echo "$out" | sed -n 's/.*= \([0-9.]\+\)\/\([0-9.]\+\)\/\([0-9.]\+\) .*$/\2/p' | tail -n1)
|
||||
fi
|
||||
|
||||
# TCP-Fallback: TLS-Handshake- oder Connect-Zeit (Sekunden -> ms)
|
||||
if [ -z "$ms" ] && command -v curl >/dev/null 2>&1; then
|
||||
local sec
|
||||
if echo "$PING_TCP_URL" | grep -q '^https://'; then
|
||||
sec=$(curl -sS -o /dev/null --max-time "$PING_TCP_TIMEOUT" -w "%{time_appconnect}" "$PING_TCP_URL" || true)
|
||||
else
|
||||
sec=$(curl -sS -o /dev/null --max-time "$PING_TCP_TIMEOUT" -w "%{time_connect}" "$PING_TCP_URL" || true)
|
||||
fi
|
||||
if echo "$sec" | grep -Eq '^[0-9.]+$'; then
|
||||
ms=$(awk -v s="$sec" 'BEGIN{printf "%.0f", s*1000}')
|
||||
fi
|
||||
fi
|
||||
|
||||
if echo "$ms" | grep -Eq '^[0-9.]+$'; then
|
||||
awk -v a="$ms" 'BEGIN{printf "%.0f", a}'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Push an Kuma (mit optionalem ping=) ---
|
||||
push_to_kuma() {
|
||||
local status="$1" msg="$2" ping_ms="$3"
|
||||
local args=( -fsS --retry 2 --max-time 8 --get "${KUMA_BASE_URL}/${KUMA_PUSH_ID}"
|
||||
--data-urlencode "status=${status}"
|
||||
--data-urlencode "msg=${msg}" )
|
||||
[ -n "$ping_ms" ] && args+=( --data-urlencode "ping=${ping_ms}" )
|
||||
if [ "$NO_PUSH" = "1" ]; then
|
||||
echo "[dry-run] would push: status=$status ping=${ping_ms:-} msg=$msg"
|
||||
return 0
|
||||
fi
|
||||
curl "${args[@]}" >/dev/null || true
|
||||
}
|
||||
|
||||
# --- CPU-Load pro Core ---
|
||||
cores=$(nproc 2>/dev/null || echo 1)
|
||||
load1=$(awk '{print $1}' /proc/loadavg)
|
||||
load_per_core=$(awk -v l="$load1" -v c="$cores" 'BEGIN{printf "%.2f", (c>0?l/c:l)}')
|
||||
awk -v a="$load_per_core" -v m="$LOAD_PER_CORE_MAX" 'BEGIN{exit (a>m)?0:1}' && \
|
||||
add_alert "Hohe CPU-Last: ${load1} (=${load_per_core}/Core, Limit ${LOAD_PER_CORE_MAX}/Core)"
|
||||
|
||||
# --- RAM-Nutzung (MemAvailable) ---
|
||||
mem_used_pct=$(awk '
|
||||
/MemTotal:/ {t=$2}
|
||||
/MemAvailable:/ {a=$2}
|
||||
END { if(t>0){printf "%.0f", (1- a/t)*100} else {print 0} }
|
||||
' /proc/meminfo)
|
||||
[ "${mem_used_pct:-0}" -gt "$MEM_USED_MAX" ] && add_alert "RAM hoch: ${mem_used_pct}% (Limit ${MEM_USED_MAX}%)"
|
||||
|
||||
# --- Swap: Nutzung + Aktivität (pages/s) ---
|
||||
swap_used_pct=$(awk '
|
||||
/SwapTotal:/ {t=$2}
|
||||
/SwapFree:/ {f=$2}
|
||||
END { if(t>0) printf "%.0f", (1- f/t)*100; else print 0 }
|
||||
' /proc/meminfo)
|
||||
|
||||
psin0=$(awk '/pswpin/ {print $2}' /proc/vmstat); psout0=$(awk '/pswpout/ {print $2}' /proc/vmstat)
|
||||
sleep 1
|
||||
psin1=$(awk '/pswpin/ {print $2}' /proc/vmstat); psout1=$(awk '/pswpout/ {print $2}' /proc/vmstat)
|
||||
si=$((psin1-psin0)); so=$((psout1-psout0))
|
||||
|
||||
if [ "$swap_used_pct" -gt "$SWAP_USED_MAX" ] && { [ "$si" -gt "$SWAP_SI_MIN" ] || [ "$so" -gt "$SWAP_SO_MIN" ] || [ "${mem_used_pct:-0}" -gt "$MEM_PRESSURE_FOR_SWAP_ALERT" ]; }; then
|
||||
add_alert "Swap hoch: ${swap_used_pct}% (si=${si}/s so=${so}/s)"
|
||||
fi
|
||||
|
||||
# --- IO-wait (zweites 1s Sample) ---
|
||||
read u1 n1 s1 i1 w1 _ < <(awk '/^cpu /{print $2,$3,$4,$5,$6}' /proc/stat)
|
||||
sleep 1
|
||||
read u2 n2 s2 i2 w2 _ < <(awk '/^cpu /{print $2,$3,$4,$5,$6}' /proc/stat)
|
||||
total=$(( (u2-u1)+(n2-n1)+(s2-s1)+(i2-i1)+(w2-w1) ))
|
||||
iow=$(( w2 - w1 ))
|
||||
iow_pct=$(awk -v i="$iow" -v t="$total" 'BEGIN{ if(t>0) printf "%.0f", (i*100)/t; else print 0 }')
|
||||
[ "${iow_pct:-0}" -gt 25 ] && add_alert "IO-Wait hoch: ${iow_pct}% (Limit 25%)"
|
||||
|
||||
# --- Disks: Nutzung + Inodes ---
|
||||
for m in "${MOUNTS[@]}"; do
|
||||
if mountpoint -q "$m"; then
|
||||
used=$(df -P "$m" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')
|
||||
[ -n "$used" ] && [ "$used" -gt "$DISK_USED_MAX" ] && add_alert "Disk fast voll: $m = ${used}% (Limit ${DISK_USED_MAX}%)"
|
||||
iused=$(df -Pi "$m" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')
|
||||
[ -n "$iused" ] && [ "$iused" -gt "$INODE_USED_MAX" ] && add_alert "Inodes knapp: $m = ${iused}% (Limit ${INODE_USED_MAX}%)"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Optionale Module laden ---
|
||||
MODULE_DIR="/usr/local/lib/vps-healthcheck/modules"
|
||||
|
||||
# Mailcow (optional)
|
||||
if [ "${ENABLE_MAILCOW:-0}" = "1" ] && [ -r "${MODULE_DIR}/mailcow.sh" ]; then
|
||||
log "loading module: mailcow"
|
||||
source "${MODULE_DIR}/mailcow.sh"
|
||||
fi
|
||||
|
||||
# Raspi (optional)
|
||||
if [ "${ENABLE_RASPI:-0}" = "1" ] && [ -r "${MODULE_DIR}/raspi.sh" ]; then
|
||||
log "loading module: raspi"
|
||||
source "${MODULE_DIR}/raspi.sh"
|
||||
fi
|
||||
|
||||
# --- Ergebnis pushen (mit echtem Netz-Ping) ---
|
||||
NET_PING_MS="$(measure_ping)"
|
||||
[ "$DEBUG" = "1" ] && echo "[dbg] ping(${PING_MODE}) -> ${NET_PING_MS} ms @ ${PING_TARGET:-$PING_TCP_URL}" >&2
|
||||
|
||||
if [ "${#alerts[@]}" -gt 0 ]; then
|
||||
msg="⚠ $(hostname) – $(printf '%s; ' "${alerts[@]}")"
|
||||
push_to_kuma "down" "${msg:0:900}" "$NET_PING_MS"
|
||||
echo -e "$msg"
|
||||
else
|
||||
push_to_kuma "up" "OK ($(hostname))" "$NET_PING_MS"
|
||||
echo "OK"
|
||||
fi
|
||||
Reference in New Issue
Block a user