#!/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 `; 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: [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 --yes restore a backup (DESTRUCTIVE) # ./ha-maintenance.sh --app start|stop|restart|update --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'