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.
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
# - Clears legacy Allowed-Origins to avoid fragile legacy parser
|
# - Clears legacy Allowed-Origins to avoid fragile legacy parser
|
||||||
# - Enables APT Periodic + systemd timers (when present)
|
# - Enables APT Periodic + systemd timers (when present)
|
||||||
# - Dry-run validation that won’t abort the run if APT is busy
|
# - 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.
|
# Re-run safe: overwrites 50unattended-upgrades and 20auto-upgrades.
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ set -Eeuo pipefail
|
|||||||
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
||||||
|
|
||||||
REBOOT_TIME="${REBOOT_TIME:-04:00}"
|
REBOOT_TIME="${REBOOT_TIME:-04:00}"
|
||||||
|
UPDATE_SCOPE="${UPDATE_SCOPE:-}" # optional env override: "all" or "security"
|
||||||
|
OS=""
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
@@ -29,8 +32,8 @@ detect_os() {
|
|||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
local id="${ID,,}"
|
local id="${ID,,}"
|
||||||
case "$id" in
|
case "$id" in
|
||||||
debian|ubuntu) : ;;
|
debian|ubuntu) OS="$id" ;;
|
||||||
*) [[ "${ID_LIKE:-}" =~ debian ]] || { echo "[ERROR] Unsupported OS: $ID."; exit 1; } ;;
|
*) [[ "${ID_LIKE:-}" =~ debian ]] && OS="debian" || { echo "[ERROR] Unsupported OS: $ID."; exit 1; } ;;
|
||||||
esac
|
esac
|
||||||
echo "[INFO] Detected OS: ${PRETTY_NAME:-$ID}"
|
echo "[INFO] Detected OS: ${PRETTY_NAME:-$ID}"
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,34 @@ wait_for_apt() {
|
|||||||
return 1
|
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() {
|
apt_refresh_and_install() {
|
||||||
wait_for_apt || true
|
wait_for_apt || true
|
||||||
echo "[INFO] Updating APT cache…"
|
echo "[INFO] Updating APT cache…"
|
||||||
@@ -65,9 +96,12 @@ apt_refresh_and_install() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
write_50unattended() {
|
write_50unattended() {
|
||||||
# Use a single-quoted heredoc to avoid expanding ${distro_*} macros here.
|
# Build the Origins-Pattern block dynamically without expanding unattended macros.
|
||||||
# Then substitute REBOOT_TIME safely afterward.
|
local tmp_file="/etc/apt/apt.conf.d/50unattended-upgrades"
|
||||||
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
|
|
||||||
|
{
|
||||||
|
# Header and open block (single-quoted heredoc preserves ${distro_*})
|
||||||
|
cat <<'EOF'
|
||||||
// Auto-installed by setup-auto-updates.sh
|
// Auto-installed by setup-auto-updates.sh
|
||||||
// Use Origins-Pattern only; clear legacy to avoid fragile parsing.
|
// Use Origins-Pattern only; clear legacy to avoid fragile parsing.
|
||||||
|
|
||||||
@@ -75,19 +109,28 @@ write_50unattended() {
|
|||||||
#clear Unattended-Upgrade::Origins-Pattern;
|
#clear Unattended-Upgrade::Origins-Pattern;
|
||||||
|
|
||||||
Unattended-Upgrade::Origins-Pattern {
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
// Cross-distro (Debian or Ubuntu) standard pockets (archive form)
|
EOF
|
||||||
"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=
|
# ALL vs SECURITY blocks
|
||||||
"origin=${distro_id},codename=${distro_codename}";
|
if [[ "$UPDATE_SCOPE" == "all" ]]; then
|
||||||
"origin=${distro_id},codename=${distro_codename}-updates";
|
printf ' "origin=${distro_id},archive=${distro_codename}";\n'
|
||||||
"origin=${distro_id},codename=${distro_codename}-security";
|
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)
|
# Ubuntu ESM pockets (only meaningful on Ubuntu; harmless otherwise—will be removed later on non-Ubuntu)
|
||||||
"origin=UbuntuESM,archive=${distro_codename}-infra-security";
|
printf ' "origin=UbuntuESM,archive=${distro_codename}-infra-security";\n'
|
||||||
"origin=UbuntuESMApps,archive=${distro_codename}-apps-security";
|
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 "true";
|
||||||
@@ -100,9 +143,15 @@ Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
|||||||
Unattended-Upgrade::SyslogEnable "true";
|
Unattended-Upgrade::SyslogEnable "true";
|
||||||
Unattended-Upgrade::Verbose "0";
|
Unattended-Upgrade::Verbose "0";
|
||||||
EOF
|
EOF
|
||||||
|
} > "$tmp_file"
|
||||||
|
|
||||||
# Inject the desired reboot time without touching macro lines
|
# Inject the desired reboot time without touching ${distro_*} macros
|
||||||
sed -i "s/REBOOT_TIME_PLACEHOLDER/${REBOOT_TIME}/" /etc/apt/apt.conf.d/50unattended-upgrades
|
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() {
|
write_20auto() {
|
||||||
@@ -128,9 +177,7 @@ validate_with_dryrun() {
|
|||||||
echo "[WARN] Dry run timed out or failed; see $log"
|
echo "[WARN] Dry run timed out or failed; see $log"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
# Show the header line so you can see what matched
|
|
||||||
grep -E "Allowed origins are" "$log" | head -n1 || true
|
grep -E "Allowed origins are" "$log" | head -n1 || true
|
||||||
# Catch real parser errors
|
|
||||||
if grep -qiE "Unable to parse|ValueError|AttributeError|ImportError" "$log"; then
|
if grep -qiE "Unable to parse|ValueError|AttributeError|ImportError" "$log"; then
|
||||||
echo "[ERROR] Parsing error detected; see $log"
|
echo "[ERROR] Parsing error detected; see $log"
|
||||||
return 1
|
return 1
|
||||||
@@ -139,6 +186,7 @@ validate_with_dryrun() {
|
|||||||
|
|
||||||
show_status() {
|
show_status() {
|
||||||
echo
|
echo
|
||||||
|
echo "[INFO] Update scope: $UPDATE_SCOPE"
|
||||||
echo "[INFO] Config files:"
|
echo "[INFO] Config files:"
|
||||||
ls -la /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/50unattended-upgrades 2>/dev/null || true
|
ls -la /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/50unattended-upgrades 2>/dev/null || true
|
||||||
echo
|
echo
|
||||||
@@ -150,48 +198,16 @@ show_status() {
|
|||||||
fi
|
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() {
|
main() {
|
||||||
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
|
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
|
||||||
require_root
|
require_root
|
||||||
detect_os
|
detect_os
|
||||||
|
prompt_update_scope
|
||||||
apt_refresh_and_install
|
apt_refresh_and_install
|
||||||
write_50unattended
|
write_50unattended
|
||||||
write_20auto
|
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
|
enable_timers_if_systemd
|
||||||
|
|
||||||
# Non-fatal validation (avoid aborting the run if APT is busy).
|
|
||||||
set +e
|
set +e
|
||||||
validate_with_dryrun
|
validate_with_dryrun
|
||||||
vr=$?
|
vr=$?
|
||||||
@@ -202,10 +218,7 @@ main() {
|
|||||||
|
|
||||||
show_status
|
show_status
|
||||||
echo
|
echo
|
||||||
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
|
echo "[OK] Unattended updates configured (%s updates); reboot at %s if needed." "$UPDATE_SCOPE" "$REBOOT_TIME"
|
||||||
|
|
||||||
# Offer to remove the script itself
|
|
||||||
prompt_self_delete
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user