- runbooks/home-assistant-maintenance.md: access pattern (sudo -n -i ha), command reference verified on host, recovery ops, families not scripted, docs-vs-CLI discrepancies - runbooks/scripts/ha-maintenance.sh: read-only health/logs + --yes-gated update/restart/rebuild/rollback/reboot/backup/restore/app modes - AGENTS.md: register runbook in table; hosts/hass: access pattern + link
181 lines
5.8 KiB
Bash
Executable File
181 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# ha-maintenance.sh — Home Assistant maintenance via the `ha` supervisor CLI.
|
|
#
|
|
# Target : hass.windy.lan (HAOS / Core), SSH user `hassio`.
|
|
# Access : non-interactive `sudo -n -i ha <cmd>`; the root login environment
|
|
# carries the supervisor API token (interactive login loads
|
|
# ~hassio/.zprofile which runs `exec sudo -i`). The whole remote
|
|
# procedure runs in ONE `sudo -n -i bash -s` login so the SSH & Web
|
|
# Terminal MOTD is printed only once, then stripped locally.
|
|
# See hosts/hass.windy.lan.md and runbooks/home-assistant-maintenance.md.
|
|
#
|
|
# Usage:
|
|
# ./ha-maintenance.sh read-only health snapshot
|
|
# ./ha-maintenance.sh --check-config validate core configuration
|
|
# ./ha-maintenance.sh --logs core|supervisor|host|apps:<slug> [N]
|
|
# ./ha-maintenance.sh --update --yes refresh + update core(--backup)/supervisor/os
|
|
# ./ha-maintenance.sh --restart-core [--safe-mode] --yes
|
|
# ./ha-maintenance.sh --rebuild-core --yes rebuild core image (after options change)
|
|
# ./ha-maintenance.sh --rollback-os --yes boot the previous OS slot (A/B rollback)
|
|
# ./ha-maintenance.sh --backup [NAME] --yes full backup (optionally named)
|
|
# ./ha-maintenance.sh --restore <slug> --yes restore a backup (DESTRUCTIVE)
|
|
# ./ha-maintenance.sh --app start|stop|restart|update <slug> --yes
|
|
#
|
|
# Environment: HA_HOST (default hass.windy.lan), HA_SSH_USER (default hassio).
|
|
# Mutating actions always require --yes; this script never prompts.
|
|
|
|
set -uo pipefail
|
|
|
|
HOST="${HA_HOST:-hass.windy.lan}"
|
|
SSH_USER="${HA_SSH_USER:-hassio}"
|
|
|
|
MODE="health"
|
|
TARGET="core"
|
|
LINES=100
|
|
SAFE_MODE=0
|
|
CONFIRM=0
|
|
ARG1=""
|
|
ARG2=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--check-config) MODE="check-config" ;;
|
|
--logs)
|
|
MODE="logs"
|
|
if [ $# -gt 1 ]; then TARGET="$2"; shift; fi
|
|
if [ $# -gt 1 ] && [[ "$2" =~ ^[0-9]+$ ]]; then LINES="$2"; shift; fi
|
|
;;
|
|
--update) MODE="update" ;;
|
|
--restart-core) MODE="restart-core" ;;
|
|
--safe-mode) SAFE_MODE=1 ;;
|
|
--rebuild-core) MODE="rebuild-core" ;;
|
|
--rollback-os) MODE="rollback-os" ;;
|
|
--reboot) MODE="reboot" ;;
|
|
--backup)
|
|
MODE="backup"
|
|
if [ $# -gt 1 ] && [[ "$2" != --* ]]; then ARG1="$2"; shift; fi
|
|
;;
|
|
--restore)
|
|
MODE="restore"
|
|
if [ $# -gt 1 ] && [[ "$2" != --* ]]; then ARG1="$2"; shift
|
|
else echo "ha-maintenance: --restore needs a backup slug" >&2; exit 2; fi
|
|
;;
|
|
--app)
|
|
MODE="app"
|
|
if [ $# -gt 1 ] && [[ "$2" != --* ]]; then ARG1="$2"; shift; fi
|
|
if [ $# -gt 1 ] && [[ "$2" != --* ]]; then ARG2="$2"; shift; fi
|
|
;;
|
|
--yes) CONFIRM=1 ;;
|
|
*)
|
|
echo "ha-maintenance: unknown option '$1'" >&2
|
|
echo "usage: $0 [--check-config|--logs TARGET [N]|--update|--restart-core [--safe-mode]|--rebuild-core|--rollback-os|--reboot|--backup [NAME]|--restore SLUG|--app ACTION SLUG] [--yes]" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "$MODE" = "app" ]; then
|
|
case "$ARG1" in
|
|
start|stop|restart|update) ;;
|
|
*) echo "ha-maintenance: --app action must be start|stop|restart|update (got '$ARG1')" >&2; exit 2 ;;
|
|
esac
|
|
if [ -z "$ARG2" ]; then
|
|
echo "ha-maintenance: --app needs an app slug, e.g. --app restart core_mosquitto" >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
case "$MODE" in
|
|
update|restart-core|rebuild-core|rollback-os|reboot|backup|restore|app)
|
|
if [ "$CONFIRM" -ne 1 ]; then
|
|
echo "ha-maintenance: refusing '$MODE' without --yes (mutating action)" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
REMOTE_SCRIPT=$(cat <<'EOF'
|
|
set -u
|
|
ha() { command ha --no-progress "$@"; }
|
|
|
|
case "$1" in
|
|
health)
|
|
printf '\n=== CORE ===\n'
|
|
ha core info
|
|
printf '\n=== SUPERVISOR ===\n'
|
|
ha supervisor info | grep -E '^(healthy|supported|version|version_latest|update_available|auto_update|timezone|logging):'
|
|
printf '\n=== ADDON STATES ===\n'
|
|
ha supervisor info | grep -E '^ (name|state):'
|
|
printf '\n=== OS ===\n'
|
|
ha os info
|
|
printf '\n=== HOST ===\n'
|
|
ha host info
|
|
printf '\n=== NETWORK ===\n'
|
|
ha network info | grep -E '^(address|ip_address|supervisor_internet|vlan|wifi):'
|
|
printf '\n=== AVAILABLE UPDATES ===\n'
|
|
ha available-updates
|
|
printf '\n=== JOBS ===\n'
|
|
ha jobs info
|
|
printf '\n=== RESOLUTION ===\n'
|
|
ha resolution info
|
|
printf '\n=== CORE STATS ===\n'
|
|
ha core stats
|
|
;;
|
|
check-config)
|
|
ha core check
|
|
;;
|
|
logs)
|
|
case "$2" in
|
|
core) ha core logs -n "$3" ;;
|
|
supervisor) ha supervisor logs -n "$3" ;;
|
|
host) ha host logs -n "$3" ;;
|
|
apps:*) ha apps logs -n "$3" "${2#apps:}" ;;
|
|
*) echo "unknown log target: $2" >&2; exit 2 ;;
|
|
esac
|
|
;;
|
|
update)
|
|
ha refresh-updates
|
|
printf '\n=== UPDATE CORE (with backup) ===\n'; ha core update --backup
|
|
printf '\n=== UPDATE SUPERVISOR ===\n'; ha supervisor update
|
|
printf '\n=== UPDATE OS ===\n'; ha os update
|
|
printf '\n=== PENDING AFTER ===\n'; ha available-updates
|
|
;;
|
|
restart-core)
|
|
if [ "$4" = "1" ]; then ha core restart --safe-mode; else ha core restart; fi
|
|
;;
|
|
rebuild-core)
|
|
ha core rebuild
|
|
;;
|
|
rollback-os)
|
|
ha os boot-slot other
|
|
;;
|
|
reboot)
|
|
ha host reboot
|
|
;;
|
|
backup)
|
|
if [ -n "${2:-}" ]; then ha backups new --name "$2"; else ha backups new; fi
|
|
;;
|
|
restore)
|
|
echo "WARNING: restoring backup $2"
|
|
ha backups restore "$2"
|
|
;;
|
|
app)
|
|
ha apps "$2" "$3"
|
|
;;
|
|
esac
|
|
EOF
|
|
)
|
|
|
|
if [ "$MODE" = "logs" ]; then
|
|
ARG1="$TARGET"
|
|
ARG2="$LINES"
|
|
fi
|
|
|
|
# One remote login shell (MOTD printed once), then drop the MOTD locally.
|
|
printf '%s\n' "$REMOTE_SCRIPT" \
|
|
| ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_USER}@${HOST}" \
|
|
'sudo -n -i bash -s' "$MODE" "$ARG1" "$ARG2" "$SAFE_MODE" 2>/dev/null \
|
|
| awk 'BEGIN{on=0} /^System is ready/{on=1; next} on'
|