Update auto-updates-TESTING.sh
This commit is contained in:
@@ -2,16 +2,15 @@
|
|||||||
#
|
#
|
||||||
# setup-auto-updates.sh
|
# setup-auto-updates.sh
|
||||||
#
|
#
|
||||||
# Universal unattended-upgrades setup for Debian & Ubuntu (VMs, LXCs, mixed fleets).
|
# Universal unattended-upgrades setup for Debian & Ubuntu.
|
||||||
# - Derives Allowed-Origins from `apt-cache policy` to handle "stable" vs codename cleanly
|
# - Uses Origins-Pattern (not legacy Allowed-Origins) for forward-compatibility
|
||||||
# - Supports Debian (stable/testing) incl. security pockets; forward-compatible with Trixie
|
# - Works with sources.list and deb822 .sources
|
||||||
# - Supports Ubuntu (noble, etc.) incl. -updates, -security, and optional ESM pockets
|
# - Enables security + updates pockets; optional Ubuntu ESM if present
|
||||||
# - Works with classic sources.list and deb822 `.sources` files
|
# - Validates with a dry-run; resilient to apt/dpkg locks
|
||||||
# - Validates config with a dry run and enables systemd timers when available
|
|
||||||
#
|
#
|
||||||
# Notes:
|
# Notes:
|
||||||
# * Backports are intentionally NOT auto-updated.
|
# * Backports/proposed are intentionally not enabled.
|
||||||
# * On minimal containers without systemd, the APT::Periodic cron path is used.
|
# * On non-systemd hosts, APT::Periodic drives execution via cron.
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
||||||
@@ -20,8 +19,8 @@ REBOOT_TIME="${REBOOT_TIME:-04:00}"
|
|||||||
|
|
||||||
OS=""
|
OS=""
|
||||||
CODENAME=""
|
CODENAME=""
|
||||||
DETECTED_ORIGINS=() # e.g., ("Debian:bookworm" "Debian:bookworm-security" ...)
|
declare -a POLICY_PAIRS=() # "Origin:Archive" from apt-cache
|
||||||
POLICY_PAIRS=() # raw pairs as "Origin:Archive" extracted from apt-cache
|
declare -a ORIGIN_PATTERNS=() # "origin=...,archive=..."
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
@@ -31,132 +30,119 @@ require_root() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detect_os() {
|
detect_os() {
|
||||||
if [[ ! -f /etc/os-release ]]; then
|
[[ -f /etc/os-release ]] || { echo "[ERROR] /etc/os-release not found." >&2; exit 1; }
|
||||||
echo "[ERROR] /etc/os-release not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
case "${ID,,}" in
|
case "${ID,,}" in
|
||||||
debian) OS="debian" ;;
|
debian) OS="debian" ;;
|
||||||
ubuntu) OS="ubuntu" ;;
|
ubuntu) OS="ubuntu" ;;
|
||||||
*)
|
*) [[ "${ID_LIKE:-}" =~ debian ]] && OS="debian" || { echo "[ERROR] Unsupported OS: ${ID}."; exit 1; } ;;
|
||||||
# Fall back via ID_LIKE if helpful
|
|
||||||
if [[ "${ID_LIKE:-}" =~ debian ]]; then
|
|
||||||
OS="debian"
|
|
||||||
else
|
|
||||||
echo "[ERROR] Unsupported OS: ${ID}." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# CODENAME detection with fallbacks
|
|
||||||
CODENAME="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
|
CODENAME="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
|
||||||
if [[ -z "$CODENAME" ]] && command -v lsb_release >/dev/null 2>&1; then
|
if [[ -z "$CODENAME" ]] && command -v lsb_release >/dev/null 2>&1; then
|
||||||
CODENAME="$(lsb_release -cs 2>/dev/null || true)"
|
CODENAME="$(lsb_release -cs 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
if [[ -z "$CODENAME" ]]; then
|
if [[ -z "$CODENAME" ]]; then
|
||||||
# Last-resort: attempt to infer from apt-cache (n= field)
|
|
||||||
CODENAME="$(apt-cache policy 2>/dev/null | sed -n 's/.*n=\([^,]*\).*/\1/p' | grep -E '^[a-z]+$' | head -n1 || true)"
|
CODENAME="$(apt-cache policy 2>/dev/null | sed -n 's/.*n=\([^,]*\).*/\1/p' | grep -E '^[a-z]+$' | head -n1 || true)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[INFO] Detected OS: $OS codename: ${CODENAME:-unknown}"
|
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() {
|
apt_refresh_and_install() {
|
||||||
|
wait_for_apt || true
|
||||||
echo "[INFO] Updating APT cache…"
|
echo "[INFO] Updating APT cache…"
|
||||||
apt-get update -qq || true
|
apt-get update -qq || true
|
||||||
|
|
||||||
|
wait_for_apt || true
|
||||||
echo "[INFO] Installing unattended-upgrades…"
|
echo "[INFO] Installing unattended-upgrades…"
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null
|
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_policy_pairs() {
|
||||||
# Extract distinct Origin:Archive pairs that APT knows about.
|
# Extract unique Origin:Archive pairs from apt-cache policy lines
|
||||||
# Examples: "Debian:bookworm", "Debian:stable-security", "Ubuntu:noble-updates", "UbuntuESM:noble-infra-security"
|
|
||||||
mapfile -t POLICY_PAIRS < <(
|
mapfile -t POLICY_PAIRS < <(
|
||||||
apt-cache policy 2>/dev/null \
|
apt-cache policy 2>/dev/null \
|
||||||
| sed -n 's/.*o=\([^,]*\),a=\([^,]*\).*/\1:\2/p' \
|
| sed -n 's/.*o=\([^,]*\),a=\([^,]*\).*/\1:\2/p' \
|
||||||
| awk 'NF' \
|
| awk 'NF' | sort -u
|
||||||
| sort -u
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
build_allowed_origins() {
|
build_origin_patterns() {
|
||||||
DETECTED_ORIGINS=()
|
ORIGIN_PATTERNS=()
|
||||||
|
|
||||||
# Helper: add if present in POLICY_PAIRS
|
have_pair() { printf '%s\n' "${POLICY_PAIRS[@]}" | grep -Fxq -- "$1"; }
|
||||||
have_pair() {
|
add_pat() { ORIGIN_PATTERNS+=("origin=$1,archive=$2"); }
|
||||||
local needle="$1"
|
|
||||||
printf '%s\n' "${POLICY_PAIRS[@]}" | grep -Fxq -- "$needle"
|
|
||||||
}
|
|
||||||
add_if_present() {
|
|
||||||
local pair="$1"
|
|
||||||
if have_pair "$pair"; then
|
|
||||||
DETECTED_ORIGINS+=("$pair")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Debian: prefer codename pockets; also include stable/testing aliases if present.
|
|
||||||
if [[ "$OS" == "debian" ]]; then
|
if [[ "$OS" == "debian" ]]; then
|
||||||
# Codename-based pockets (typical)
|
# Consider common Debian suites (codename/stable/testing variants)
|
||||||
add_if_present "Debian:${CODENAME}"
|
local want=( "$CODENAME" "$CODENAME-updates" "$CODENAME-security"
|
||||||
add_if_present "Debian:${CODENAME}-updates"
|
stable stable-updates stable-security
|
||||||
add_if_present "Debian:${CODENAME}-security"
|
testing testing-security )
|
||||||
|
for a in "${want[@]}"; do
|
||||||
|
have_pair "Debian:$a" && add_pat "Debian" "$a"
|
||||||
|
done
|
||||||
|
# Fallback if apt-cache didn’t 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"
|
||||||
|
|
||||||
# Suite aliases (if admin uses 'stable' or 'testing' in sources)
|
if [[ ${#ORIGIN_PATTERNS[@]} -eq 0 ]]; then
|
||||||
add_if_present "Debian:stable"
|
ORIGIN_PATTERNS+=("origin=Ubuntu,archive=$CODENAME"
|
||||||
add_if_present "Debian:stable-updates"
|
"origin=Ubuntu,archive=${CODENAME}-updates"
|
||||||
add_if_present "Debian:stable-security"
|
"origin=Ubuntu,archive=${CODENAME}-security")
|
||||||
add_if_present "Debian:testing"
|
|
||||||
add_if_present "Debian:testing-security"
|
|
||||||
|
|
||||||
# If nothing matched (e.g., first run, cache stale), fall back to sensible defaults
|
|
||||||
if [[ ${#DETECTED_ORIGINS[@]} -eq 0 ]]; then
|
|
||||||
DETECTED_ORIGINS=(
|
|
||||||
"Debian:${CODENAME}"
|
|
||||||
"Debian:${CODENAME}-updates"
|
|
||||||
"Debian:${CODENAME}-security"
|
|
||||||
)
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ubuntu: pockets + optional ESM (if enabled on the host)
|
echo "[INFO] Origins-Pattern to be written:"
|
||||||
if [[ "$OS" == "ubuntu" ]]; then
|
printf ' %s\n' "${ORIGIN_PATTERNS[@]}"
|
||||||
add_if_present "Ubuntu:${CODENAME}"
|
|
||||||
add_if_present "Ubuntu:${CODENAME}-updates"
|
|
||||||
add_if_present "Ubuntu:${CODENAME}-security"
|
|
||||||
|
|
||||||
# ESM origins (if the machine is attached to Ubuntu Pro/Advantage)
|
|
||||||
add_if_present "UbuntuESM:${CODENAME}-infra-security"
|
|
||||||
add_if_present "UbuntuESMApps:${CODENAME}-apps-security"
|
|
||||||
|
|
||||||
if [[ ${#DETECTED_ORIGINS[@]} -eq 0 ]]; then
|
|
||||||
# As with Debian, fall back to standard pockets
|
|
||||||
DETECTED_ORIGINS=(
|
|
||||||
"Ubuntu:${CODENAME}"
|
|
||||||
"Ubuntu:${CODENAME}-updates"
|
|
||||||
"Ubuntu:${CODENAME}-security"
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[INFO] Allowed-Origins to be written:"
|
|
||||||
printf ' %s\n' "${DETECTED_ORIGINS[@]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write_50unattended() {
|
write_50unattended() {
|
||||||
local origins_block=""
|
local patterns=""
|
||||||
for o in "${DETECTED_ORIGINS[@]}"; do
|
for p in "${ORIGIN_PATTERNS[@]}"; do
|
||||||
origins_block+=" \"${o}\";\n"
|
patterns+=" \"$p\";\n"
|
||||||
done
|
done
|
||||||
|
|
||||||
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
||||||
// Auto-installed by setup-auto-updates.sh
|
// Auto-installed by setup-auto-updates.sh
|
||||||
// Built from apt-cache policy to handle codename vs suite (stable/testing) seamlessly.
|
// Use Origins-Pattern (modern) and explicitly clear legacy Allowed-Origins.
|
||||||
|
|
||||||
Unattended-Upgrade::Allowed-Origins {
|
// Clear legacy stanzas to avoid legacy parser bugs.
|
||||||
${origins_block}
|
#clear Unattended-Upgrade::Allowed-Origins;
|
||||||
|
#clear Unattended-Upgrade::Origins-Pattern;
|
||||||
|
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
${patterns}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reboot policy
|
// Reboot policy
|
||||||
@@ -171,8 +157,6 @@ Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
|||||||
// Logging
|
// Logging
|
||||||
Unattended-Upgrade::SyslogEnable "true";
|
Unattended-Upgrade::SyslogEnable "true";
|
||||||
Unattended-Upgrade::Verbose "0";
|
Unattended-Upgrade::Verbose "0";
|
||||||
|
|
||||||
// Keep defaults for packages blacklist/whitelist (none here)
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,47 +171,23 @@ EOF
|
|||||||
|
|
||||||
enable_timers_if_systemd() {
|
enable_timers_if_systemd() {
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
# Not all containers have systemd; ignore failures.
|
|
||||||
systemctl enable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
|
systemctl enable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_with_dryrun() {
|
validate_with_dryrun() {
|
||||||
|
wait_for_apt || true
|
||||||
echo "[INFO] Validating unattended-upgrades with a dry run…"
|
echo "[INFO] Validating unattended-upgrades with a dry run…"
|
||||||
local log="/tmp/unattended-upgrades-dryrun.$$"
|
local log="/tmp/unattended-upgrades-dryrun.$$"
|
||||||
if ! timeout 90 unattended-upgrades --dry-run --debug >"$log" 2>&1; then
|
if ! timeout 180 unattended-upgrades --dry-run --debug >"$log" 2>&1; then
|
||||||
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 recognized allowed origins summary
|
|
||||||
grep -E "Allowed origins are" "$log" | head -n1 || true
|
grep -E "Allowed origins are" "$log" | head -n1 || true
|
||||||
|
|
||||||
# If nothing to upgrade, that's fine; ensure there is no obvious parse error.
|
|
||||||
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Confirm at least one of our pairs appears in the "Allowed origins are" line
|
|
||||||
local ok=0
|
|
||||||
local line
|
|
||||||
line="$(grep -m1 "Allowed origins are" "$log" || true)"
|
|
||||||
if [[ -n "$line" ]]; then
|
|
||||||
for pair in "${DETECTED_ORIGINS[@]}"; do
|
|
||||||
# Translate "Origin:Archive" -> "o=Origin,a=Archive" for comparison
|
|
||||||
if [[ "$pair" =~ ^([^:]+):(.+)$ ]]; then
|
|
||||||
local o="o=${BASH_REMATCH[1]},a=${BASH_REMATCH[2]}"
|
|
||||||
if [[ "$line" == *"$o"* ]]; then
|
|
||||||
ok=1; break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( ok == 0 )); then
|
|
||||||
echo "[WARN] None of the configured origins were echoed in the debug header; this can be benign if the host has no updates yet or APT lists are stale. See $log for context."
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_status() {
|
show_status() {
|
||||||
@@ -245,18 +205,24 @@ show_status() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
|
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
|
||||||
|
|
||||||
require_root
|
require_root
|
||||||
detect_os
|
detect_os
|
||||||
apt_refresh_and_install
|
apt_refresh_and_install
|
||||||
extract_policy_pairs
|
extract_policy_pairs
|
||||||
build_allowed_origins
|
build_origin_patterns
|
||||||
write_50unattended
|
write_50unattended
|
||||||
write_20auto
|
write_20auto
|
||||||
enable_timers_if_systemd
|
enable_timers_if_systemd
|
||||||
validate_with_dryrun
|
|
||||||
show_status
|
|
||||||
|
|
||||||
|
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
|
||||||
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
|
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user