From d07a25d742172c2578dcc23beac93774777c18e5 Mon Sep 17 00:00:00 2001 From: mike Date: Mon, 28 Jul 2025 06:02:24 +0000 Subject: [PATCH] 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. --- setup-auto-updates.sh | 127 +++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/setup-auto-updates.sh b/setup-auto-updates.sh index 3d74169..12339dc 100644 --- a/setup-auto-updates.sh +++ b/setup-auto-updates.sh @@ -8,6 +8,7 @@ # - Clears legacy Allowed-Origins to avoid fragile legacy parser # - Enables APT Periodic + systemd timers (when present) # - Dry-run validation that won’t abort the run if APT is busy +# - Interactive choice: ALL updates or SECURITY-only updates # # Re-run safe: overwrites 50unattended-upgrades and 20auto-upgrades. @@ -15,6 +16,8 @@ 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 @@ -29,8 +32,8 @@ detect_os() { . /etc/os-release local id="${ID,,}" case "$id" in - debian|ubuntu) : ;; - *) [[ "${ID_LIKE:-}" =~ debian ]] || { echo "[ERROR] Unsupported OS: $ID."; exit 1; } ;; + debian|ubuntu) OS="$id" ;; + *) [[ "${ID_LIKE:-}" =~ debian ]] && OS="debian" || { echo "[ERROR] Unsupported OS: $ID."; exit 1; } ;; esac echo "[INFO] Detected OS: ${PRETTY_NAME:-$ID}" } @@ -50,6 +53,34 @@ wait_for_apt() { 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…" @@ -65,9 +96,12 @@ apt_refresh_and_install() { } 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' + # 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. @@ -75,19 +109,28 @@ write_50unattended() { #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"; +EOF - // 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"; + # 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 (harmless on Debian; matches only if present/attached) - "origin=UbuntuESM,archive=${distro_codename}-infra-security"; - "origin=UbuntuESMApps,archive=${distro_codename}-apps-security"; + # 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"; @@ -100,9 +143,15 @@ 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 macro lines - sed -i "s/REBOOT_TIME_PLACEHOLDER/${REBOOT_TIME}/" /etc/apt/apt.conf.d/50unattended-upgrades + # 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() { @@ -128,9 +177,7 @@ validate_with_dryrun() { 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 @@ -139,6 +186,7 @@ validate_with_dryrun() { 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 @@ -150,48 +198,16 @@ show_status() { fi } -prompt_self_delete() { - # Only prompt on an interactive TTY - if [[ -t 0 ]]; then - echo - read -r -p "Script successful. Do you wish to delete this script? [y/N] " reply - case "$reply" in - [yY]|[yY][eE][sS]) - echo "[INFO] Removing script: $0" - rm -- "$0" 2>/dev/null || echo "[WARN] Could not delete $0 (permission or filesystem issue)." - ;; - *) - echo "[INFO] Keeping script: $0" - ;; - esac - else - echo "[INFO] Non-interactive session; skipping delete prompt." - fi -} - main() { echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)" require_root detect_os + prompt_update_scope apt_refresh_and_install write_50unattended write_20auto - - # If detect_os didn't export OS, derive it so we can gate ESM cleanup. - if [[ -z "${OS:-}" ]] && [[ -r /etc/os-release ]]; then - # shellcheck disable=SC1091 - . /etc/os-release - OS="${ID,,}" - fi - - # Drop Ubuntu ESM patterns on non-Ubuntu systems (cosmetic; harmless if kept). - if [[ "${OS:-}" != "ubuntu" ]]; then - sed -i '/UbuntuESM/d' /etc/apt/apt.conf.d/50unattended-upgrades - fi - enable_timers_if_systemd - # Non-fatal validation (avoid aborting the run if APT is busy). set +e validate_with_dryrun vr=$? @@ -202,10 +218,7 @@ main() { show_status echo - echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed." - - # Offer to remove the script itself - prompt_self_delete + echo "[OK] Unattended updates configured (%s updates); reboot at %s if needed." "$UPDATE_SCOPE" "$REBOOT_TIME" } main "$@"