Backup automatico script del 2026-07-12 07:00
This commit is contained in:
@@ -54,4 +54,4 @@ else
|
||||
fi
|
||||
|
||||
# 5. Pulizia locale
|
||||
rm "$TEMP_FILE"
|
||||
sudo rm -f "$TEMP_FILE"
|
||||
|
||||
@@ -5,11 +5,20 @@
|
||||
# Controlla solo che il Pi-2 sia vivo.
|
||||
# ================================================
|
||||
|
||||
# --- CONFIGURAZIONE ---
|
||||
# 👇👇 INSERISCI I TUOI DATI VERI QUI 👇👇
|
||||
BOT_TOKEN="8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
|
||||
CHAT_ID="64463169"
|
||||
# 👆👆 FINE MODIFICHE 👆👆
|
||||
ENV_FILE="/home/daniely/.config/watchdog_pi2.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Errore: file credenziali non trovato ($ENV_FILE)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
|
||||
if [ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ]; then
|
||||
echo "Errore: BOT_TOKEN o CHAT_ID mancanti in $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Il bersaglio da controllare (Pi-2 Backup)
|
||||
TARGET_IP="192.168.128.81"
|
||||
|
||||
Executable
+454
@@ -0,0 +1,454 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Manutenzione settimanale Raspberry Pi
|
||||
# Aggiorna OS, Pi-hole, EEPROM; verifica Docker/pip; report + Telegram.
|
||||
# Conclude sempre con reboot host (salvo --no-reboot o REBOOT_ON_SUCCESS=false).
|
||||
# Eseguire come root (cron root).
|
||||
# ==============================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CONF_FILE="/etc/weekly-maintenance.conf"
|
||||
LOG_FILE="/var/log/weekly-maintenance.log"
|
||||
REPORT_FILE="/var/log/weekly-maintenance-report.txt"
|
||||
TOKEN_FILE="/etc/telegram_dpc_bot_token"
|
||||
CHAT_ID="64463169"
|
||||
PIHOLE_BIN="/usr/local/bin/pihole"
|
||||
REBOOT_DELAY_MIN=2
|
||||
WATCHTOWER_IMAGE="nickfedor/watchtower:latest"
|
||||
WATCHTOWER_TIMEOUT=900
|
||||
|
||||
HOST_LABEL="$(hostname -s)"
|
||||
REBOOT_ON_SUCCESS=true
|
||||
TELEGRAM_ENABLED=true
|
||||
CHECK_PIP3=false
|
||||
DOCKER_IGNORE_IMAGES=()
|
||||
DOCKER_PINNED_IMAGES=()
|
||||
|
||||
declare -a ERRORS=()
|
||||
declare -a WARNINGS=()
|
||||
declare -a NOTES=()
|
||||
REBOOT_RECOMMENDED=false
|
||||
APT_CHANGED=false
|
||||
DOCKER_UPDATED=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-reboot) REBOOT_ON_SUCCESS=false ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Configurazione host (override via /etc/weekly-maintenance.conf) ---
|
||||
case "$(hostname -s)" in
|
||||
pi1)
|
||||
HOST_LABEL="Pi-1 (Master)"
|
||||
DOCKER_IGNORE_IMAGES=("turni-app:live-latest")
|
||||
;;
|
||||
pi2)
|
||||
HOST_LABEL="Pi-2 (Backup)"
|
||||
CHECK_PIP3=true
|
||||
DOCKER_IGNORE_IMAGES=("irrigazione:latest" "turni-app:beta-latest" "turni-app:alpha-latest")
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -f "$CONF_FILE" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONF_FILE"
|
||||
fi
|
||||
|
||||
# --- Utility ---
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
append_report() {
|
||||
printf '%s\n' "$*" >> "$REPORT_FILE"
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local title="$1"
|
||||
shift
|
||||
local output
|
||||
local rc=0
|
||||
|
||||
log "▶ $title"
|
||||
append_report ""
|
||||
append_report "=== $title ==="
|
||||
|
||||
output="$("$@" 2>&1)" || rc=$?
|
||||
if [[ -n "$output" ]]; then
|
||||
append_report "$output"
|
||||
log "$output"
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
NOTES+=("OK: $title")
|
||||
log "✓ $title completato"
|
||||
else
|
||||
ERRORS+=("$title (exit $rc)")
|
||||
log "✗ $title fallito (exit $rc)"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
load_telegram_token() {
|
||||
if [[ ! -f "$TOKEN_FILE" ]]; then
|
||||
WARNINGS+=("Token Telegram assente ($TOKEN_FILE): notifiche disabilitate")
|
||||
TELEGRAM_ENABLED=false
|
||||
return 1
|
||||
fi
|
||||
BOT_TOKEN=$(tr -d '\n\r' < "$TOKEN_FILE")
|
||||
if [[ -z "$BOT_TOKEN" ]]; then
|
||||
WARNINGS+=("Token Telegram vuoto: notifiche disabilitate")
|
||||
TELEGRAM_ENABLED=false
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="$1"
|
||||
[[ "$TELEGRAM_ENABLED" == true ]] || return 0
|
||||
curl -s --max-time 15 -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CHAT_ID}" \
|
||||
--data-urlencode "text=${msg}" \
|
||||
-d "parse_mode=Markdown" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
send_telegram_document() {
|
||||
[[ "$TELEGRAM_ENABLED" == true ]] || return 0
|
||||
[[ -f "$REPORT_FILE" ]] || return 0
|
||||
curl -s --max-time 30 -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendDocument" \
|
||||
-F "chat_id=${CHAT_ID}" \
|
||||
-F "document=@${REPORT_FILE}" \
|
||||
-F "caption=Report completo ${HOST_LABEL}" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
echo "Eseguire come root (es. cron root)." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
image_is_ignored() {
|
||||
local image="$1"
|
||||
local ignored
|
||||
for ignored in "${DOCKER_IGNORE_IMAGES[@]}"; do
|
||||
[[ "$image" == "$ignored" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_watchtower_labels() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
local name image
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
name=${line%%|*}
|
||||
image=${line#*|}
|
||||
image_is_ignored "$image" || continue
|
||||
docker update --label-add com.centurylinklabs.watchtower.enable=false "$name" >/dev/null 2>&1 || true
|
||||
done < <(docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null || true)
|
||||
}
|
||||
|
||||
run_watchtower() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
|
||||
ensure_watchtower_labels
|
||||
|
||||
log "▶ Watchtower (run-once, container aggiornabili da registry)"
|
||||
append_report ""
|
||||
append_report "=== Watchtower run-once ==="
|
||||
|
||||
local output rc=0
|
||||
output=$(docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_CLEANUP=true \
|
||||
-e WATCHTOWER_ROLLING_RESTART=true \
|
||||
-e WATCHTOWER_TIMEOUT="${WATCHTOWER_TIMEOUT}" \
|
||||
-e TZ=Europe/Rome \
|
||||
"$WATCHTOWER_IMAGE" \
|
||||
--run-once 2>&1) || rc=$?
|
||||
|
||||
if [[ -n "$output" ]]; then
|
||||
append_report "$output"
|
||||
printf '%s\n' "$output" | tail -30 | while IFS= read -r line; do log "$line"; done
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
NOTES+=("Watchtower run-once OK")
|
||||
if grep -q "updated=[1-9]" <<< "$output"; then
|
||||
DOCKER_UPDATED=true
|
||||
REBOOT_RECOMMENDED=true
|
||||
NOTES+=("Watchtower: container aggiornati")
|
||||
fi
|
||||
else
|
||||
ERRORS+=("Watchtower run-once (exit $rc)")
|
||||
fi
|
||||
}
|
||||
|
||||
audit_apt() {
|
||||
local holds
|
||||
holds=$(apt-mark showhold 2>/dev/null || true)
|
||||
if [[ -n "$holds" ]]; then
|
||||
WARNINGS+=("Pacchetti apt bloccati (hold): ${holds//$'\n'/, }")
|
||||
append_report "$holds"
|
||||
fi
|
||||
|
||||
local upgradable
|
||||
upgradable=$(apt list --upgradable 2>/dev/null | tail -n +2 || true)
|
||||
if [[ -n "$upgradable" ]]; then
|
||||
WARNINGS+=("Restano pacchetti non aggiornati dopo full-upgrade")
|
||||
append_report "$upgradable"
|
||||
fi
|
||||
}
|
||||
|
||||
audit_pihole_versions() {
|
||||
local out
|
||||
out=$("$PIHOLE_BIN" -v 2>&1 || true)
|
||||
append_report "$out"
|
||||
if grep -qiE 'version is .*\(Latest: .*\)' <<< "$out"; then
|
||||
while IFS= read -r line; do
|
||||
local current latest
|
||||
current=$(sed -n 's/.*Version is \([^ ]*\).*/\1/p' <<< "$line")
|
||||
latest=$(sed -n 's/.*Latest: \([^)]*\).*/\1/p' <<< "$line")
|
||||
if [[ -n "$current" && -n "$latest" && "$current" != "$latest" && "$current" != "N/A" ]]; then
|
||||
WARNINGS+=("Pi-hole non allineato: $line")
|
||||
fi
|
||||
done <<< "$out"
|
||||
fi
|
||||
}
|
||||
|
||||
audit_eeprom() {
|
||||
command -v rpi-eeprom-update >/dev/null 2>&1 || return 0
|
||||
local out
|
||||
out=$(rpi-eeprom-update 2>&1 || true)
|
||||
append_report "$out"
|
||||
if grep -qiE 'UPDATE REQUIRED|update available|NEW EEPROM' <<< "$out"; then
|
||||
log "EEPROM: aggiornamento disponibile, applico..."
|
||||
append_report ""
|
||||
append_report "=== rpi-eeprom-update -a ==="
|
||||
if rpi-eeprom-update -a >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("EEPROM aggiornato")
|
||||
REBOOT_RECOMMENDED=true
|
||||
else
|
||||
ERRORS+=("rpi-eeprom-update -a")
|
||||
fi
|
||||
else
|
||||
NOTES+=("EEPROM: aggiornato")
|
||||
fi
|
||||
}
|
||||
|
||||
audit_docker() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
|
||||
append_report ""
|
||||
append_report "=== Docker audit ==="
|
||||
|
||||
local exited
|
||||
exited=$(docker ps -a --filter "status=exited" --format '{{.Names}} ({{.Image}})' 2>/dev/null || true)
|
||||
if [[ -n "$exited" ]]; then
|
||||
WARNINGS+=("Container Docker fermati (Exited): ${exited//$'\n'/, }")
|
||||
append_report "Exited:"
|
||||
append_report "$exited"
|
||||
fi
|
||||
|
||||
local name image
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
name=${line%%|*}
|
||||
image=${line#*|}
|
||||
|
||||
image_is_ignored "$image" && continue
|
||||
|
||||
if [[ "$image" =~ ^[0-9a-f]{12}$ ]]; then
|
||||
WARNINGS+=("Container '$name' usa immagine per ID ($image): preferire un tag versionato")
|
||||
fi
|
||||
|
||||
local pinned
|
||||
for pinned in "${DOCKER_PINNED_IMAGES[@]}"; do
|
||||
if [[ "$image" == "$pinned" ]]; then
|
||||
WARNINGS+=("Container '$name' usa tag bloccato ($pinned): Watchtower non passerà a :latest")
|
||||
fi
|
||||
done
|
||||
done < <(docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null || true)
|
||||
}
|
||||
|
||||
audit_pip3() {
|
||||
[[ "$CHECK_PIP3" == true ]] || return 0
|
||||
command -v pip3 >/dev/null 2>&1 || return 0
|
||||
|
||||
append_report ""
|
||||
append_report "=== pip3 outdated (sistema) ==="
|
||||
local outdated count
|
||||
outdated=$(pip3 list --outdated --format=columns 2>/dev/null | tail -n +3 || true)
|
||||
if [[ -n "$outdated" ]]; then
|
||||
count=$(wc -l <<< "$outdated")
|
||||
append_report "$outdated"
|
||||
WARNINGS+=("pip3: $count pacchetti Python di sistema non aggiornati (aggiornamento manuale/consigliato in venv)")
|
||||
else
|
||||
NOTES+=("pip3: tutti aggiornati")
|
||||
fi
|
||||
}
|
||||
|
||||
build_telegram_summary() {
|
||||
local icon status
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
icon="🚨"
|
||||
status="ERRORI"
|
||||
elif ((${#WARNINGS[@]} > 0)); then
|
||||
icon="⚠️"
|
||||
status="WARNINGS"
|
||||
else
|
||||
icon="✅"
|
||||
status="OK"
|
||||
fi
|
||||
|
||||
local msg="${icon} *Manutenzione settimanale ${HOST_LABEL}*
|
||||
Stato: *${status}*
|
||||
Data: $(date '+%d/%m/%Y %H:%M')"
|
||||
|
||||
if [[ "$APT_CHANGED" == true ]]; then
|
||||
msg+="
|
||||
📦 apt: pacchetti aggiornati"
|
||||
fi
|
||||
if [[ "$DOCKER_UPDATED" == true ]]; then
|
||||
msg+="
|
||||
🐳 Docker: container aggiornati"
|
||||
fi
|
||||
if [[ "$REBOOT_ON_SUCCESS" == true ]]; then
|
||||
msg+="
|
||||
🔄 Reboot host programmato tra ${REBOOT_DELAY_MIN} min (fine manutenzione)"
|
||||
fi
|
||||
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
msg+="
|
||||
*Errori (${#ERRORS[@]}):*"
|
||||
local e
|
||||
for e in "${ERRORS[@]}"; do
|
||||
msg+="
|
||||
• ${e}"
|
||||
done
|
||||
fi
|
||||
|
||||
if ((${#WARNINGS[@]} > 0)); then
|
||||
msg+="
|
||||
*Avvisi (${#WARNINGS[@]}):*"
|
||||
local w
|
||||
local n=0
|
||||
for w in "${WARNINGS[@]}"; do
|
||||
((n++)) || true
|
||||
[[ $n -le 8 ]] && msg+="
|
||||
• ${w}"
|
||||
done
|
||||
if ((${#WARNINGS[@]} > 8)); then
|
||||
msg+="
|
||||
• … +$((${#WARNINGS[@]} - 8)) avvisi (vedi report)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((${#ERRORS[@]} == 0 && ${#WARNINGS[@]} == 0)); then
|
||||
msg+="
|
||||
Tutto aggiornato. Log: \`${LOG_FILE}\`"
|
||||
else
|
||||
msg+="
|
||||
Report completo allegato o in \`${REPORT_FILE}\`"
|
||||
fi
|
||||
|
||||
send_telegram "$msg"
|
||||
if ((${#ERRORS[@]} > 0 || ${#WARNINGS[@]} > 0)); then
|
||||
send_telegram_document
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
require_root
|
||||
|
||||
: > "$REPORT_FILE"
|
||||
log "========== Inizio manutenzione settimanale ($HOST_LABEL) =========="
|
||||
append_report "Manutenzione settimanale - $HOST_LABEL"
|
||||
append_report "Avviata: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
append_report "Host: $(hostname -f) ($(hostname -I | awk '{print $1}'))"
|
||||
|
||||
load_telegram_token || true
|
||||
|
||||
# 1. Aggiornamento pacchetti sistema
|
||||
if apt update >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("apt update OK")
|
||||
log "✓ apt update"
|
||||
else
|
||||
ERRORS+=("apt update")
|
||||
log "✗ apt update fallito"
|
||||
fi
|
||||
|
||||
BEFORE_UPGRADES=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l || echo 0)
|
||||
if apt full-upgrade -y >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("apt full-upgrade OK")
|
||||
log "✓ apt full-upgrade"
|
||||
AFTER_UPGRADES=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l || echo 0)
|
||||
if [[ "$BEFORE_UPGRADES" -gt "$AFTER_UPGRADES" ]] || [[ "$BEFORE_UPGRADES" -gt 0 ]]; then
|
||||
APT_CHANGED=true
|
||||
REBOOT_RECOMMENDED=true
|
||||
fi
|
||||
else
|
||||
ERRORS+=("apt full-upgrade")
|
||||
log "✗ apt full-upgrade fallito"
|
||||
fi
|
||||
|
||||
run_step "apt autoremove" apt autoremove -y
|
||||
audit_apt
|
||||
|
||||
# 2. Pi-hole
|
||||
if [[ -x "$PIHOLE_BIN" ]]; then
|
||||
run_step "pihole -up" "$PIHOLE_BIN" -up
|
||||
audit_pihole_versions
|
||||
else
|
||||
ERRORS+=("pihole non trovato in $PIHOLE_BIN")
|
||||
fi
|
||||
|
||||
# 3. EEPROM firmware
|
||||
audit_eeprom
|
||||
|
||||
# 4. Aggiornamento container Docker (Watchtower run-once, sostituisce il daemon schedulato)
|
||||
run_watchtower
|
||||
|
||||
# 5. Audit residui
|
||||
audit_docker
|
||||
audit_pip3
|
||||
|
||||
append_report ""
|
||||
append_report "=== Riepilogo ==="
|
||||
append_report "Errori: ${#ERRORS[@]}"
|
||||
append_report "Avvisi: ${#WARNINGS[@]}"
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
append_report "Dettaglio errori:"
|
||||
printf '%s\n' "${ERRORS[@]}" >> "$REPORT_FILE"
|
||||
fi
|
||||
if ((${#WARNINGS[@]} > 0)); then
|
||||
append_report "Dettaglio avvisi:"
|
||||
printf '%s\n' "${WARNINGS[@]}" >> "$REPORT_FILE"
|
||||
fi
|
||||
append_report "Fine: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
build_telegram_summary
|
||||
|
||||
# 6. Reboot fisso di conclusione (salvo --no-reboot o REBOOT_ON_SUCCESS=false in conf)
|
||||
if [[ "$REBOOT_ON_SUCCESS" == true ]]; then
|
||||
log "Reboot host programmato tra ${REBOOT_DELAY_MIN} minuti (fine manutenzione settimanale)"
|
||||
append_report ""
|
||||
append_report "=== Reboot ==="
|
||||
append_report "Programmato tra ${REBOOT_DELAY_MIN} minuti"
|
||||
shutdown -r "+${REBOOT_DELAY_MIN}" "Manutenzione settimanale ${HOST_LABEL}" || {
|
||||
ERRORS+=("shutdown -r (reboot programmato)")
|
||||
log "✗ Impossibile programmare il reboot"
|
||||
}
|
||||
else
|
||||
log "Reboot disabilitato (--no-reboot o REBOOT_ON_SUCCESS=false)"
|
||||
append_report ""
|
||||
append_report "=== Reboot ==="
|
||||
append_report "Saltato (disabilitato)"
|
||||
fi
|
||||
|
||||
log "========== Fine manutenzione settimanale =========="
|
||||
exit $(( ${#ERRORS[@]} > 0 ? 1 : 0 ))
|
||||
@@ -44,6 +44,7 @@ TARGETS=(
|
||||
"📶 WiFi Luca (.102)|192.168.128.102"
|
||||
"📶 WiFi Taverna (.103)|192.168.128.103"
|
||||
"📶 WiFi Dado (.104)|192.168.128.104"
|
||||
"📶 WiFi Esterno (.108)|192.168.128.108"
|
||||
|
||||
# CAMERE 📷 (Sottorete .135)
|
||||
"📷 Cam Matrimoniale|192.168.135.2"
|
||||
|
||||
Executable
+454
@@ -0,0 +1,454 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Manutenzione settimanale Raspberry Pi
|
||||
# Aggiorna OS, Pi-hole, EEPROM; verifica Docker/pip; report + Telegram.
|
||||
# Conclude sempre con reboot host (salvo --no-reboot o REBOOT_ON_SUCCESS=false).
|
||||
# Eseguire come root (cron root).
|
||||
# ==============================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CONF_FILE="/etc/weekly-maintenance.conf"
|
||||
LOG_FILE="/var/log/weekly-maintenance.log"
|
||||
REPORT_FILE="/var/log/weekly-maintenance-report.txt"
|
||||
TOKEN_FILE="/etc/telegram_dpc_bot_token"
|
||||
CHAT_ID="64463169"
|
||||
PIHOLE_BIN="/usr/local/bin/pihole"
|
||||
REBOOT_DELAY_MIN=2
|
||||
WATCHTOWER_IMAGE="nickfedor/watchtower:latest"
|
||||
WATCHTOWER_TIMEOUT=900
|
||||
|
||||
HOST_LABEL="$(hostname -s)"
|
||||
REBOOT_ON_SUCCESS=true
|
||||
TELEGRAM_ENABLED=true
|
||||
CHECK_PIP3=false
|
||||
DOCKER_IGNORE_IMAGES=()
|
||||
DOCKER_PINNED_IMAGES=()
|
||||
|
||||
declare -a ERRORS=()
|
||||
declare -a WARNINGS=()
|
||||
declare -a NOTES=()
|
||||
REBOOT_RECOMMENDED=false
|
||||
APT_CHANGED=false
|
||||
DOCKER_UPDATED=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-reboot) REBOOT_ON_SUCCESS=false ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Configurazione host (override via /etc/weekly-maintenance.conf) ---
|
||||
case "$(hostname -s)" in
|
||||
pi1)
|
||||
HOST_LABEL="Pi-1 (Master)"
|
||||
DOCKER_IGNORE_IMAGES=("turni-app:live-latest")
|
||||
;;
|
||||
pi2)
|
||||
HOST_LABEL="Pi-2 (Backup)"
|
||||
CHECK_PIP3=true
|
||||
DOCKER_IGNORE_IMAGES=("irrigazione:latest" "turni-app:beta-latest" "turni-app:alpha-latest")
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -f "$CONF_FILE" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONF_FILE"
|
||||
fi
|
||||
|
||||
# --- Utility ---
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
append_report() {
|
||||
printf '%s\n' "$*" >> "$REPORT_FILE"
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local title="$1"
|
||||
shift
|
||||
local output
|
||||
local rc=0
|
||||
|
||||
log "▶ $title"
|
||||
append_report ""
|
||||
append_report "=== $title ==="
|
||||
|
||||
output="$("$@" 2>&1)" || rc=$?
|
||||
if [[ -n "$output" ]]; then
|
||||
append_report "$output"
|
||||
log "$output"
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
NOTES+=("OK: $title")
|
||||
log "✓ $title completato"
|
||||
else
|
||||
ERRORS+=("$title (exit $rc)")
|
||||
log "✗ $title fallito (exit $rc)"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
load_telegram_token() {
|
||||
if [[ ! -f "$TOKEN_FILE" ]]; then
|
||||
WARNINGS+=("Token Telegram assente ($TOKEN_FILE): notifiche disabilitate")
|
||||
TELEGRAM_ENABLED=false
|
||||
return 1
|
||||
fi
|
||||
BOT_TOKEN=$(tr -d '\n\r' < "$TOKEN_FILE")
|
||||
if [[ -z "$BOT_TOKEN" ]]; then
|
||||
WARNINGS+=("Token Telegram vuoto: notifiche disabilitate")
|
||||
TELEGRAM_ENABLED=false
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="$1"
|
||||
[[ "$TELEGRAM_ENABLED" == true ]] || return 0
|
||||
curl -s --max-time 15 -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CHAT_ID}" \
|
||||
--data-urlencode "text=${msg}" \
|
||||
-d "parse_mode=Markdown" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
send_telegram_document() {
|
||||
[[ "$TELEGRAM_ENABLED" == true ]] || return 0
|
||||
[[ -f "$REPORT_FILE" ]] || return 0
|
||||
curl -s --max-time 30 -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendDocument" \
|
||||
-F "chat_id=${CHAT_ID}" \
|
||||
-F "document=@${REPORT_FILE}" \
|
||||
-F "caption=Report completo ${HOST_LABEL}" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
echo "Eseguire come root (es. cron root)." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
image_is_ignored() {
|
||||
local image="$1"
|
||||
local ignored
|
||||
for ignored in "${DOCKER_IGNORE_IMAGES[@]}"; do
|
||||
[[ "$image" == "$ignored" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_watchtower_labels() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
local name image
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
name=${line%%|*}
|
||||
image=${line#*|}
|
||||
image_is_ignored "$image" || continue
|
||||
docker update --label-add com.centurylinklabs.watchtower.enable=false "$name" >/dev/null 2>&1 || true
|
||||
done < <(docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null || true)
|
||||
}
|
||||
|
||||
run_watchtower() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
|
||||
ensure_watchtower_labels
|
||||
|
||||
log "▶ Watchtower (run-once, container aggiornabili da registry)"
|
||||
append_report ""
|
||||
append_report "=== Watchtower run-once ==="
|
||||
|
||||
local output rc=0
|
||||
output=$(docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_CLEANUP=true \
|
||||
-e WATCHTOWER_ROLLING_RESTART=true \
|
||||
-e WATCHTOWER_TIMEOUT="${WATCHTOWER_TIMEOUT}" \
|
||||
-e TZ=Europe/Rome \
|
||||
"$WATCHTOWER_IMAGE" \
|
||||
--run-once 2>&1) || rc=$?
|
||||
|
||||
if [[ -n "$output" ]]; then
|
||||
append_report "$output"
|
||||
printf '%s\n' "$output" | tail -30 | while IFS= read -r line; do log "$line"; done
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
NOTES+=("Watchtower run-once OK")
|
||||
if grep -q "updated=[1-9]" <<< "$output"; then
|
||||
DOCKER_UPDATED=true
|
||||
REBOOT_RECOMMENDED=true
|
||||
NOTES+=("Watchtower: container aggiornati")
|
||||
fi
|
||||
else
|
||||
ERRORS+=("Watchtower run-once (exit $rc)")
|
||||
fi
|
||||
}
|
||||
|
||||
audit_apt() {
|
||||
local holds
|
||||
holds=$(apt-mark showhold 2>/dev/null || true)
|
||||
if [[ -n "$holds" ]]; then
|
||||
WARNINGS+=("Pacchetti apt bloccati (hold): ${holds//$'\n'/, }")
|
||||
append_report "$holds"
|
||||
fi
|
||||
|
||||
local upgradable
|
||||
upgradable=$(apt list --upgradable 2>/dev/null | tail -n +2 || true)
|
||||
if [[ -n "$upgradable" ]]; then
|
||||
WARNINGS+=("Restano pacchetti non aggiornati dopo full-upgrade")
|
||||
append_report "$upgradable"
|
||||
fi
|
||||
}
|
||||
|
||||
audit_pihole_versions() {
|
||||
local out
|
||||
out=$("$PIHOLE_BIN" -v 2>&1 || true)
|
||||
append_report "$out"
|
||||
if grep -qiE 'version is .*\(Latest: .*\)' <<< "$out"; then
|
||||
while IFS= read -r line; do
|
||||
local current latest
|
||||
current=$(sed -n 's/.*Version is \([^ ]*\).*/\1/p' <<< "$line")
|
||||
latest=$(sed -n 's/.*Latest: \([^)]*\).*/\1/p' <<< "$line")
|
||||
if [[ -n "$current" && -n "$latest" && "$current" != "$latest" && "$current" != "N/A" ]]; then
|
||||
WARNINGS+=("Pi-hole non allineato: $line")
|
||||
fi
|
||||
done <<< "$out"
|
||||
fi
|
||||
}
|
||||
|
||||
audit_eeprom() {
|
||||
command -v rpi-eeprom-update >/dev/null 2>&1 || return 0
|
||||
local out
|
||||
out=$(rpi-eeprom-update 2>&1 || true)
|
||||
append_report "$out"
|
||||
if grep -qiE 'UPDATE REQUIRED|update available|NEW EEPROM' <<< "$out"; then
|
||||
log "EEPROM: aggiornamento disponibile, applico..."
|
||||
append_report ""
|
||||
append_report "=== rpi-eeprom-update -a ==="
|
||||
if rpi-eeprom-update -a >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("EEPROM aggiornato")
|
||||
REBOOT_RECOMMENDED=true
|
||||
else
|
||||
ERRORS+=("rpi-eeprom-update -a")
|
||||
fi
|
||||
else
|
||||
NOTES+=("EEPROM: aggiornato")
|
||||
fi
|
||||
}
|
||||
|
||||
audit_docker() {
|
||||
command -v docker >/dev/null 2>&1 || return 0
|
||||
|
||||
append_report ""
|
||||
append_report "=== Docker audit ==="
|
||||
|
||||
local exited
|
||||
exited=$(docker ps -a --filter "status=exited" --format '{{.Names}} ({{.Image}})' 2>/dev/null || true)
|
||||
if [[ -n "$exited" ]]; then
|
||||
WARNINGS+=("Container Docker fermati (Exited): ${exited//$'\n'/, }")
|
||||
append_report "Exited:"
|
||||
append_report "$exited"
|
||||
fi
|
||||
|
||||
local name image
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
name=${line%%|*}
|
||||
image=${line#*|}
|
||||
|
||||
image_is_ignored "$image" && continue
|
||||
|
||||
if [[ "$image" =~ ^[0-9a-f]{12}$ ]]; then
|
||||
WARNINGS+=("Container '$name' usa immagine per ID ($image): preferire un tag versionato")
|
||||
fi
|
||||
|
||||
local pinned
|
||||
for pinned in "${DOCKER_PINNED_IMAGES[@]}"; do
|
||||
if [[ "$image" == "$pinned" ]]; then
|
||||
WARNINGS+=("Container '$name' usa tag bloccato ($pinned): Watchtower non passerà a :latest")
|
||||
fi
|
||||
done
|
||||
done < <(docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null || true)
|
||||
}
|
||||
|
||||
audit_pip3() {
|
||||
[[ "$CHECK_PIP3" == true ]] || return 0
|
||||
command -v pip3 >/dev/null 2>&1 || return 0
|
||||
|
||||
append_report ""
|
||||
append_report "=== pip3 outdated (sistema) ==="
|
||||
local outdated count
|
||||
outdated=$(pip3 list --outdated --format=columns 2>/dev/null | tail -n +3 || true)
|
||||
if [[ -n "$outdated" ]]; then
|
||||
count=$(wc -l <<< "$outdated")
|
||||
append_report "$outdated"
|
||||
WARNINGS+=("pip3: $count pacchetti Python di sistema non aggiornati (aggiornamento manuale/consigliato in venv)")
|
||||
else
|
||||
NOTES+=("pip3: tutti aggiornati")
|
||||
fi
|
||||
}
|
||||
|
||||
build_telegram_summary() {
|
||||
local icon status
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
icon="🚨"
|
||||
status="ERRORI"
|
||||
elif ((${#WARNINGS[@]} > 0)); then
|
||||
icon="⚠️"
|
||||
status="WARNINGS"
|
||||
else
|
||||
icon="✅"
|
||||
status="OK"
|
||||
fi
|
||||
|
||||
local msg="${icon} *Manutenzione settimanale ${HOST_LABEL}*
|
||||
Stato: *${status}*
|
||||
Data: $(date '+%d/%m/%Y %H:%M')"
|
||||
|
||||
if [[ "$APT_CHANGED" == true ]]; then
|
||||
msg+="
|
||||
📦 apt: pacchetti aggiornati"
|
||||
fi
|
||||
if [[ "$DOCKER_UPDATED" == true ]]; then
|
||||
msg+="
|
||||
🐳 Docker: container aggiornati"
|
||||
fi
|
||||
if [[ "$REBOOT_ON_SUCCESS" == true ]]; then
|
||||
msg+="
|
||||
🔄 Reboot host programmato tra ${REBOOT_DELAY_MIN} min (fine manutenzione)"
|
||||
fi
|
||||
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
msg+="
|
||||
*Errori (${#ERRORS[@]}):*"
|
||||
local e
|
||||
for e in "${ERRORS[@]}"; do
|
||||
msg+="
|
||||
• ${e}"
|
||||
done
|
||||
fi
|
||||
|
||||
if ((${#WARNINGS[@]} > 0)); then
|
||||
msg+="
|
||||
*Avvisi (${#WARNINGS[@]}):*"
|
||||
local w
|
||||
local n=0
|
||||
for w in "${WARNINGS[@]}"; do
|
||||
((n++)) || true
|
||||
[[ $n -le 8 ]] && msg+="
|
||||
• ${w}"
|
||||
done
|
||||
if ((${#WARNINGS[@]} > 8)); then
|
||||
msg+="
|
||||
• … +$((${#WARNINGS[@]} - 8)) avvisi (vedi report)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((${#ERRORS[@]} == 0 && ${#WARNINGS[@]} == 0)); then
|
||||
msg+="
|
||||
Tutto aggiornato. Log: \`${LOG_FILE}\`"
|
||||
else
|
||||
msg+="
|
||||
Report completo allegato o in \`${REPORT_FILE}\`"
|
||||
fi
|
||||
|
||||
send_telegram "$msg"
|
||||
if ((${#ERRORS[@]} > 0 || ${#WARNINGS[@]} > 0)); then
|
||||
send_telegram_document
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
require_root
|
||||
|
||||
: > "$REPORT_FILE"
|
||||
log "========== Inizio manutenzione settimanale ($HOST_LABEL) =========="
|
||||
append_report "Manutenzione settimanale - $HOST_LABEL"
|
||||
append_report "Avviata: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
append_report "Host: $(hostname -f) ($(hostname -I | awk '{print $1}'))"
|
||||
|
||||
load_telegram_token || true
|
||||
|
||||
# 1. Aggiornamento pacchetti sistema
|
||||
if apt update >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("apt update OK")
|
||||
log "✓ apt update"
|
||||
else
|
||||
ERRORS+=("apt update")
|
||||
log "✗ apt update fallito"
|
||||
fi
|
||||
|
||||
BEFORE_UPGRADES=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l || echo 0)
|
||||
if apt full-upgrade -y >> "$REPORT_FILE" 2>&1; then
|
||||
NOTES+=("apt full-upgrade OK")
|
||||
log "✓ apt full-upgrade"
|
||||
AFTER_UPGRADES=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l || echo 0)
|
||||
if [[ "$BEFORE_UPGRADES" -gt "$AFTER_UPGRADES" ]] || [[ "$BEFORE_UPGRADES" -gt 0 ]]; then
|
||||
APT_CHANGED=true
|
||||
REBOOT_RECOMMENDED=true
|
||||
fi
|
||||
else
|
||||
ERRORS+=("apt full-upgrade")
|
||||
log "✗ apt full-upgrade fallito"
|
||||
fi
|
||||
|
||||
run_step "apt autoremove" apt autoremove -y
|
||||
audit_apt
|
||||
|
||||
# 2. Pi-hole
|
||||
if [[ -x "$PIHOLE_BIN" ]]; then
|
||||
run_step "pihole -up" "$PIHOLE_BIN" -up
|
||||
audit_pihole_versions
|
||||
else
|
||||
ERRORS+=("pihole non trovato in $PIHOLE_BIN")
|
||||
fi
|
||||
|
||||
# 3. EEPROM firmware
|
||||
audit_eeprom
|
||||
|
||||
# 4. Aggiornamento container Docker (Watchtower run-once, sostituisce il daemon schedulato)
|
||||
run_watchtower
|
||||
|
||||
# 5. Audit residui
|
||||
audit_docker
|
||||
audit_pip3
|
||||
|
||||
append_report ""
|
||||
append_report "=== Riepilogo ==="
|
||||
append_report "Errori: ${#ERRORS[@]}"
|
||||
append_report "Avvisi: ${#WARNINGS[@]}"
|
||||
if ((${#ERRORS[@]} > 0)); then
|
||||
append_report "Dettaglio errori:"
|
||||
printf '%s\n' "${ERRORS[@]}" >> "$REPORT_FILE"
|
||||
fi
|
||||
if ((${#WARNINGS[@]} > 0)); then
|
||||
append_report "Dettaglio avvisi:"
|
||||
printf '%s\n' "${WARNINGS[@]}" >> "$REPORT_FILE"
|
||||
fi
|
||||
append_report "Fine: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
build_telegram_summary
|
||||
|
||||
# 6. Reboot fisso di conclusione (salvo --no-reboot o REBOOT_ON_SUCCESS=false in conf)
|
||||
if [[ "$REBOOT_ON_SUCCESS" == true ]]; then
|
||||
log "Reboot host programmato tra ${REBOOT_DELAY_MIN} minuti (fine manutenzione settimanale)"
|
||||
append_report ""
|
||||
append_report "=== Reboot ==="
|
||||
append_report "Programmato tra ${REBOOT_DELAY_MIN} minuti"
|
||||
shutdown -r "+${REBOOT_DELAY_MIN}" "Manutenzione settimanale ${HOST_LABEL}" || {
|
||||
ERRORS+=("shutdown -r (reboot programmato)")
|
||||
log "✗ Impossibile programmare il reboot"
|
||||
}
|
||||
else
|
||||
log "Reboot disabilitato (--no-reboot o REBOOT_ON_SUCCESS=false)"
|
||||
append_report ""
|
||||
append_report "=== Reboot ==="
|
||||
append_report "Saltato (disabilitato)"
|
||||
fi
|
||||
|
||||
log "========== Fine manutenzione settimanale =========="
|
||||
exit $(( ${#ERRORS[@]} > 0 ? 1 : 0 ))
|
||||
Reference in New Issue
Block a user