ha skill
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
collect_ssh_evidence.sh --credentials /path/to/credentials.env [--output /path/to/output]
|
||||
|
||||
Collecte en lecture seule via SSH :
|
||||
- informations système (CPU, mémoire, swap, disques)
|
||||
- indices sur la version Home Assistant
|
||||
- extraits de logs si accessibles
|
||||
- fichiers de configuration principaux si accessibles
|
||||
|
||||
Le script n'effectue aucune modification distante.
|
||||
USAGE
|
||||
}
|
||||
|
||||
credentials_file=""
|
||||
output_dir=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--credentials)
|
||||
credentials_file="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
output_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Argument inconnu: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$credentials_file" ]]; then
|
||||
echo "--credentials est obligatoire" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$credentials_file" ]]; then
|
||||
echo "Fichier d'identifiants introuvable: $credentials_file" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$credentials_file"
|
||||
|
||||
: "${HA_SSH_HOST:?HA_SSH_HOST manquant}"
|
||||
: "${HA_SSH_USER:?HA_SSH_USER manquant}"
|
||||
HA_SSH_PORT="${HA_SSH_PORT:-22}"
|
||||
HA_SSH_PASSWORD="${HA_SSH_PASSWORD:-}"
|
||||
HA_SSH_KEY_PATH="${HA_SSH_KEY_PATH:-}"
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
if [[ -z "$output_dir" ]]; then
|
||||
output_dir="./ha-ssh-evidence-$timestamp"
|
||||
fi
|
||||
mkdir -p "$output_dir"/{system,home-assistant,config}
|
||||
chmod 700 "$output_dir"
|
||||
|
||||
ssh_base=(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT")
|
||||
scp_base=(scp -P "$HA_SSH_PORT" -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
if [[ -n "$HA_SSH_KEY_PATH" ]]; then
|
||||
ssh_base+=(-i "$HA_SSH_KEY_PATH")
|
||||
scp_base+=(-i "$HA_SSH_KEY_PATH")
|
||||
fi
|
||||
|
||||
run_ssh() {
|
||||
local remote_cmd="$1"
|
||||
if [[ -n "$HA_SSH_PASSWORD" && -z "$HA_SSH_KEY_PATH" ]]; then
|
||||
if ! command -v sshpass >/dev/null 2>&1; then
|
||||
echo "sshpass est requis pour l'authentification par mot de passe, mais il est absent." >&2
|
||||
return 127
|
||||
fi
|
||||
sshpass -p "$HA_SSH_PASSWORD" ssh -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT" "$HA_SSH_USER@$HA_SSH_HOST" "$remote_cmd"
|
||||
else
|
||||
"${ssh_base[@]}" "$HA_SSH_USER@$HA_SSH_HOST" "$remote_cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_remote_file() {
|
||||
local remote_path="$1"
|
||||
local local_path="$2"
|
||||
run_ssh "if [ -r '$remote_path' ]; then cat '$remote_path'; fi" > "$local_path" 2>/dev/null || true
|
||||
if [[ ! -s "$local_path" ]]; then
|
||||
rm -f "$local_path"
|
||||
fi
|
||||
}
|
||||
|
||||
collect_interactive_core_logs() {
|
||||
local lines="$1"
|
||||
local raw_path="$2"
|
||||
local clean_path="$3"
|
||||
local start_marker="__HA_LOG_START__"
|
||||
local end_marker="__HA_LOG_END__"
|
||||
|
||||
if [[ -n "$HA_SSH_PASSWORD" && -z "$HA_SSH_KEY_PATH" ]]; then
|
||||
if ! command -v sshpass >/dev/null 2>&1; then
|
||||
return 127
|
||||
fi
|
||||
printf 'echo %s\nha core logs --lines %s\necho %s\nexit\n' "$start_marker" "$lines" "$end_marker" \
|
||||
| sshpass -p "$HA_SSH_PASSWORD" ssh -tt -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT" "$HA_SSH_USER@$HA_SSH_HOST" \
|
||||
> "$raw_path" 2>&1 || true
|
||||
else
|
||||
printf 'echo %s\nha core logs --lines %s\necho %s\nexit\n' "$start_marker" "$lines" "$end_marker" \
|
||||
| "${ssh_base[@]}" -tt "$HA_SSH_USER@$HA_SSH_HOST" \
|
||||
> "$raw_path" 2>&1 || true
|
||||
fi
|
||||
|
||||
python3 - "$raw_path" "$clean_path" "$start_marker" "$end_marker" <<'PY2'
|
||||
import re, sys
|
||||
from pathlib import Path
|
||||
raw_path, clean_path, start_marker, end_marker = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3], sys.argv[4]
|
||||
text = raw_path.read_text(errors='ignore')
|
||||
text = re.sub(r'\x1B\][^\x07]*(?:\x07|\x1b\\)', '', text)
|
||||
text = re.sub(r'\x1B\[[0-9;?]*[ -/]*[@-~]', '', text)
|
||||
text = text.replace('\r', '')
|
||||
lines = text.splitlines()
|
||||
# The command script itself is echoed before execution. Keep the *last* start marker,
|
||||
# which is the marker printed by the shell, then the last matching end marker after it.
|
||||
start_idx = max((i for i,l in enumerate(lines) if start_marker in l), default=-1)
|
||||
end_idx = max((i for i,l in enumerate(lines) if end_marker in l and i > start_idx), default=-1)
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
body = lines[start_idx + 1:end_idx]
|
||||
clean = []
|
||||
for line in body:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
clean.append('')
|
||||
continue
|
||||
if 'ha core logs --lines' in stripped:
|
||||
continue
|
||||
if stripped == start_marker or stripped == end_marker or stripped == 'exit':
|
||||
continue
|
||||
if stripped.startswith('➜') or stripped == '#':
|
||||
continue
|
||||
clean.append(line)
|
||||
clean_path.write_text('\n'.join(clean).strip() + '\n')
|
||||
PY2
|
||||
}
|
||||
|
||||
{
|
||||
echo "collected_at_utc=$timestamp"
|
||||
echo "ssh_host=$HA_SSH_HOST"
|
||||
echo "ssh_port=$HA_SSH_PORT"
|
||||
echo "ssh_user=$HA_SSH_USER"
|
||||
echo "install_type_hint_source=ssh_shell_only"
|
||||
} > "$output_dir/collection.meta"
|
||||
|
||||
run_ssh 'uname -a || true' > "$output_dir/system/uname.txt" || true
|
||||
run_ssh 'cat /etc/os-release 2>/dev/null || true' > "$output_dir/system/os-release.txt" || true
|
||||
run_ssh 'if [ -d /usr/share/hassio ] || [ -e /data/supervisor ]; then echo home_assistant_os_or_supervised; else echo unknown; fi' > "$output_dir/home-assistant/install-type-hint.txt" || true
|
||||
run_ssh 'nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || true' > "$output_dir/system/cpu-count.txt" || true
|
||||
run_ssh 'lscpu 2>/dev/null || cat /proc/cpuinfo 2>/dev/null || true' > "$output_dir/system/cpu.txt" || true
|
||||
run_ssh 'free -h 2>/dev/null || cat /proc/meminfo 2>/dev/null || true' > "$output_dir/system/memory.txt" || true
|
||||
run_ssh 'df -hT 2>/dev/null || df -h 2>/dev/null || true' > "$output_dir/system/disks.txt" || true
|
||||
run_ssh 'swapon --show 2>/dev/null || true' > "$output_dir/system/swap.txt" || true
|
||||
|
||||
run_ssh '
|
||||
if command -v ha >/dev/null 2>&1; then
|
||||
ha core info 2>/dev/null || true
|
||||
fi
|
||||
' > "$output_dir/home-assistant/ha-core-info.txt" || true
|
||||
|
||||
run_ssh '
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null || true
|
||||
fi
|
||||
' > "$output_dir/home-assistant/docker-ps.txt" || true
|
||||
|
||||
run_ssh '
|
||||
for f in \
|
||||
/config/.HA_VERSION \
|
||||
/usr/share/hassio/homeassistant/.HA_VERSION \
|
||||
/home/homeassistant/.homeassistant/.HA_VERSION; do
|
||||
if [ -r "$f" ]; then
|
||||
echo "=== $f ==="
|
||||
cat "$f"
|
||||
fi
|
||||
done
|
||||
' > "$output_dir/home-assistant/version-files.txt" || true
|
||||
|
||||
run_ssh '
|
||||
for f in \
|
||||
/config/home-assistant.log \
|
||||
/usr/share/hassio/homeassistant/home-assistant.log \
|
||||
/home/homeassistant/.homeassistant/home-assistant.log; do
|
||||
if [ -r "$f" ] && [ -s "$f" ]; then
|
||||
echo "=== $f ==="
|
||||
tail -n 400 "$f"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
' > "$output_dir/home-assistant/home-assistant-log-tail.txt" || true
|
||||
|
||||
run_ssh '
|
||||
echo "current_log_exists=$( [ -r /config/home-assistant.log ] && echo yes || echo no )"
|
||||
echo "haos_default_log_file_expected=no"
|
||||
for f in /config/home-assistant.log /config/home-assistant.log.1 /config/home-assistant.log.fault; do
|
||||
if [ -e "$f" ]; then
|
||||
size=$(wc -c < "$f" 2>/dev/null || echo unknown)
|
||||
echo "$f bytes=$size"
|
||||
fi
|
||||
done
|
||||
' > "$output_dir/home-assistant/log-status.txt" || true
|
||||
|
||||
run_ssh 'ha core logs --lines 40 2>&1 || true' > "$output_dir/home-assistant/ha-core-logs-attempt.txt" || true
|
||||
|
||||
if grep -q '401: Unauthorized' "$output_dir/home-assistant/ha-core-logs-attempt.txt" 2>/dev/null; then
|
||||
collect_interactive_core_logs 120 "$output_dir/home-assistant/ha-core-logs-interactive.raw.txt" "$output_dir/home-assistant/ha-core-logs-interactive.txt" || true
|
||||
fi
|
||||
|
||||
if [[ -s "$output_dir/home-assistant/ha-core-logs-interactive.txt" ]] && command -v rtk >/dev/null 2>&1; then
|
||||
rtk log "$output_dir/home-assistant/ha-core-logs-interactive.txt" > "$output_dir/home-assistant/ha-core-logs-interactive.compact.txt" || true
|
||||
fi
|
||||
|
||||
if [[ -s "$output_dir/home-assistant/home-assistant-log-tail.txt" ]] && command -v rtk >/dev/null 2>&1; then
|
||||
rtk log "$output_dir/home-assistant/home-assistant-log-tail.txt" > "$output_dir/home-assistant/home-assistant-log-compact.txt" || true
|
||||
fi
|
||||
|
||||
run_ssh '
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
journalctl -u home-assistant@homeassistant.service -n 300 --no-pager 2>/dev/null || true
|
||||
fi
|
||||
' > "$output_dir/home-assistant/journal-home-assistant.txt" || true
|
||||
|
||||
for remote in \
|
||||
/config/configuration.yaml \
|
||||
/config/automations.yaml \
|
||||
/config/scripts.yaml \
|
||||
/config/scenes.yaml \
|
||||
/usr/share/hassio/homeassistant/configuration.yaml \
|
||||
/home/homeassistant/.homeassistant/configuration.yaml; do
|
||||
name="$(echo "$remote" | sed 's#^/##; s#/#__#g')"
|
||||
copy_remote_file "$remote" "$output_dir/config/$name" || true
|
||||
done
|
||||
|
||||
run_ssh '
|
||||
for d in /config/.storage /usr/share/hassio/homeassistant/.storage /home/homeassistant/.homeassistant/.storage; do
|
||||
if [ -d "$d" ]; then
|
||||
echo "=== $d ==="
|
||||
for f in "$d"/*; do
|
||||
[ -f "$f" ] && basename "$f"
|
||||
done | sort
|
||||
fi
|
||||
done
|
||||
' > "$output_dir/config/storage-index.txt" || true
|
||||
|
||||
for remote in \
|
||||
/config/.storage/lovelace.lovelace \
|
||||
/config/.storage/lovelace_dashboards \
|
||||
/config/.storage/lovelace_resources \
|
||||
/config/.storage/repairs.issue_registry \
|
||||
/config/.storage/core.entity_registry \
|
||||
/config/.storage/core.config_entries; do
|
||||
name="$(echo "$remote" | sed 's#^/##; s#/#__#g')"
|
||||
copy_remote_file "$remote" "$output_dir/config/$name" || true
|
||||
done
|
||||
|
||||
echo "Collecte terminée: $output_dir"
|
||||
Reference in New Issue
Block a user