Files
apt-autoupdate-install/auto-updates-TESTING.sh
2025-07-28 05:22:34 +00:00

231 lines
7.2 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 (not legacy Allowed-Origins) for forward-compatibility
# - Works with sources.list and deb822 .sources
# - Enables security + updates pockets; optional Ubuntu ESM if present
# - Validates with a dry-run; resilient to apt/dpkg locks
#
# Notes:
# * Backports/proposed are intentionally not enabled.
# * On non-systemd hosts, APT::Periodic drives execution via cron.
set -Eeuo pipefail
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
REBOOT_TIME="${REBOOT_TIME:-04:00}"
OS=""
CODENAME=""
declare -a POLICY_PAIRS=() # "Origin:Archive" from apt-cache
declare -a ORIGIN_PATTERNS=() # "origin=...,archive=..."
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
case "${ID,,}" in
debian) OS="debian" ;;
ubuntu) OS="ubuntu" ;;
*) [[ "${ID_LIKE:-}" =~ debian ]] && OS="debian" || { echo "[ERROR] Unsupported OS: ${ID}."; exit 1; } ;;
esac
CODENAME="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
if [[ -z "$CODENAME" ]] && command -v lsb_release >/dev/null 2>&1; then
CODENAME="$(lsb_release -cs 2>/dev/null || true)"
fi
if [[ -z "$CODENAME" ]]; then
CODENAME="$(apt-cache policy 2>/dev/null | sed -n 's/.*n=\([^,]*\).*/\1/p' | grep -E '^[a-z]+$' | head -n1 || true)"
fi
echo "[INFO] Detected OS: $OS codename: ${CODENAME:-unknown}"
}
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
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
}
extract_policy_pairs() {
# Extract unique Origin:Archive pairs from apt-cache policy lines
mapfile -t POLICY_PAIRS < <(
apt-cache policy 2>/dev/null \
| sed -n 's/.*o=\([^,]*\),a=\([^,]*\).*/\1:\2/p' \
| awk 'NF' | sort -u
)
}
build_origin_patterns() {
ORIGIN_PATTERNS=()
have_pair() { printf '%s\n' "${POLICY_PAIRS[@]}" | grep -Fxq -- "$1"; }
add_pat() { ORIGIN_PATTERNS+=("origin=$1,archive=$2"); }
if [[ "$OS" == "debian" ]]; then
# Consider common Debian suites (codename/stable/testing variants)
local want=( "$CODENAME" "$CODENAME-updates" "$CODENAME-security"
stable stable-updates stable-security
testing testing-security )
for a in "${want[@]}"; do
have_pair "Debian:$a" && add_pat "Debian" "$a"
done
# Fallback if apt-cache didnt show anything yet
if [[ ${#ORIGIN_PATTERNS[@]} -eq 0 ]]; then
ORIGIN_PATTERNS+=("origin=Debian,archive=$CODENAME"
"origin=Debian,archive=${CODENAME}-updates"
"origin=Debian,archive=${CODENAME}-security")
fi
else # ubuntu
# Standard pockets
for a in "$CODENAME" "${CODENAME}-updates" "${CODENAME}-security"; do
have_pair "Ubuntu:$a" && add_pat "Ubuntu" "$a"
done
# Optional ESM, only if attached and present
have_pair "UbuntuESM:${CODENAME}-infra-security" && add_pat "UbuntuESM" "${CODENAME}-infra-security"
have_pair "UbuntuESMApps:${CODENAME}-apps-security" && add_pat "UbuntuESMApps" "${CODENAME}-apps-security"
if [[ ${#ORIGIN_PATTERNS[@]} -eq 0 ]]; then
ORIGIN_PATTERNS+=("origin=Ubuntu,archive=$CODENAME"
"origin=Ubuntu,archive=${CODENAME}-updates"
"origin=Ubuntu,archive=${CODENAME}-security")
fi
fi
echo "[INFO] Origins-Pattern to be written:"
printf ' %s\n' "${ORIGIN_PATTERNS[@]}"
}
write_50unattended() {
local patterns=""
for p in "${ORIGIN_PATTERNS[@]}"; do
patterns+=" \"$p\";\n"
done
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
// Auto-installed by setup-auto-updates.sh
// Use Origins-Pattern (modern) and explicitly clear legacy Allowed-Origins.
// Clear legacy stanzas to avoid legacy parser bugs.
#clear Unattended-Upgrade::Allowed-Origins;
#clear Unattended-Upgrade::Origins-Pattern;
Unattended-Upgrade::Origins-Pattern {
${patterns}
};
// Reboot policy
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "${REBOOT_TIME}";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
// Cleanups
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Logging
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::Verbose "0";
EOF
}
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] 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
extract_policy_pairs
build_origin_patterns
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; normal timers/periodic will still run. Inspect the log above if needed."
fi
show_status
echo
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
}
main "$@"