Files
apt-autoupdate-install/setup-auto-updates.sh
2025-07-28 05:33:48 +00:00

186 lines
5.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
wait_for_apt_units || true
echo "[INFO] Validating unattended-upgrades with a dry run…"
local log="/tmp/unattended-upgrades-dryrun.$$"
# Temporarily disable ERR trap so a non-zero here doesn't print "[ERROR] Line …"
trap - ERR
set +e
timeout 240 unattended-upgrades --dry-run --debug >"$log" 2>&1
local rc=$?
set -e
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
if (( rc != 0 )); then
echo "[WARN] Dry run timed out or failed; see $log"
return 1
fi
grep -E "Allowed origins are" "$log" | head -n1 || true
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 "$@"