feat(sys_autoupdate): notify on pending kernel reboot

Detect drift between the running kernel and the newest installed one
(Arch: /usr/lib/modules vs uname -r; Debian: /var/run/reboot-required)
and push a weekly ntfy reminder until the host is rebooted.

Also bundles the in-flight ignore-packages feature and hostname
refactor in the autoupdate script.
This commit is contained in:
Clément Désiles
2026-09-05 16:59:49 +02:00
parent 24502f543f
commit b94743567f
4 changed files with 136 additions and 10 deletions
@@ -13,6 +13,7 @@ PODMAN_PROJECTS_DIR="{{ sys_autoupdate_podman_projects_dir }}"
PODMAN_USER="{{ ansible_user }}"
PODMAN_PRUNE_UNTIL="{{ sys_autoupdate_podman_prune_until }}"
HOSTNAME="{{ ansible_facts['hostname'] }}"
DISTRO_NAME="{{ sys_autoupdate_distro_name }}"
OS_KEY="{{ sys_autoupdate_os_key }}"
ALLOW_DOWNGRADE="{{ sys_autoupdate_allow_downgrade | lower }}"
@@ -20,6 +21,8 @@ HAS_NEWS_CHECK="{{ sys_autoupdate_has_news_check | lower }}"
CHECK_NEWS="{{ sys_autoupdate_check_news | lower }}"
NEWS_HOURS="{{ sys_autoupdate_news_hours }}"
NEWS_URL="{{ sys_autoupdate_news_url }}"
IGNORE_PACKAGES="{{ sys_autoupdate_ignore_packages | join(',') }}"
REBOOT_NOTIFY_WEEKDAY="{{ sys_autoupdate_reboot_notify_weekday }}"
{% raw %}
log() {
@@ -53,10 +56,20 @@ check_pending_updates() {
archlinux)
# checkupdates (pacman-contrib) exits 2 when no updates are pending;
# swallow non-zero so set -e doesn't abort the script.
checkupdates 2>/dev/null || true
# Honor ignored packages by filtering them out of the pending list.
local raw
raw=$(checkupdates 2>/dev/null || true)
if [[ -n "$IGNORE_PACKAGES" && -n "$raw" ]]; then
local ignore_regex
ignore_regex="^($(echo "$IGNORE_PACKAGES" | tr ',' '|')) "
echo "$raw" | grep -Ev "$ignore_regex" || true
else
echo "$raw"
fi
;;
debian)
apt-get update -qq >/dev/null 2>&1
# Held packages are automatically excluded from dist-upgrade simulation.
apt-get -s dist-upgrade 2>/dev/null | grep '^Inst ' || true
;;
*)
@@ -69,13 +82,18 @@ check_pending_updates() {
apply_upgrade() {
case "$OS_KEY" in
archlinux)
local -a pacman_args=(-Syu --noconfirm)
if [[ "$ALLOW_DOWNGRADE" == "true" ]]; then
pacman -Syu --noconfirm --allow-downgrade
else
pacman -Syu --noconfirm
pacman_args+=(--allow-downgrade)
fi
if [[ -n "$IGNORE_PACKAGES" ]]; then
pacman_args+=(--ignore="$IGNORE_PACKAGES")
fi
pacman "${pacman_args[@]}"
;;
debian)
# Holds are reconciled by Ansible (apt-mark hold/unhold) before each
# run. dist-upgrade automatically skips held packages.
local -a apt_args=(dist-upgrade -y)
if [[ "$ALLOW_DOWNGRADE" == "true" ]]; then
apt_args+=(--allow-downgrades)
@@ -137,7 +155,7 @@ check_distro_news() {
log "Recent $DISTRO_NAME news detected (${#recent_titles[@]} items within ${hours}h):"
log "$news_list"
send_notification \
"$(hostname): Manual Intervention Required" \
"$HOSTNAME: Manual Intervention Required" \
"Recent $DISTRO_NAME news (last ${hours}h). Review before updating.\n\n${news_list}\n\n$NEWS_URL" \
"warning" \
"high"
@@ -165,14 +183,14 @@ update_system() {
if apply_upgrade 2>&1; then
log "System updates completed successfully ($update_count packages)"
send_notification \
"$(hostname): System Update Success" \
"$HOSTNAME: System Update Success" \
"$update_count packages updated" \
"white_check_mark" \
"default"
else
log "ERROR: System update failed"
send_notification \
"$(hostname): System Update FAILED" \
"$HOSTNAME: System Update FAILED" \
"Update failed. Check: journalctl -u sys-autoupdate.service" \
"x" \
"urgent"
@@ -180,6 +198,45 @@ update_system() {
fi
}
# Detect drift between the running kernel and the newest installed one.
# Fires a notification when a reboot is pending, so a long-lived host isn't
# silently left on a stale kernel.
check_kernel_reboot() {
# Only notify once a week (1=Mon .. 7=Sun) to avoid daily nagging.
if [[ "$(date +%u)" != "$REBOOT_NOTIFY_WEEKDAY" ]]; then
return 0
fi
local detail=""
case "$OS_KEY" in
archlinux)
local running latest
running=$(uname -r)
latest=$(ls /usr/lib/modules/ 2>/dev/null | sort -V | tail -1 || echo "")
if [[ -n "$latest" && "$running" != "$latest" ]]; then
detail="Running kernel $running is older than installed $latest."
fi
;;
debian)
if [[ -f /var/run/reboot-required ]]; then
detail=$(cat /var/run/reboot-required)
fi
;;
esac
if [[ -n "$detail" ]]; then
log "Kernel drift detected: $detail"
send_notification \
"$HOSTNAME: Reboot Required" \
"${detail}\n\nA reboot is needed to load the new kernel." \
"arrows_counterclockwise" \
"high"
else
log "Running kernel is up to date (no reboot pending)"
fi
}
# Run a command as the rootless Podman user with a proper login environment.
# `runuser -l` invokes a login shell, so XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS,
# PATH, and HOME are set correctly for rootless Podman state under
@@ -223,7 +280,7 @@ update_podman_images() {
local pull_tail
pull_tail=$(echo "$pull_output" | tail -n 5)
send_notification \
"$(hostname): Podman Pull Failed ($project_name)" \
"$HOSTNAME: Podman Pull Failed ($project_name)" \
"Failed to pull images.\n\nLast output:\n$pull_tail\n\nFull log: journalctl -u sys-autoupdate.service" \
"x" \
"high"
@@ -260,7 +317,7 @@ update_podman_images() {
up_tail=$(echo "$up_output" | tail -n 5)
send_notification \
"$(hostname): Podman Restart FAILED ($project_name)" \
"$HOSTNAME: Podman Restart FAILED ($project_name)" \
"up -d exit=$up_status, $running_count container(s) running.\n\nLast output:\n$up_tail\n\nFull log: journalctl -u sys-autoupdate.service" \
"x" \
"urgent"
@@ -283,7 +340,7 @@ update_podman_images() {
service_list=$(printf '%s, ' "${updated_services[@]}")
service_list="${service_list%, }"
send_notification \
"$(hostname): Podman Images Updated" \
"$HOSTNAME: Podman Images Updated" \
"${#updated_services[@]} project(s) updated: $service_list" \
"whale" \
"default"
@@ -309,6 +366,8 @@ main() {
update_podman_images
fi
check_kernel_reboot
log "=== Auto-update run complete ==="
}