Add setup-auto-updates.sh

This commit is contained in:
2025-07-28 05:28:34 +00:00
parent b62f12c4cc
commit e20ca8fa4f

175
setup-auto-updates.sh Normal file
View File

@@ -0,0 +1,175 @@
#!/usr/bin/env bash
#
# setup-auto-updates.sh
#
# Universal unattended-upgrades setup for Debian & Ubuntu.
# - Uses Origins-Pattern (modern) with ${distro_id}/${distro_codename} macros
# - Safe for Debian Bookworm/Trixie and Ubuntu Noble+ (classic & deb822 sources)
# - Clears legacy Allowed-Origins to avoid fragile legacy parser
# - Enables APT Periodic + systemd timers (when present)
# - Dry-run validation that wont abort the run if APT is busy
#
# Re-run safe: overwrites 50unattended-upgrades and 20auto-upgrades.
set -Eeuo pipefail
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
REBOOT_TIME="${REBOOT_TIME:-04:00}"
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] Run as root (sudo)." >&2
exit 1
fi
}
detect_os() {
[[ -f /etc/os-release ]] || { echo "[ERROR] /etc/os-release not found." >&2; exit 1; }
# shellcheck disable=SC1091
. /etc/os-release
local id="${ID,,}"
case "$id" in
debian|ubuntu) : ;;
*) [[ "${ID_LIKE:-}" =~ debian ]] || { echo "[ERROR] Unsupported OS: $ID."; exit 1; } ;;
esac
echo "[INFO] Detected OS: ${PRETTY_NAME:-$ID}"
}
wait_for_apt() {
echo "[INFO] Checking for apt/dpkg locks…"
local locks=(/var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock)
for i in {1..90}; do
if ! fuser "${locks[@]}" >/dev/null 2>&1; then
echo "[INFO] No locks detected."
return 0
fi
(( i % 10 == 0 )) && echo "[INFO] apt/dpkg busy…waiting $(($i*2))s"
sleep 2
done
echo "[WARN] apt/dpkg still busy after ~180s; continuing anyway."
return 1
}
apt_refresh_and_install() {
wait_for_apt || true
echo "[INFO] Updating APT cache…"
apt-get update -qq || true
wait_for_apt || true
echo "[INFO] Installing unattended-upgrades…"
DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null || true
# Ensure periodic is enabled via debconf
echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' | debconf-set-selections || true
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive unattended-upgrades >/dev/null || true
}
write_50unattended() {
# Use a single-quoted heredoc to avoid expanding ${distro_*} macros here.
# Then substitute REBOOT_TIME safely afterward.
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
// Auto-installed by setup-auto-updates.sh
// Use Origins-Pattern only; clear legacy to avoid fragile parsing.
#clear Unattended-Upgrade::Allowed-Origins;
#clear Unattended-Upgrade::Origins-Pattern;
Unattended-Upgrade::Origins-Pattern {
// Cross-distro (Debian or Ubuntu) standard pockets (archive form)
"origin=${distro_id},archive=${distro_codename}";
"origin=${distro_id},archive=${distro_codename}-updates";
"origin=${distro_id},archive=${distro_codename}-security";
// Also allow codename form to match mirrors that expose only n=
"origin=${distro_id},codename=${distro_codename}";
"origin=${distro_id},codename=${distro_codename}-updates";
"origin=${distro_id},codename=${distro_codename}-security";
// Ubuntu ESM (harmless on Debian; matches only if present/attached)
"origin=UbuntuESM,archive=${distro_codename}-infra-security";
"origin=UbuntuESMApps,archive=${distro_codename}-apps-security";
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "REBOOT_TIME_PLACEHOLDER";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::Verbose "0";
EOF
# Inject the desired reboot time without touching macro lines
sed -i "s/REBOOT_TIME_PLACEHOLDER/${REBOOT_TIME}/" /etc/apt/apt.conf.d/50unattended-upgrades
}
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
systemctl enable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
fi
}
validate_with_dryrun() {
wait_for_apt || true
echo "[INFO] Validating unattended-upgrades with a dry run…"
local log="/tmp/unattended-upgrades-dryrun.$$"
if ! timeout 180 unattended-upgrades --dry-run --debug >"$log" 2>&1; then
echo "[WARN] Dry run timed out or failed; see $log"
return 1
fi
# Show the header line so you can see what matched
grep -E "Allowed origins are" "$log" | head -n1 || true
# Catch real parser errors
if grep -qiE "Unable to parse|ValueError|AttributeError|ImportError" "$log"; then
echo "[ERROR] Parsing error detected; see $log"
return 1
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
write_50unattended
write_20auto
enable_timers_if_systemd
set +e
validate_with_dryrun
vr=$?
set -e
if (( vr != 0 )); then
echo "[WARN] Validation failed or timed out. Config is written; timers/periodic will still run."
fi
show_status
echo
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
}
main "$@"