Files
apt-autoupdate-install/setup-auto-updates.sh
mike d07a25d742 Add interactive prompt for full vs. security-only updates
The script now prompts the user to choose between enabling all updates
or limiting to security-only updates via unattended-upgrades. This
choice is respected in the Origins-Pattern configuration, making the
script more flexible for different update policies. Non-interactive
runs default to all updates. Support for UPDATE_SCOPE env var added.
2025-07-28 06:02:24 +00:00

225 lines
7.1 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/CI first
if [[ -n "$UPDATE_SCOPE" ]]; then
case "${UPDATE_SCOPE,,}" in
all|security) ;; # ok
*) echo "[ERROR] UPDATE_SCOPE must be 'all' or 'security'"; exit 1 ;;
esac
echo "[INFO] Update scope (from env): $UPDATE_SCOPE"
return 0
fi
# If non-interactive, default to ALL
if [[ ! -t 0 ]]; then
UPDATE_SCOPE="all"
echo "[INFO] Non-interactive session: defaulting to ALL updates."
return 0
fi
echo
read -r -p "Enable installation of ALL updates (not just security)? [Y/n] " reply
case "$reply" in
""|[yY]|[yY][eE][sS]) UPDATE_SCOPE="all" ;;
[nN]|[nN][oO]) UPDATE_SCOPE="security" ;;
*) UPDATE_SCOPE="all" ;; # default
esac
echo "[INFO] Chosen update scope: $UPDATE_SCOPE"
}
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 "$@"