Update auto-updates-TESTING.sh
This commit is contained in:
@@ -2,479 +2,263 @@
|
|||||||
#
|
#
|
||||||
# setup-auto-updates.sh
|
# setup-auto-updates.sh
|
||||||
#
|
#
|
||||||
# Universal Debian/Ubuntu unattended-upgrades setup for mixed environments:
|
# Universal unattended-upgrades setup for Debian & Ubuntu (VMs, LXCs, mixed fleets).
|
||||||
# - Robust repository detection (handles stable/codename/future releases)
|
# - Derives Allowed-Origins from `apt-cache policy` to handle "stable" vs codename cleanly
|
||||||
# - Works across 30+ VMs/LXCs with different configurations
|
# - Supports Debian (stable/testing) incl. security pockets; forward-compatible with Trixie
|
||||||
# - Future-proof for Trixie and beyond
|
# - Supports Ubuntu (noble, etc.) incl. -updates, -security, and optional ESM pockets
|
||||||
# - Fast and reliable detection algorithms
|
# - Works with classic sources.list and deb822 `.sources` files
|
||||||
# - Validates configuration before completion
|
# - Validates config with a dry run and enables systemd timers when available
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# * Backports are intentionally NOT auto-updated.
|
||||||
|
# * On minimal containers without systemd, the APT::Periodic cron path is used.
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
trap 'echo "[ERROR] Line $LINENO failed" >&2' ERR
|
||||||
|
|
||||||
REBOOT_TIME="04:00"
|
REBOOT_TIME="${REBOOT_TIME:-04:00}"
|
||||||
TARGET_LOCALE="en_US.UTF-8"
|
|
||||||
|
|
||||||
# Repository detection results (global variables)
|
OS=""
|
||||||
declare -g MAIN_ARCHIVE=""
|
CODENAME=""
|
||||||
declare -g UPDATES_ARCHIVE=""
|
DETECTED_ORIGINS=() # e.g., ("Debian:bookworm" "Debian:bookworm-security" ...)
|
||||||
declare -g SECURITY_ARCHIVE=""
|
POLICY_PAIRS=() # raw pairs as "Origin:Archive" extracted from apt-cache
|
||||||
declare -g DETECTED_ORIGINS=()
|
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo "[ERROR] Run as root (sudo)" >&2
|
echo "[ERROR] Run as root (sudo)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_os() {
|
detect_os() {
|
||||||
if [[ ! -f /etc/os-release ]]; then
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
echo "[ERROR] /etc/os-release not found" >&2
|
echo "[ERROR] /etc/os-release not found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
case "$ID" in
|
case "${ID,,}" in
|
||||||
ubuntu) OS="ubuntu" ;;
|
|
||||||
debian) OS="debian" ;;
|
debian) OS="debian" ;;
|
||||||
*) echo "[ERROR] Unsupported OS: $ID" >&2; exit 1 ;;
|
ubuntu) OS="ubuntu" ;;
|
||||||
esac
|
|
||||||
CODENAME="${VERSION_CODENAME:-$UBUNTU_CODENAME}"
|
|
||||||
VERSION_ID="${VERSION_ID:-unknown}"
|
|
||||||
echo "[INFO] Detected: $PRETTY_NAME ($OS / $CODENAME)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract repository info from sources
|
|
||||||
parse_apt_sources() {
|
|
||||||
local sources_data=""
|
|
||||||
|
|
||||||
# Collect all active deb lines from sources.list and sources.list.d
|
|
||||||
if [[ -f /etc/apt/sources.list ]]; then
|
|
||||||
sources_data+=$(grep -E "^deb\s+" /etc/apt/sources.list 2>/dev/null || true)
|
|
||||||
sources_data+=$'\n'
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d /etc/apt/sources.list.d ]]; then
|
|
||||||
sources_data+=$(find /etc/apt/sources.list.d -name "*.list" -exec grep -E "^deb\s+" {} \; 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$sources_data"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect repositories for Ubuntu
|
|
||||||
detect_ubuntu_repos() {
|
|
||||||
echo "[INFO] Detecting Ubuntu repository configuration..."
|
|
||||||
|
|
||||||
local sources_data
|
|
||||||
sources_data=$(parse_apt_sources)
|
|
||||||
|
|
||||||
# Ubuntu typically uses consistent codename-based archives
|
|
||||||
MAIN_ARCHIVE="$CODENAME"
|
|
||||||
UPDATES_ARCHIVE="${CODENAME}-updates"
|
|
||||||
SECURITY_ARCHIVE="${CODENAME}-security"
|
|
||||||
|
|
||||||
# Verify these exist in sources
|
|
||||||
if echo "$sources_data" | grep -q "ubuntu.*$CODENAME\s"; then
|
|
||||||
echo "[INFO] Found Ubuntu $CODENAME repositories"
|
|
||||||
|
|
||||||
# Check for ESM (Extended Security Maintenance) repos for LTS versions
|
|
||||||
local esm_origins=()
|
|
||||||
if echo "$sources_data" | grep -q "esm.ubuntu.com"; then
|
|
||||||
esm_origins+=("Ubuntu ESM:${CODENAME}-infra-security")
|
|
||||||
esm_origins+=("Ubuntu ESM Apps:${CODENAME}-apps-security")
|
|
||||||
echo "[INFO] Found Ubuntu ESM repositories"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build origins array
|
|
||||||
DETECTED_ORIGINS=(
|
|
||||||
"Ubuntu:${MAIN_ARCHIVE}"
|
|
||||||
"Ubuntu:${UPDATES_ARCHIVE}"
|
|
||||||
"Ubuntu:${SECURITY_ARCHIVE}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add ESM if present
|
|
||||||
DETECTED_ORIGINS+=("${esm_origins[@]}")
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "[WARN] Standard Ubuntu repositories not found, using defaults"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect repositories for Debian with comprehensive fallback logic
|
|
||||||
detect_debian_repos() {
|
|
||||||
echo "[INFO] Detecting Debian repository configuration..."
|
|
||||||
|
|
||||||
# Update package lists for accurate detection
|
|
||||||
apt-get update -qq >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
local sources_data
|
|
||||||
sources_data=$(parse_apt_sources)
|
|
||||||
|
|
||||||
# Strategy 1: Parse sources.list for archive names
|
|
||||||
local candidate_archives=()
|
|
||||||
|
|
||||||
# Extract archives from main Debian repos (not security)
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" =~ deb[[:space:]]+[^[:space:]]+[[:space:]]+([^[:space:]]+) ]] &&
|
|
||||||
[[ "$line" == *"debian"* ]] &&
|
|
||||||
[[ "$line" != *"security"* ]]; then
|
|
||||||
local archive="${BASH_REMATCH[1]}"
|
|
||||||
if [[ "$archive" =~ ^(stable|testing|unstable|[a-z]+)$ ]]; then
|
|
||||||
candidate_archives+=("$archive")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$sources_data"
|
|
||||||
|
|
||||||
# Strategy 2: Check apt-cache policy for active origins
|
|
||||||
local policy_archives=()
|
|
||||||
if command -v apt-cache >/dev/null; then
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" =~ o=Debian,a=([^,]+) ]]; then
|
|
||||||
local archive="${BASH_REMATCH[1]}"
|
|
||||||
policy_archives+=("$archive")
|
|
||||||
fi
|
|
||||||
done < <(apt-cache policy 2>/dev/null | grep -E "^\s*[0-9]+\s+.*o=Debian,a=" || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Strategy 3: Test which archives actually have packages
|
|
||||||
local working_archives=()
|
|
||||||
local test_archives=("${candidate_archives[@]}" "${policy_archives[@]}" "stable" "$CODENAME")
|
|
||||||
|
|
||||||
# Remove duplicates and test each
|
|
||||||
local unique_archives=($(printf '%s\n' "${test_archives[@]}" | sort -u))
|
|
||||||
|
|
||||||
for archive in "${unique_archives[@]}"; do
|
|
||||||
if [[ -z "$archive" ]]; then continue; fi
|
|
||||||
|
|
||||||
# Test if this archive has packages available
|
|
||||||
if apt-cache policy 2>/dev/null | grep -q "o=Debian,a=$archive" ||
|
|
||||||
echo "$sources_data" | grep -q "debian.*$archive"; then
|
|
||||||
working_archives+=("$archive")
|
|
||||||
echo "[DEBUG] Found working archive: $archive"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Strategy 4: Choose the best archive
|
|
||||||
if [[ ${#working_archives[@]} -gt 0 ]]; then
|
|
||||||
# Prefer stable > codename > testing > others
|
|
||||||
for preferred in "stable" "$CODENAME" "testing"; do
|
|
||||||
for archive in "${working_archives[@]}"; do
|
|
||||||
if [[ "$archive" == "$preferred" ]]; then
|
|
||||||
MAIN_ARCHIVE="$archive"
|
|
||||||
break 2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
# If no preferred found, use first working archive
|
|
||||||
if [[ -z "$MAIN_ARCHIVE" ]]; then
|
|
||||||
MAIN_ARCHIVE="${working_archives[0]}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Fallback to codename
|
|
||||||
echo "[WARN] No working archives detected, using codename: $CODENAME"
|
|
||||||
MAIN_ARCHIVE="$CODENAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set related archives
|
|
||||||
UPDATES_ARCHIVE="${MAIN_ARCHIVE}-updates"
|
|
||||||
SECURITY_ARCHIVE="${MAIN_ARCHIVE}-security"
|
|
||||||
|
|
||||||
# Special handling for suite names
|
|
||||||
if [[ "$MAIN_ARCHIVE" == "testing" ]]; then
|
|
||||||
SECURITY_ARCHIVE="testing-security"
|
|
||||||
elif [[ "$MAIN_ARCHIVE" == "unstable" ]]; then
|
|
||||||
UPDATES_ARCHIVE="" # Unstable doesn't have updates
|
|
||||||
SECURITY_ARCHIVE="" # Unstable doesn't have security
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify security archive exists
|
|
||||||
if [[ -n "$SECURITY_ARCHIVE" ]]; then
|
|
||||||
if ! (apt-cache policy 2>/dev/null | grep -q "o=Debian,a=$SECURITY_ARCHIVE" ||
|
|
||||||
echo "$sources_data" | grep -q "security.*$SECURITY_ARCHIVE"); then
|
|
||||||
echo "[WARN] Security archive $SECURITY_ARCHIVE not found"
|
|
||||||
# Try alternative security archive naming
|
|
||||||
if [[ "$MAIN_ARCHIVE" != "stable" ]] &&
|
|
||||||
(apt-cache policy 2>/dev/null | grep -q "o=Debian,a=stable-security" ||
|
|
||||||
echo "$sources_data" | grep -q "security.*stable-security"); then
|
|
||||||
SECURITY_ARCHIVE="stable-security"
|
|
||||||
echo "[INFO] Using stable-security instead"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build origins array
|
|
||||||
DETECTED_ORIGINS=("Debian:${MAIN_ARCHIVE}")
|
|
||||||
|
|
||||||
if [[ -n "$UPDATES_ARCHIVE" ]]; then
|
|
||||||
DETECTED_ORIGINS+=("Debian:${UPDATES_ARCHIVE}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$SECURITY_ARCHIVE" ]]; then
|
|
||||||
DETECTED_ORIGINS+=("Debian-Security:${SECURITY_ARCHIVE}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[INFO] Debian archives selected:"
|
|
||||||
echo " Main: $MAIN_ARCHIVE"
|
|
||||||
[[ -n "$UPDATES_ARCHIVE" ]] && echo " Updates: $UPDATES_ARCHIVE"
|
|
||||||
[[ -n "$SECURITY_ARCHIVE" ]] && echo " Security: $SECURITY_ARCHIVE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main repository detection function
|
|
||||||
detect_repositories() {
|
|
||||||
echo "[INFO] Detecting repository configuration for $OS..."
|
|
||||||
|
|
||||||
case "$OS" in
|
|
||||||
ubuntu)
|
|
||||||
detect_ubuntu_repos
|
|
||||||
;;
|
|
||||||
debian)
|
|
||||||
detect_debian_repos
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "[ERROR] Unsupported OS for repository detection: $OS"
|
# Fall back via ID_LIKE if helpful
|
||||||
|
if [[ "${ID_LIKE:-}" =~ debian ]]; then
|
||||||
|
OS="debian"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Unsupported OS: ${ID}." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# CODENAME detection with fallbacks
|
||||||
|
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
|
||||||
|
# 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)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Detected OS: $OS codename: ${CODENAME:-unknown}"
|
||||||
|
}
|
||||||
|
|
||||||
|
apt_refresh_and_install() {
|
||||||
|
echo "[INFO] Updating APT cache…"
|
||||||
|
apt-get update -qq || true
|
||||||
|
echo "[INFO] Installing unattended-upgrades…"
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_policy_pairs() {
|
||||||
|
# Extract distinct Origin:Archive pairs that APT knows about.
|
||||||
|
# Examples: "Debian:bookworm", "Debian:stable-security", "Ubuntu:noble-updates", "UbuntuESM:noble-infra-security"
|
||||||
|
mapfile -t POLICY_PAIRS < <(
|
||||||
|
apt-cache policy 2>/dev/null \
|
||||||
|
| sed -n 's/.*o=\([^,]*\),a=\([^,]*\).*/\1:\2/p' \
|
||||||
|
| awk 'NF' \
|
||||||
|
| sort -u
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
build_allowed_origins() {
|
||||||
|
DETECTED_ORIGINS=()
|
||||||
|
|
||||||
|
# Helper: add if present in POLICY_PAIRS
|
||||||
|
have_pair() {
|
||||||
|
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
|
||||||
|
# Codename-based pockets (typical)
|
||||||
|
add_if_present "Debian:${CODENAME}"
|
||||||
|
add_if_present "Debian:${CODENAME}-updates"
|
||||||
|
add_if_present "Debian:${CODENAME}-security"
|
||||||
|
|
||||||
|
# Suite aliases (if admin uses 'stable' or 'testing' in sources)
|
||||||
|
add_if_present "Debian:stable"
|
||||||
|
add_if_present "Debian:stable-updates"
|
||||||
|
add_if_present "Debian:stable-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
|
||||||
|
|
||||||
|
# Ubuntu: pockets + optional ESM (if enabled on the host)
|
||||||
|
if [[ "$OS" == "ubuntu" ]]; then
|
||||||
|
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
|
if [[ ${#DETECTED_ORIGINS[@]} -eq 0 ]]; then
|
||||||
echo "[ERROR] No valid repository origins detected!"
|
# As with Debian, fall back to standard pockets
|
||||||
exit 1
|
DETECTED_ORIGINS=(
|
||||||
|
"Ubuntu:${CODENAME}"
|
||||||
|
"Ubuntu:${CODENAME}-updates"
|
||||||
|
"Ubuntu:${CODENAME}-security"
|
||||||
|
)
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[INFO] Final detected origins:"
|
echo "[INFO] Allowed-Origins to be written:"
|
||||||
printf ' %s\n' "${DETECTED_ORIGINS[@]}"
|
printf ' %s\n' "${DETECTED_ORIGINS[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fix_locale() {
|
write_50unattended() {
|
||||||
echo "[INFO] Ensuring locale ${TARGET_LOCALE} is generated…"
|
local origins_block=""
|
||||||
apt-get update -y >/dev/null 2>&1 || true
|
for o in "${DETECTED_ORIGINS[@]}"; do
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y locales >/dev/null 2>&1 || true
|
origins_block+=" \"${o}\";\n"
|
||||||
|
|
||||||
if [[ -f /etc/locale.gen ]]; then
|
|
||||||
if grep -qE "^\s*#\s*${TARGET_LOCALE}\s+UTF-8" /etc/locale.gen; then
|
|
||||||
sed -ri "s/^\s*#\s*(${TARGET_LOCALE}\s+UTF-8)/\1/" /etc/locale.gen
|
|
||||||
elif ! grep -qE "^\s*${TARGET_LOCALE}\s+UTF-8" /etc/locale.gen; then
|
|
||||||
echo "${TARGET_LOCALE} UTF-8" >> /etc/locale.gen
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "${TARGET_LOCALE} UTF-8" > /etc/locale.gen
|
|
||||||
fi
|
|
||||||
|
|
||||||
locale-gen >/dev/null 2>&1
|
|
||||||
update-locale LANG=${TARGET_LOCALE} LC_ALL=${TARGET_LOCALE}
|
|
||||||
export LANG=${TARGET_LOCALE}
|
|
||||||
export LC_ALL=${TARGET_LOCALE}
|
|
||||||
}
|
|
||||||
|
|
||||||
install_unattended() {
|
|
||||||
echo "[INFO] Updating apt cache…"
|
|
||||||
apt update
|
|
||||||
echo "[INFO] Installing unattended-upgrades…"
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt install -y unattended-upgrades
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_unattended() {
|
|
||||||
echo "[INFO] Enabling unattended-upgrades via debconf…"
|
|
||||||
echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' | debconf-set-selections
|
|
||||||
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive unattended-upgrades
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate origins configuration from detected origins
|
|
||||||
write_50_conf_universal() {
|
|
||||||
local origins_config=""
|
|
||||||
for origin in "${DETECTED_ORIGINS[@]}"; do
|
|
||||||
origins_config+=" \"${origin}\";"$'\n'
|
|
||||||
done
|
done
|
||||||
|
|
||||||
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
||||||
// ${OS^}: auto-install regular + security updates (kernels included)
|
// Auto-installed by setup-auto-updates.sh
|
||||||
// Detected origins: ${DETECTED_ORIGINS[*]}
|
// Built from apt-cache policy to handle codename vs suite (stable/testing) seamlessly.
|
||||||
|
|
||||||
Unattended-Upgrade::Allowed-Origins {
|
Unattended-Upgrade::Allowed-Origins {
|
||||||
${origins_config}};
|
${origins_block}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reboot policy
|
||||||
Unattended-Upgrade::Automatic-Reboot "true";
|
Unattended-Upgrade::Automatic-Reboot "true";
|
||||||
Unattended-Upgrade::Automatic-Reboot-Time "${REBOOT_TIME}";
|
Unattended-Upgrade::Automatic-Reboot-Time "${REBOOT_TIME}";
|
||||||
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
|
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
|
||||||
|
|
||||||
|
// Cleanups
|
||||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
||||||
|
|
||||||
Unattended-Upgrade::MinimalSteps "true";
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
write_20_conf() {
|
write_20auto() {
|
||||||
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
||||||
APT::Periodic::Update-Package-Lists "1";
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
APT::Periodic::AutocleanInterval "7";
|
|
||||||
APT::Periodic::Unattended-Upgrade "1";
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Comprehensive validation with detailed error diagnosis
|
enable_timers_if_systemd() {
|
||||||
validate_config() {
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
echo "[INFO] Validating unattended-upgrades configuration..."
|
# Not all containers have systemd; ignore failures.
|
||||||
|
systemctl enable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
|
||||||
local temp_log="/tmp/unattended-upgrades-validation-$.log"
|
|
||||||
|
|
||||||
# Test the configuration
|
|
||||||
if ! timeout 60 unattended-upgrades --dry-run --debug > "$temp_log" 2>&1; then
|
|
||||||
echo "[ERROR] Configuration validation failed (timeout or error)!"
|
|
||||||
echo "[ERROR] Last 20 lines of debug output:"
|
|
||||||
tail -n 20 "$temp_log"
|
|
||||||
rm -f "$temp_log"
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for critical errors
|
|
||||||
local critical_errors=(
|
|
||||||
"not enough values to unpack"
|
|
||||||
"Unable to parse.*Allowed-Origins"
|
|
||||||
"ValueError:"
|
|
||||||
"AttributeError:"
|
|
||||||
"ImportError:"
|
|
||||||
)
|
|
||||||
|
|
||||||
for error_pattern in "${critical_errors[@]}"; do
|
|
||||||
if grep -qi "$error_pattern" "$temp_log"; then
|
|
||||||
echo "[ERROR] Critical configuration error detected:"
|
|
||||||
grep -i "$error_pattern" "$temp_log" || true
|
|
||||||
rm -f "$temp_log"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Analyze the results
|
|
||||||
if grep -q "Allowed origins are:" "$temp_log"; then
|
|
||||||
local allowed_origins
|
|
||||||
allowed_origins=$(grep "Allowed origins are:" "$temp_log" | head -n1)
|
|
||||||
echo "[INFO] $allowed_origins"
|
|
||||||
|
|
||||||
# Convert our expected origins to the debug output format for comparison
|
|
||||||
# "Debian:stable" becomes "o=Debian,a=stable"
|
|
||||||
# "Debian-Security:stable-security" becomes "o=Debian-Security,a=stable-security"
|
|
||||||
local configs_working=0
|
|
||||||
for origin in "${DETECTED_ORIGINS[@]}"; do
|
|
||||||
if [[ "$origin" =~ ^([^:]+):(.+)$ ]]; then
|
|
||||||
local expected_format="o=${BASH_REMATCH[1]},a=${BASH_REMATCH[2]}"
|
|
||||||
if [[ "$allowed_origins" == *"$expected_format"* ]]; then
|
|
||||||
((configs_working++))
|
|
||||||
echo "[DEBUG] Found expected origin: $expected_format"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $configs_working -eq 0 ]]; then
|
|
||||||
echo "[ERROR] None of the configured origins are being recognized!"
|
|
||||||
echo "[ERROR] Expected origins (in config format): ${DETECTED_ORIGINS[*]}"
|
|
||||||
echo "[ERROR] Debug log may show format differences - check /tmp/unattended-upgrades-test.log"
|
|
||||||
rm -f "$temp_log"
|
|
||||||
return 1
|
|
||||||
elif [[ $configs_working -lt ${#DETECTED_ORIGINS[@]} ]]; then
|
|
||||||
echo "[WARN] Only $configs_working/${#DETECTED_ORIGINS[@]} origins recognized, but continuing..."
|
|
||||||
else
|
|
||||||
echo "[OK] All $configs_working configured origins are being recognized"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for blocking issues
|
|
||||||
if grep -q "No packages found that can be upgraded unattended" "$temp_log"; then
|
|
||||||
if grep -q "Marking not allowed.*-32768" "$temp_log"; then
|
|
||||||
# Count how many packages are being blocked
|
|
||||||
local blocked_count
|
|
||||||
blocked_count=$(grep -c "Marking not allowed.*-32768" "$temp_log" || echo "0")
|
|
||||||
echo "[INFO] $blocked_count non-official packages are correctly being blocked"
|
|
||||||
echo "[OK] Configuration is working - no official packages need updates"
|
|
||||||
else
|
|
||||||
echo "[OK] No packages need updating - configuration appears correct"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[OK] Configuration validation passed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Save full log for debugging
|
|
||||||
cp "$temp_log" "/tmp/unattended-upgrades-test.log"
|
|
||||||
rm -f "$temp_log"
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dry_run_test() {
|
validate_with_dryrun() {
|
||||||
echo "[INFO] Testing unattended-upgrades configuration…"
|
echo "[INFO] Validating unattended-upgrades with a dry run…"
|
||||||
|
local log="/tmp/unattended-upgrades-dryrun.$$"
|
||||||
if validate_config; then
|
if ! timeout 90 unattended-upgrades --dry-run --debug >"$log" 2>&1; then
|
||||||
echo "[OK] Configuration test passed"
|
echo "[WARN] Dry run timed out or failed; see $log"
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "[ERROR] Configuration test failed - see details above"
|
|
||||||
echo "[INFO] Full debug log saved to /tmp/unattended-upgrades-test.log"
|
|
||||||
echo "[INFO] You can retry with: sudo unattended-upgrades --dry-run --debug"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Show recognized allowed origins summary
|
||||||
|
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
|
||||||
|
echo "[ERROR] Parsing error detected; see $log"
|
||||||
|
return 1
|
||||||
|
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() {
|
||||||
echo
|
echo
|
||||||
echo "[INFO] Service and timer status:"
|
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
|
systemctl list-timers --all | grep -E 'apt-(daily|daily-upgrade)\.timer' || true
|
||||||
echo
|
else
|
||||||
echo "[INFO] Configuration files:"
|
echo "[INFO] systemd not present; relying on APT::Periodic via cron."
|
||||||
ls -la /etc/apt/apt.conf.d/{20auto-upgrades,50unattended-upgrades}
|
|
||||||
echo
|
|
||||||
echo "[INFO] Detected configuration summary:"
|
|
||||||
echo " OS: $OS $CODENAME"
|
|
||||||
echo " Origins: ${DETECTED_ORIGINS[*]}"
|
|
||||||
if [[ "$OS" == "debian" ]]; then
|
|
||||||
echo " Main archive: $MAIN_ARCHIVE"
|
|
||||||
[[ -n "$UPDATES_ARCHIVE" ]] && echo " Updates: $UPDATES_ARCHIVE"
|
|
||||||
[[ -n "$SECURITY_ARCHIVE" ]] && echo " Security: $SECURITY_ARCHIVE"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_self_delete() {
|
|
||||||
echo
|
|
||||||
read -r -p "Remove this script? [Y/n] " confirm
|
|
||||||
case "$confirm" in
|
|
||||||
[nN]*) echo "[INFO] Script not removed." ;;
|
|
||||||
*) echo "[INFO] Removing script: $0"; rm -- "$0" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo "[INFO] Universal Unattended-Upgrades Setup Script"
|
echo "[INFO] Unattended-upgrades configurator (Debian/Ubuntu)"
|
||||||
echo "[INFO] Supports mixed Debian/Ubuntu environments with auto-detection"
|
|
||||||
echo
|
|
||||||
|
|
||||||
require_root
|
require_root
|
||||||
detect_os
|
detect_os
|
||||||
detect_repositories
|
apt_refresh_and_install
|
||||||
fix_locale
|
extract_policy_pairs
|
||||||
install_unattended
|
build_allowed_origins
|
||||||
enable_unattended
|
write_50unattended
|
||||||
|
write_20auto
|
||||||
echo "[INFO] Writing configuration files…"
|
enable_timers_if_systemd
|
||||||
write_50_conf_universal
|
validate_with_dryrun
|
||||||
write_20_conf
|
|
||||||
|
|
||||||
if ! dry_run_test; then
|
|
||||||
echo "[ERROR] Setup completed but configuration validation failed"
|
|
||||||
echo "[ERROR] Manual review of /etc/apt/apt.conf.d/50unattended-upgrades may be needed"
|
|
||||||
show_status
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
show_status
|
show_status
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "[OK] ✅ Unattended updates configured successfully!"
|
echo "[OK] Unattended updates configured. Regular + security updates will apply automatically; reboot at ${REBOOT_TIME} if needed."
|
||||||
echo "[OK] Security + regular updates enabled, reboot @ ${REBOOT_TIME} if needed"
|
|
||||||
echo "[OK] Compatible with current and future Debian/Ubuntu releases"
|
|
||||||
|
|
||||||
prompt_self_delete
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
Reference in New Issue
Block a user