Files
apt-autoupdate-install/auto-updates-TESTING.sh
2025-07-28 05:16:20 +00:00

265 lines
7.9 KiB
Bash

#!/usr/bin/env bash
#
# setup-auto-updates.sh
#
# Universal unattended-upgrades setup for Debian & Ubuntu (VMs, LXCs, mixed fleets).
# - Derives Allowed-Origins from `apt-cache policy` to handle "stable" vs codename cleanly
# - Supports Debian (stable/testing) incl. security pockets; forward-compatible with Trixie
# - Supports Ubuntu (noble, etc.) incl. -updates, -security, and optional ESM pockets
# - Works with classic sources.list and deb822 `.sources` files
# - Validates config with a dry run and enables systemd timers when available
#
# Notes:
# * Backports are intentionally NOT auto-updated.
# * On minimal containers without systemd, the APT::Periodic cron path is used.
set -Eeuo pipefail
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
REBOOT_TIME="${REBOOT_TIME:-04:00}"
OS=""
CODENAME=""
DETECTED_ORIGINS=() # e.g., ("Debian:bookworm" "Debian:bookworm-security" ...)
POLICY_PAIRS=() # raw pairs as "Origin:Archive" extracted from apt-cache
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] Run as root (sudo)." >&2
exit 1
fi
}
detect_os() {
if [[ ! -f /etc/os-release ]]; then
echo "[ERROR] /etc/os-release not found." >&2
exit 1
fi
# shellcheck disable=SC1091
. /etc/os-release
case "${ID,,}" in
debian) OS="debian" ;;
ubuntu) OS="ubuntu" ;;
*)
# Fall back via ID_LIKE if helpful
if [[ "${ID_LIKE:-}" =~ debian ]]; then
OS="debian"
else
echo "[ERROR] Unsupported OS: ${ID}." >&2
exit 1
fi
;;
esac
# CODENAME detection with fallbacks
CODENAME="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
if [[ -z "$CODENAME" ]] && command -v lsb_release >/dev/null 2>&1; then
CODENAME="$(lsb_release -cs 2>/dev/null || true)"
fi
if [[ -z "$CODENAME" ]]; then
# Last-resort: attempt to infer from apt-cache (n= field)
CODENAME="$(apt-cache policy 2>/dev/null | sed -n 's/.*n=\([^,]*\).*/\1/p' | grep -E '^[a-z]+$' | head -n1 || true)"
fi
echo "[INFO] Detected OS: $OS codename: ${CODENAME:-unknown}"
}
apt_refresh_and_install() {
echo "[INFO] Updating APT cache…"
apt-get update -qq || true
echo "[INFO] Installing unattended-upgrades…"
DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null
}
extract_policy_pairs() {
# Extract distinct Origin:Archive pairs that APT knows about.
# Examples: "Debian:bookworm", "Debian:stable-security", "Ubuntu:noble-updates", "UbuntuESM:noble-infra-security"
mapfile -t POLICY_PAIRS < <(
apt-cache policy 2>/dev/null \
| sed -n 's/.*o=\([^,]*\),a=\([^,]*\).*/\1:\2/p' \
| awk 'NF' \
| sort -u
)
}
build_allowed_origins() {
DETECTED_ORIGINS=()
# Helper: add if present in POLICY_PAIRS
have_pair() {
local needle="$1"
printf '%s\n' "${POLICY_PAIRS[@]}" | grep -Fxq -- "$needle"
}
add_if_present() {
local pair="$1"
if have_pair "$pair"; then
DETECTED_ORIGINS+=("$pair")
fi
}
# Debian: prefer codename pockets; also include stable/testing aliases if present.
if [[ "$OS" == "debian" ]]; then
# Codename-based pockets (typical)
add_if_present "Debian:${CODENAME}"
add_if_present "Debian:${CODENAME}-updates"
add_if_present "Debian:${CODENAME}-security"
# Suite aliases (if admin uses 'stable' or 'testing' in sources)
add_if_present "Debian:stable"
add_if_present "Debian:stable-updates"
add_if_present "Debian:stable-security"
add_if_present "Debian:testing"
add_if_present "Debian:testing-security"
# If nothing matched (e.g., first run, cache stale), fall back to sensible defaults
if [[ ${#DETECTED_ORIGINS[@]} -eq 0 ]]; then
DETECTED_ORIGINS=(
"Debian:${CODENAME}"
"Debian:${CODENAME}-updates"
"Debian:${CODENAME}-security"
)
fi
fi
# Ubuntu: pockets + optional ESM (if enabled on the host)
if [[ "$OS" == "ubuntu" ]]; then
add_if_present "Ubuntu:${CODENAME}"
add_if_present "Ubuntu:${CODENAME}-updates"
add_if_present "Ubuntu:${CODENAME}-security"
# ESM origins (if the machine is attached to Ubuntu Pro/Advantage)
add_if_present "UbuntuESM:${CODENAME}-infra-security"
add_if_present "UbuntuESMApps:${CODENAME}-apps-security"
if [[ ${#DETECTED_ORIGINS[@]} -eq 0 ]]; then
# As with Debian, fall back to standard pockets
DETECTED_ORIGINS=(
"Ubuntu:${CODENAME}"
"Ubuntu:${CODENAME}-updates"
"Ubuntu:${CODENAME}-security"
)
fi
fi
echo "[INFO] Allowed-Origins to be written:"
printf ' %s\n' "${DETECTED_ORIGINS[@]}"
}
write_50unattended() {
local origins_block=""
for o in "${DETECTED_ORIGINS[@]}"; do
origins_block+=" \"${o}\";\n"
done
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
// Auto-installed by setup-auto-updates.sh
// Built from apt-cache policy to handle codename vs suite (stable/testing) seamlessly.
Unattended-Upgrade::Allowed-Origins {
${origins_block}
};
// Reboot policy
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "${REBOOT_TIME}";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
// Cleanups
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Logging
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::Verbose "0";
// Keep defaults for packages blacklist/whitelist (none here)
EOF
}
write_20auto() {
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
}
enable_timers_if_systemd() {
if command -v systemctl >/dev/null 2>&1; then
# Not all containers have systemd; ignore failures.
systemctl enable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
fi
}
validate_with_dryrun() {
echo "[INFO] Validating unattended-upgrades with a dry run…"
local log="/tmp/unattended-upgrades-dryrun.$$"
if ! timeout 90 unattended-upgrades --dry-run --debug >"$log" 2>&1; then
echo "[WARN] Dry run timed out or failed; see $log"
return 1
fi
# Show recognized allowed origins summary
grep -E "Allowed origins are" "$log" | head -n1 || true
# If nothing to upgrade, that's fine; ensure there is no obvious parse error.
if grep -qiE "Unable to parse|ValueError|AttributeError|ImportError" "$log"; then
echo "[ERROR] Parsing error detected; see $log"
return 1
fi
# Confirm at least one of our pairs appears in the "Allowed origins are" line
local ok=0
local line
line="$(grep -m1 "Allowed origins are" "$log" || true)"
if [[ -n "$line" ]]; then
for pair in "${DETECTED_ORIGINS[@]}"; do
# Translate "Origin:Archive" -> "o=Origin,a=Archive" for comparison
if [[ "$pair" =~ ^([^:]+):(.+)$ ]]; then
local o="o=${BASH_REMATCH[1]},a=${BASH_REMATCH[2]}"
if [[ "$line" == *"$o"* ]]; then
ok=1; break
fi
fi
done
fi
if (( ok == 0 )); then
echo "[WARN] None of the configured origins were echoed in the debug header; this can be benign if the host has no updates yet or APT lists are stale. See $log for context."
fi
}
show_status() {
echo
echo "[INFO] Config files:"
ls -la /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/50unattended-upgrades 2>/dev/null || true
echo
if command -v systemctl >/dev/null 2>&1; then
echo "[INFO] Timers:"
systemctl list-timers --all | grep -E 'apt-(daily|daily-upgrade)\.timer' || true
else
echo "[INFO] systemd not present; relying on APT::Periodic via cron."
fi
}
main() {
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
require_root
detect_os
apt_refresh_and_install
extract_policy_pairs
build_allowed_origins
write_50unattended
write_20auto
enable_timers_if_systemd
validate_with_dryrun
show_status
echo
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
}
main "$@"