Files
apt-autoupdate-install/setup-auto-updates.sh
2025-07-28 06:11:39 +00:00

233 lines
7.7 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
# - Interactive choice: ALL updates or SECURITY-only updates
#
# 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}"
UPDATE_SCOPE="${UPDATE_SCOPE:-}" # optional env override: "all" or "security"
OS=""
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) OS="$id" ;;
*) [[ "${ID_LIKE:-}" =~ debian ]] && OS="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
}
prompt_update_scope() {
# Honor env override first (supports: all, full, security)
if [[ -n "${UPDATE_SCOPE:-}" ]]; then
case "${UPDATE_SCOPE,,}" in
all|full) UPDATE_SCOPE="all"; echo "[INFO] Update scope (from env): ALL"; return 0 ;;
security) UPDATE_SCOPE="security"; echo "[INFO] Update scope (from env): SECURITY-only"; return 0 ;;
*) echo "[ERROR] UPDATE_SCOPE must be 'all' (or 'full') or 'security'."; exit 1 ;;
esac
fi
# Non-interactive (no TTY): default to Full
if [[ ! -t 0 ]]; then
UPDATE_SCOPE="all"
echo "[INFO] Non-interactive session: defaulting to ALL updates."
return 0
fi
echo
echo "Configure automatic updates. Choose one:"
echo " 1) Full updates — install regular and security updates from your distro"
echo " (recommended for most systems; includes -updates and -security pockets)."
echo " 2) Security-only — install only security updates from your distro"
echo " (fewer changes; functional fixes may be delayed)."
echo
while true; do
read -r -p "Select [1/2] (default: 1): " choice
case "$choice" in
""|1) UPDATE_SCOPE="all"; echo "[INFO] Chosen update scope: ALL"; break ;;
2) UPDATE_SCOPE="security"; echo "[INFO] Chosen update scope: SECURITY-only"; break ;;
*) echo "Please enter 1 or 2."; ;;
esac
done
}
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() {
# Build the Origins-Pattern block dynamically without expanding unattended macros.
local tmp_file="/etc/apt/apt.conf.d/50unattended-upgrades"
{
# Header and open block (single-quoted heredoc preserves ${distro_*})
cat <<'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 {
EOF
# ALL vs SECURITY blocks
if [[ "$UPDATE_SCOPE" == "all" ]]; then
printf ' "origin=${distro_id},archive=${distro_codename}";\n'
printf ' "origin=${distro_id},archive=${distro_codename}-updates";\n'
printf ' "origin=${distro_id},archive=${distro_codename}-security";\n'
printf ' "origin=${distro_id},codename=${distro_codename}";\n'
printf ' "origin=${distro_id},codename=${distro_codename}-updates";\n'
printf ' "origin=${distro_id},codename=${distro_codename}-security";\n'
else
# Security-only
printf ' "origin=${distro_id},archive=${distro_codename}-security";\n'
printf ' "origin=${distro_id},codename=${distro_codename}-security";\n'
fi
# Ubuntu ESM pockets (only meaningful on Ubuntu; harmless otherwise—will be removed later on non-Ubuntu)
printf ' "origin=UbuntuESM,archive=${distro_codename}-infra-security";\n'
printf ' "origin=UbuntuESMApps,archive=${distro_codename}-apps-security";\n'
# Close block + rest of configuration
cat <<'EOF'
};
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
} > "$tmp_file"
# Inject the desired reboot time without touching ${distro_*} macros
sed -i "s/REBOOT_TIME_PLACEHOLDER/${REBOOT_TIME}/" "$tmp_file"
# For neatness: drop ESM patterns on non-Ubuntu systems
if [[ "$OS" != "ubuntu" ]]; then
sed -i '/UbuntuESM/d' "$tmp_file"
fi
}
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
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] Update scope: $UPDATE_SCOPE"
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
prompt_update_scope
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 (%s updates); reboot at %s if needed." "$UPDATE_SCOPE" "$REBOOT_TIME"
}
main "$@"