Enhance host status tracking by parsing Ansible PLAY RECAP to update host reachability and last_seen timestamps after health-check playbook executions, add inventory group resolution to host API responses, and trigger automatic data refresh in dashboard after task completion to reflect updated host health indicators
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled

This commit is contained in:
2025-12-22 10:43:17 -05:00
parent 6c51fb5c75
commit 46823eb42d
15 changed files with 1161 additions and 8 deletions
+6
View File
@@ -998,6 +998,12 @@ class DashboardManager {
// Rafraîchir les logs de tâches pour voir la tâche terminée
this.refreshTaskLogs();
// Rafraîchir aussi les hosts/métriques : un health-check met à jour status/last_seen côté backend
// (sinon l'indicateur de santé peut rester sur l'ancien état)
this.loadAllData().catch((e) => {
console.error('Erreur rafraîchissement données après fin de tâche:', e);
});
// Notification
const status = taskData.status || 'completed';
+96
View File
@@ -7,6 +7,7 @@ import subprocess
from datetime import datetime, timezone
from time import perf_counter
from typing import Optional, Dict, Any
import re
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,6 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.dependencies import get_db, verify_api_key
from app.crud.task import TaskRepository
from app.crud.host import HostRepository
from app.schemas.ansible import AnsibleExecutionRequest, AdHocCommandRequest, AdHocCommandResult, BootstrapRequest
from app.schemas.task_api import Task
from app.schemas.common import CommandResult
@@ -35,6 +37,50 @@ router = APIRouter()
task_log_service = TaskLogService(settings.tasks_logs_dir)
def _parse_ansible_play_recap(stdout: str) -> dict[str, dict[str, int]]:
if not stdout:
return {}
recap: dict[str, dict[str, int]] = {}
pattern = re.compile(
r"^\s*(?P<host>[^\s:]+)\s*:\s*.*?unreachable=(?P<unreachable>\d+)\s+failed=(?P<failed>\d+)\b",
re.IGNORECASE,
)
for line in stdout.splitlines():
m = pattern.match(line)
if not m:
continue
host = m.group("host")
recap[host] = {
"unreachable": int(m.group("unreachable")),
"failed": int(m.group("failed")),
}
return recap
async def _resolve_host_from_recap_name(
host_repo: HostRepository,
recap_host_name: str,
inventory_alias_to_host: dict[str, str],
):
host = await host_repo.get_by_name(recap_host_name)
if not host:
host = await host_repo.get_by_ip(recap_host_name)
if host:
return host
mapped = inventory_alias_to_host.get(recap_host_name)
if mapped:
host = await host_repo.get_by_ip(mapped)
if not host:
host = await host_repo.get_by_name(mapped)
if host:
return host
return None
@router.get("/playbooks")
async def get_ansible_playbooks(
target: Optional[str] = None,
@@ -327,6 +373,56 @@ async def execute_ansible_playbook(
error_message=task.error,
result_data={"output": result.get("stdout", "")[:5000]}
)
# Si c'est un health-check, mettre à jour le statut/last_seen des hosts impliqués
if request.playbook and "health-check" in request.playbook and request.target:
try:
host_repo = HostRepository(db_session)
now = datetime.now(timezone.utc)
inventory_alias_to_host: dict[str, str] = {}
try:
for inv_host in ansible_service.get_hosts_from_inventory():
if inv_host.name and inv_host.ansible_host:
inventory_alias_to_host[inv_host.name] = inv_host.ansible_host
except Exception:
inventory_alias_to_host = {}
recap = _parse_ansible_play_recap(result.get("stdout", "") or "")
if recap:
for host_name, counters in recap.items():
unreachable = int(counters.get("unreachable", 0) or 0)
reachable = unreachable == 0
status = "online" if reachable else "offline"
host = await _resolve_host_from_recap_name(
host_repo,
host_name,
inventory_alias_to_host,
)
if host:
await host_repo.update(
host,
status=status,
reachable=reachable,
last_seen=now,
)
elif request.target != "all":
host = await _resolve_host_from_recap_name(
host_repo,
request.target,
inventory_alias_to_host,
)
if host:
await host_repo.update(
host,
status="online" if bool(result.get("success")) else "offline",
reachable=bool(result.get("success")),
last_seen=now,
)
except Exception:
pass
await db_session.commit()
# Notification
+37 -5
View File
@@ -18,8 +18,13 @@ from app.services import ansible_service, ws_manager
router = APIRouter()
def _host_to_response(host, bootstrap=None) -> dict:
def _host_to_response(host, bootstrap=None, inventory_groups: Optional[List[str]] = None) -> dict:
"""Convertit un modèle Host DB en réponse API."""
groups: List[str] = [host.ansible_group] if host.ansible_group else []
if inventory_groups:
for g in inventory_groups:
if g and g not in groups:
groups.append(g)
return {
"id": host.id,
"name": host.name,
@@ -28,7 +33,7 @@ def _host_to_response(host, bootstrap=None) -> dict:
"os": "Linux",
"last_seen": host.last_seen,
"created_at": host.created_at,
"groups": [host.ansible_group] if host.ansible_group else [],
"groups": groups,
"bootstrap_ok": bootstrap.status == "success" if bootstrap else False,
"bootstrap_date": bootstrap.last_attempt if bootstrap else None,
}
@@ -62,7 +67,15 @@ async def get_host_by_name(
raise HTTPException(status_code=404, detail=f"Hôte '{host_name}' non trouvé")
bootstrap = await bs_repo.latest_for_host(host.id)
return _host_to_response(host, bootstrap)
inventory_groups: Optional[List[str]] = None
try:
inv_hosts = ansible_service.get_hosts_from_inventory()
inv_match = next((h for h in inv_hosts if h.name == host.name), None)
inventory_groups = inv_match.groups if inv_match else None
except Exception:
inventory_groups = None
return _host_to_response(host, bootstrap, inventory_groups=inventory_groups)
@router.post("/refresh")
@@ -154,7 +167,15 @@ async def get_host(
raise HTTPException(status_code=404, detail="Hôte non trouvé")
bootstrap = await bs_repo.latest_for_host(host.id)
return _host_to_response(host, bootstrap)
inventory_groups: Optional[List[str]] = None
try:
inv_hosts = ansible_service.get_hosts_from_inventory()
inv_match = next((h for h in inv_hosts if h.name == host.name), None)
inventory_groups = inv_match.groups if inv_match else None
except Exception:
inventory_groups = None
return _host_to_response(host, bootstrap, inventory_groups=inventory_groups)
@router.get("")
@@ -198,6 +219,13 @@ async def get_hosts(
})
return fallback_results
inventory_groups_map = {}
try:
for inv_host in ansible_service.get_hosts_from_inventory():
inventory_groups_map[inv_host.name] = inv_host.groups
except Exception:
inventory_groups_map = {}
result = []
for host in hosts:
bootstrap = await bs_repo.latest_for_host(host.id)
@@ -206,7 +234,11 @@ async def get_hosts(
continue
if bootstrap_status == "not_configured" and bootstrap and bootstrap.status == "success":
continue
result.append(_host_to_response(host, bootstrap))
result.append(_host_to_response(
host,
bootstrap,
inventory_groups=inventory_groups_map.get(host.name),
))
return result
+114 -1
View File
@@ -7,6 +7,7 @@ import asyncio
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import re
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,6 +17,7 @@ from app.core.constants import ACTION_PLAYBOOK_MAP, ACTION_DISPLAY_NAMES
from app.core.dependencies import get_db, verify_api_key
from app.crud.task import TaskRepository
from app.crud.log import LogRepository
from app.crud.host import HostRepository
from app.schemas.task_api import TaskRequest
from app.services import ws_manager, db, ansible_service, notification_service
from app.services.task_log_service import TaskLogService
@@ -33,6 +35,56 @@ task_log_service = TaskLogService(settings.tasks_logs_dir)
running_task_handles = {}
def _parse_ansible_play_recap(stdout: str) -> dict[str, dict[str, int]]:
"""Parse Ansible PLAY RECAP to extract per-host unreachable/failed counters.
Returns a mapping host_name -> {"unreachable": int, "failed": int}.
"""
if not stdout:
return {}
# Typical line:
# host1 : ok=10 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
recap: dict[str, dict[str, int]] = {}
pattern = re.compile(
r"^\s*(?P<host>[^\s:]+)\s*:\s*.*?unreachable=(?P<unreachable>\d+)\s+failed=(?P<failed>\d+)\b",
re.IGNORECASE,
)
for line in stdout.splitlines():
m = pattern.match(line)
if not m:
continue
host = m.group("host")
recap[host] = {
"unreachable": int(m.group("unreachable")),
"failed": int(m.group("failed")),
}
return recap
async def _resolve_host_from_recap_name(
host_repo: HostRepository,
recap_host_name: str,
inventory_alias_to_host: dict[str, str],
):
host = await host_repo.get_by_name(recap_host_name)
if not host:
host = await host_repo.get_by_ip(recap_host_name)
if host:
return host
mapped = inventory_alias_to_host.get(recap_host_name)
if mapped:
host = await host_repo.get_by_ip(mapped)
if not host:
host = await host_repo.get_by_name(mapped)
if host:
return host
return None
@router.get("")
async def get_tasks(
limit: int = 100,
@@ -428,7 +480,68 @@ async def _execute_task_playbook(
error_message=mem_task.error,
result_data={"output": result.get("stdout", "")[:5000]}
)
await session.commit()
# Si c'est un health-check ciblé, mettre à jour le statut/last_seen de l'hôte
if playbook and "health-check" in playbook and target:
try:
host_repo = HostRepository(session)
now = datetime.now(timezone.utc)
inventory_alias_to_host: dict[str, str] = {}
try:
for inv_host in ansible_service.get_hosts_from_inventory():
if inv_host.name and inv_host.ansible_host:
inventory_alias_to_host[inv_host.name] = inv_host.ansible_host
except Exception:
inventory_alias_to_host = {}
recap = _parse_ansible_play_recap(result.get("stdout", "") or "")
# If we have a recap, update each host that appears in the execution.
if recap:
for host_name, counters in recap.items():
unreachable = int(counters.get("unreachable", 0) or 0)
failed = int(counters.get("failed", 0) or 0)
# reachable means Ansible could contact the host (unreachable=0)
reachable = unreachable == 0
# status represents connectivity (not task success)
status = "online" if reachable else "offline"
host = await _resolve_host_from_recap_name(
host_repo,
host_name,
inventory_alias_to_host,
)
if host:
await host_repo.update(
host,
status=status,
reachable=reachable,
last_seen=now,
)
# No recap: keep old behavior for targeted host, but avoid touching all hosts blindly.
elif target != "all":
host = await _resolve_host_from_recap_name(
host_repo,
target,
inventory_alias_to_host,
)
if host:
await host_repo.update(
host,
status="online" if bool(success) else "offline",
reachable=bool(success),
last_seen=now,
)
except Exception:
# Ne pas faire échouer la tâche si la MAJ de statut host échoue
pass
await session.commit()
# Sauvegarder le log markdown
try:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
# ✅ Vérification de santé
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `13e2b84be54a41eb945785378b60f5f6` |
| **Nom** | Vérification de santé |
| **Cible** | `ali2v.xeon.home` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T02:16:51.460152+00:00 |
| **Fin** | 2025-12-22T02:16:59.573843+00:00 |
| **Durée** | 8.1s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [ali2v.xeon.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [ali2v.xeon.home]
TASK [Get system uptime] *******************************************************
ok: [ali2v.xeon.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.002844", "end": "2025-12-21 21:16:55.811614", "msg": "", "rc": 0, "start": "2025-12-21 21:16:55.808770", "stderr": "", "stderr_lines": [], "stdout": " 21:16:55 up 1 day, 22:16, 1 user, load average: 0.07, 0.26, 0.30", "stdout_lines": [" 21:16:55 up 1 day, 22:16, 1 user, load average: 0.07, 0.26, 0.30"]}
TASK [Get disk usage] **********************************************************
ok: [ali2v.xeon.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.004099", "end": "2025-12-21 21:16:56.243269", "msg": "", "rc": 0, "start": "2025-12-21 21:16:56.239170", "stderr": "", "stderr_lines": [], "stdout": "22%", "stdout_lines": ["22%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [ali2v.xeon.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.004345", "end": "2025-12-21 21:16:56.958250", "msg": "", "rc": 0, "start": "2025-12-21 21:16:56.953905", "stderr": "", "stderr_lines": [], "stdout": "20.6%", "stdout_lines": ["20.6%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [ali2v.xeon.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.004879", "end": "2025-12-21 21:16:57.736676", "msg": "", "rc": 0, "start": "2025-12-21 21:16:57.731797", "stderr": "", "stderr_lines": [], "stdout": "45.0°C", "stdout_lines": ["45.0°C"]}
TASK [Get CPU load] ************************************************************
ok: [ali2v.xeon.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.004063", "end": "2025-12-21 21:16:58.470317", "msg": "", "rc": 0, "start": "2025-12-21 21:16:58.466254", "stderr": "", "stderr_lines": [], "stdout": "0.06", "stdout_lines": ["0.06"]}
TASK [Display health status] ***************************************************
ok: [ali2v.xeon.home] => {
"msg": "═══════════════════════════════════════\nHost: ali2v.xeon.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 21:16:55 up 1 day, 22:16, 1 user, load average: 0.07, 0.26, 0.30\nDisk Usage: 22%\nMemory Usage: 20.6%\nCPU Load: 0.06\nCPU Temp: 45.0°C\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
ali2v.xeon.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T02:16:59.680539+00:00*
@@ -0,0 +1,69 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_4e00871ceb5d` |
| **Nom** | Playbook: Health Check |
| **Cible** | `env_lab` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T02:18:41.245690+00:00 |
| **Fin** | 2025-12-22T02:18:50.313464+00:00 |
| **Durée** | 9.7s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [dev.lab.home] => {"changed": false, "ping": "pong"}
ok: [media.labb.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [dev.lab.home]
ok: [media.labb.home]
TASK [Get system uptime] *******************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.002993", "end": "2025-12-21 21:18:46.738790", "msg": "", "rc": 0, "start": "2025-12-21 21:18:46.735797", "stderr": "", "stderr_lines": [], "stdout": " 21:18:46 up 3:33, 0 user, load average: 0.09, 0.18, 0.17", "stdout_lines": [" 21:18:46 up 3:33, 0 user, load average: 0.09, 0.18, 0.17"]}
ok: [media.labb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.003514", "end": "2025-12-21 21:18:08.052826", "msg": "", "rc": 0, "start": "2025-12-21 21:18:08.049312", "stderr": "", "stderr_lines": [], "stdout": " 21:18:08 up 22 days, 5:20, 0 user, load average: 0.00, 0.02, 0.00", "stdout_lines": [" 21:18:08 up 22 days, 5:20, 0 user, load average: 0.00, 0.02, 0.00"]}
TASK [Get disk usage] **********************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.005481", "end": "2025-12-21 21:18:47.378500", "msg": "", "rc": 0, "start": "2025-12-21 21:18:47.373019", "stderr": "", "stderr_lines": [], "stdout": "50%", "stdout_lines": ["50%"]}
ok: [media.labb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.005819", "end": "2025-12-21 21:18:08.717887", "msg": "", "rc": 0, "start": "2025-12-21 21:18:08.712068", "stderr": "", "stderr_lines": [], "stdout": "25%", "stdout_lines": ["25%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.005529", "end": "2025-12-21 21:18:48.084897", "msg": "", "rc": 0, "start": "2025-12-21 21:18:48.079368", "stderr": "", "stderr_lines": [], "stdout": "77.2%", "stdout_lines": ["77.2%"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.004326", "end": "2025-12-21 21:18:09.398504", "msg": "", "rc": 0, "start": "2025-12-21 21:18:09.394178", "stderr": "", "stderr_lines": [], "stdout": "60.4%", "stdout_lines": ["60.4%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.002563", "end": "2025-12-21 21:18:48.724451", "msg": "", "rc": 0, "start": "2025-12-21 21:18:48.721888", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.003053", "end": "2025-12-21 21:18:10.054191", "msg": "", "rc": 0, "start": "2025-12-21 21:18:10.051138", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
TASK [Get CPU load] ************************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.004795", "end": "2025-12-21 21:18:49.383952", "msg": "", "rc": 0, "start": "2025-12-21 21:18:49.379157", "stderr": "", "stderr_lines": [], "stdout": "0.09", "stdout_lines": ["0.09"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.005060", "end": "2025-12-21 21:18:10.713741", "msg": "", "rc": 0, "start": "2025-12-21 21:18:10.708681", "stderr": "", "stderr_lines": [], "stdout": "0.00", "stdout_lines": ["0.00"]}
TASK [Display health status] ***************************************************
ok: [dev.lab.home] => {
"msg": "═══════════════════════════════════════\nHost: dev.lab.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 21:18:46 up 3:33, 0 user, load average: 0.09, 0.18, 0.17\nDisk Usage: 50%\nMemory Usage: 77.2%\nCPU Load: 0.09\nCPU Temp: N/A\n═══════════════════════════════════════\n"
}
ok: [media.labb.home] => {
"msg": "═══════════════════════════════════════\nHost: media.labb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 21:18:08 up 22 days, 5:20, 0 user, load average: 0.00, 0.02, 0.00\nDisk Usage: 25%\nMemory Usage: 60.4%\nCPU Load: 0.00\nCPU Temp: N/A\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
dev.lab.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
media.labb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T02:18:50.322512+00:00*
@@ -0,0 +1,69 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_f8b6e41ff159` |
| **Nom** | Playbook: Health Check |
| **Cible** | `env_lab` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T02:44:52.164420+00:00 |
| **Fin** | 2025-12-22T02:45:04.128027+00:00 |
| **Durée** | 11.9s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [dev.lab.home] => {"changed": false, "ping": "pong"}
ok: [media.labb.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [dev.lab.home]
ok: [media.labb.home]
TASK [Get system uptime] *******************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.002863", "end": "2025-12-21 21:44:59.687460", "msg": "", "rc": 0, "start": "2025-12-21 21:44:59.684597", "stderr": "", "stderr_lines": [], "stdout": " 21:44:59 up 3:59, 0 user, load average: 0.13, 0.16, 0.17", "stdout_lines": [" 21:44:59 up 3:59, 0 user, load average: 0.13, 0.16, 0.17"]}
ok: [media.labb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.003623", "end": "2025-12-21 21:44:21.007560", "msg": "", "rc": 0, "start": "2025-12-21 21:44:21.003937", "stderr": "", "stderr_lines": [], "stdout": " 21:44:21 up 22 days, 5:46, 0 user, load average: 0.15, 0.05, 0.01", "stdout_lines": [" 21:44:21 up 22 days, 5:46, 0 user, load average: 0.15, 0.05, 0.01"]}
TASK [Get disk usage] **********************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.005219", "end": "2025-12-21 21:45:00.423346", "msg": "", "rc": 0, "start": "2025-12-21 21:45:00.418127", "stderr": "", "stderr_lines": [], "stdout": "50%", "stdout_lines": ["50%"]}
ok: [media.labb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.005561", "end": "2025-12-21 21:44:21.744850", "msg": "", "rc": 0, "start": "2025-12-21 21:44:21.739289", "stderr": "", "stderr_lines": [], "stdout": "25%", "stdout_lines": ["25%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.003939", "end": "2025-12-21 21:45:01.130170", "msg": "", "rc": 0, "start": "2025-12-21 21:45:01.126231", "stderr": "", "stderr_lines": [], "stdout": "77.3%", "stdout_lines": ["77.3%"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.004259", "end": "2025-12-21 21:44:22.460821", "msg": "", "rc": 0, "start": "2025-12-21 21:44:22.456562", "stderr": "", "stderr_lines": [], "stdout": "60.2%", "stdout_lines": ["60.2%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.002817", "end": "2025-12-21 21:45:01.803874", "msg": "", "rc": 0, "start": "2025-12-21 21:45:01.801057", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.003187", "end": "2025-12-21 21:44:23.165750", "msg": "", "rc": 0, "start": "2025-12-21 21:44:23.162563", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
TASK [Get CPU load] ************************************************************
ok: [dev.lab.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.004399", "end": "2025-12-21 21:45:02.508423", "msg": "", "rc": 0, "start": "2025-12-21 21:45:02.504024", "stderr": "", "stderr_lines": [], "stdout": "0.12", "stdout_lines": ["0.12"]}
ok: [media.labb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.004939", "end": "2025-12-21 21:44:23.843717", "msg": "", "rc": 0, "start": "2025-12-21 21:44:23.838778", "stderr": "", "stderr_lines": [], "stdout": "0.15", "stdout_lines": ["0.15"]}
TASK [Display health status] ***************************************************
ok: [media.labb.home] => {
"msg": "═══════════════════════════════════════\nHost: media.labb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 21:44:21 up 22 days, 5:46, 0 user, load average: 0.15, 0.05, 0.01\nDisk Usage: 25%\nMemory Usage: 60.2%\nCPU Load: 0.15\nCPU Temp: N/A\n═══════════════════════════════════════\n"
}
ok: [dev.lab.home] => {
"msg": "═══════════════════════════════════════\nHost: dev.lab.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 21:44:59 up 3:59, 0 user, load average: 0.13, 0.16, 0.17\nDisk Usage: 50%\nMemory Usage: 77.3%\nCPU Load: 0.12\nCPU Temp: N/A\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
dev.lab.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
media.labb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T02:45:04.136628+00:00*
@@ -0,0 +1,80 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_35848579687b` |
| **Nom** | Playbook: Health Check |
| **Cible** | `role_sbc` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T03:04:43.692808+00:00 |
| **Fin** | 2025-12-22T03:05:04.816419+00:00 |
| **Durée** | 21.1s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [raspi.8gb.home] => {"changed": false, "ping": "pong"}
ok: [raspi.4gb.home] => {"changed": false, "ping": "pong"}
ok: [orangepi.pc.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [raspi.8gb.home]
ok: [raspi.4gb.home]
ok: [orangepi.pc.home]
TASK [Get system uptime] *******************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.008297", "end": "2025-12-21 22:04:55.585526", "msg": "", "rc": 0, "start": "2025-12-21 22:04:55.577229", "stderr": "", "stderr_lines": [], "stdout": " 22:04:55 up 198 days, 13:37, 1 user, load average: 0.14, 0.16, 0.11", "stdout_lines": [" 22:04:55 up 198 days, 13:37, 1 user, load average: 0.14, 0.16, 0.11"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.009138", "end": "2025-12-21 22:04:55.658156", "msg": "", "rc": 0, "start": "2025-12-21 22:04:55.649018", "stderr": "", "stderr_lines": [], "stdout": " 22:04:55 up 198 days, 13:37, 1 user, load average: 0.34, 0.23, 0.20", "stdout_lines": [" 22:04:55 up 198 days, 13:37, 1 user, load average: 0.34, 0.23, 0.20"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.019335", "end": "2025-12-21 22:04:56.303262", "msg": "", "rc": 0, "start": "2025-12-21 22:04:56.283927", "stderr": "", "stderr_lines": [], "stdout": " 22:04:56 up 19 days, 11:31, 1 user, load average: 0.29, 0.16, 0.11", "stdout_lines": [" 22:04:56 up 19 days, 11:31, 1 user, load average: 0.29, 0.16, 0.11"]}
TASK [Get disk usage] **********************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.011907", "end": "2025-12-21 22:04:57.332665", "msg": "", "rc": 0, "start": "2025-12-21 22:04:57.320758", "stderr": "", "stderr_lines": [], "stdout": "3%", "stdout_lines": ["3%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.016772", "end": "2025-12-21 22:04:57.388120", "msg": "", "rc": 0, "start": "2025-12-21 22:04:57.371348", "stderr": "", "stderr_lines": [], "stdout": "6%", "stdout_lines": ["6%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.025342", "end": "2025-12-21 22:04:57.991044", "msg": "", "rc": 0, "start": "2025-12-21 22:04:57.965702", "stderr": "", "stderr_lines": [], "stdout": "21%", "stdout_lines": ["21%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.010925", "end": "2025-12-21 22:04:58.963435", "msg": "", "rc": 0, "start": "2025-12-21 22:04:58.952510", "stderr": "", "stderr_lines": [], "stdout": "6.8%", "stdout_lines": ["6.8%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011741", "end": "2025-12-21 22:04:58.999650", "msg": "", "rc": 0, "start": "2025-12-21 22:04:58.987909", "stderr": "", "stderr_lines": [], "stdout": "13.1%", "stdout_lines": ["13.1%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.025349", "end": "2025-12-21 22:04:59.630074", "msg": "", "rc": 0, "start": "2025-12-21 22:04:59.604725", "stderr": "", "stderr_lines": [], "stdout": "21.0%", "stdout_lines": ["21.0%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.011633", "end": "2025-12-21 22:05:00.568277", "msg": "", "rc": 0, "start": "2025-12-21 22:05:00.556644", "stderr": "", "stderr_lines": [], "stdout": "31.6°C", "stdout_lines": ["31.6°C"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.012898", "end": "2025-12-21 22:05:00.623881", "msg": "", "rc": 0, "start": "2025-12-21 22:05:00.610983", "stderr": "", "stderr_lines": [], "stdout": "35.5°C", "stdout_lines": ["35.5°C"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.028697", "end": "2025-12-21 22:05:01.252436", "msg": "", "rc": 0, "start": "2025-12-21 22:05:01.223739", "stderr": "", "stderr_lines": [], "stdout": "35.7°C", "stdout_lines": ["35.7°C"]}
TASK [Get CPU load] ************************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010036", "end": "2025-12-21 22:05:02.249747", "msg": "", "rc": 0, "start": "2025-12-21 22:05:02.239711", "stderr": "", "stderr_lines": [], "stdout": "0.13", "stdout_lines": ["0.13"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.011128", "end": "2025-12-21 22:05:02.279561", "msg": "", "rc": 0, "start": "2025-12-21 22:05:02.268433", "stderr": "", "stderr_lines": [], "stdout": "0.31", "stdout_lines": ["0.31"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.024624", "end": "2025-12-21 22:05:02.902166", "msg": "", "rc": 0, "start": "2025-12-21 22:05:02.877542", "stderr": "", "stderr_lines": [], "stdout": "0.34", "stdout_lines": ["0.34"]}
TASK [Display health status] ***************************************************
ok: [orangepi.pc.home] => {
"msg": "═══════════════════════════════════════\nHost: orangepi.pc.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:04:56 up 19 days, 11:31, 1 user, load average: 0.29, 0.16, 0.11\nDisk Usage: 21%\nMemory Usage: 21.0%\nCPU Load: 0.34\nCPU Temp: 35.7°C\n═══════════════════════════════════════\n"
}
ok: [raspi.4gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.4gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:04:55 up 198 days, 13:37, 1 user, load average: 0.34, 0.23, 0.20\nDisk Usage: 6%\nMemory Usage: 13.1%\nCPU Load: 0.31\nCPU Temp: 35.5°C\n═══════════════════════════════════════\n"
}
ok: [raspi.8gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.8gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:04:55 up 198 days, 13:37, 1 user, load average: 0.14, 0.16, 0.11\nDisk Usage: 3%\nMemory Usage: 6.8%\nCPU Load: 0.13\nCPU Temp: 31.6°C\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
orangepi.pc.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.4gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.8gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T03:05:04.836981+00:00*
@@ -0,0 +1,80 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_56aa2f1702ef` |
| **Nom** | Playbook: Health Check |
| **Cible** | `role_sbc` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T03:22:48.526773+00:00 |
| **Fin** | 2025-12-22T03:23:09.484672+00:00 |
| **Durée** | 21.6s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [raspi.8gb.home] => {"changed": false, "ping": "pong"}
ok: [raspi.4gb.home] => {"changed": false, "ping": "pong"}
ok: [orangepi.pc.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [raspi.8gb.home]
ok: [raspi.4gb.home]
ok: [orangepi.pc.home]
TASK [Get system uptime] *******************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.008335", "end": "2025-12-21 22:23:01.161234", "msg": "", "rc": 0, "start": "2025-12-21 22:23:01.152899", "stderr": "", "stderr_lines": [], "stdout": " 22:23:01 up 198 days, 13:56, 1 user, load average: 0.17, 0.19, 0.14", "stdout_lines": [" 22:23:01 up 198 days, 13:56, 1 user, load average: 0.17, 0.19, 0.14"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.009265", "end": "2025-12-21 22:23:01.206222", "msg": "", "rc": 0, "start": "2025-12-21 22:23:01.196957", "stderr": "", "stderr_lines": [], "stdout": " 22:23:01 up 198 days, 13:55, 1 user, load average: 0.23, 0.22, 0.22", "stdout_lines": [" 22:23:01 up 198 days, 13:55, 1 user, load average: 0.23, 0.22, 0.22"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.019386", "end": "2025-12-21 22:23:01.861536", "msg": "", "rc": 0, "start": "2025-12-21 22:23:01.842150", "stderr": "", "stderr_lines": [], "stdout": " 22:23:01 up 19 days, 11:49, 1 user, load average: 0.33, 0.18, 0.15", "stdout_lines": [" 22:23:01 up 19 days, 11:49, 1 user, load average: 0.33, 0.18, 0.15"]}
TASK [Get disk usage] **********************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.010654", "end": "2025-12-21 22:23:02.880120", "msg": "", "rc": 0, "start": "2025-12-21 22:23:02.869466", "stderr": "", "stderr_lines": [], "stdout": "3%", "stdout_lines": ["3%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.011445", "end": "2025-12-21 22:23:02.893955", "msg": "", "rc": 0, "start": "2025-12-21 22:23:02.882510", "stderr": "", "stderr_lines": [], "stdout": "6%", "stdout_lines": ["6%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.025622", "end": "2025-12-21 22:23:03.531944", "msg": "", "rc": 0, "start": "2025-12-21 22:23:03.506322", "stderr": "", "stderr_lines": [], "stdout": "21%", "stdout_lines": ["21%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.010846", "end": "2025-12-21 22:23:04.571534", "msg": "", "rc": 0, "start": "2025-12-21 22:23:04.560688", "stderr": "", "stderr_lines": [], "stdout": "6.8%", "stdout_lines": ["6.8%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011902", "end": "2025-12-21 22:23:04.615751", "msg": "", "rc": 0, "start": "2025-12-21 22:23:04.603849", "stderr": "", "stderr_lines": [], "stdout": "12.9%", "stdout_lines": ["12.9%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.025546", "end": "2025-12-21 22:23:05.146668", "msg": "", "rc": 0, "start": "2025-12-21 22:23:05.121122", "stderr": "", "stderr_lines": [], "stdout": "21.3%", "stdout_lines": ["21.3%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.012031", "end": "2025-12-21 22:23:06.111557", "msg": "", "rc": 0, "start": "2025-12-21 22:23:06.099526", "stderr": "", "stderr_lines": [], "stdout": "31.2°C", "stdout_lines": ["31.2°C"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.012934", "end": "2025-12-21 22:23:06.136283", "msg": "", "rc": 0, "start": "2025-12-21 22:23:06.123349", "stderr": "", "stderr_lines": [], "stdout": "36.0°C", "stdout_lines": ["36.0°C"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.028299", "end": "2025-12-21 22:23:06.778035", "msg": "", "rc": 0, "start": "2025-12-21 22:23:06.749736", "stderr": "", "stderr_lines": [], "stdout": "34.7°C", "stdout_lines": ["34.7°C"]}
TASK [Get CPU load] ************************************************************
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.011173", "end": "2025-12-21 22:23:07.826571", "msg": "", "rc": 0, "start": "2025-12-21 22:23:07.815398", "stderr": "", "stderr_lines": [], "stdout": "0.29", "stdout_lines": ["0.29"]}
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.009990", "end": "2025-12-21 22:23:07.825103", "msg": "", "rc": 0, "start": "2025-12-21 22:23:07.815113", "stderr": "", "stderr_lines": [], "stdout": "0.16", "stdout_lines": ["0.16"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.024916", "end": "2025-12-21 22:23:08.474920", "msg": "", "rc": 0, "start": "2025-12-21 22:23:08.450004", "stderr": "", "stderr_lines": [], "stdout": "0.38", "stdout_lines": ["0.38"]}
TASK [Display health status] ***************************************************
ok: [orangepi.pc.home] => {
"msg": "═══════════════════════════════════════\nHost: orangepi.pc.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:23:01 up 19 days, 11:49, 1 user, load average: 0.33, 0.18, 0.15\nDisk Usage: 21%\nMemory Usage: 21.3%\nCPU Load: 0.38\nCPU Temp: 34.7°C\n═══════════════════════════════════════\n"
}
ok: [raspi.4gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.4gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:23:01 up 198 days, 13:55, 1 user, load average: 0.23, 0.22, 0.22\nDisk Usage: 6%\nMemory Usage: 12.9%\nCPU Load: 0.29\nCPU Temp: 36.0°C\n═══════════════════════════════════════\n"
}
ok: [raspi.8gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.8gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:23:01 up 198 days, 13:56, 1 user, load average: 0.17, 0.19, 0.14\nDisk Usage: 3%\nMemory Usage: 6.8%\nCPU Load: 0.16\nCPU Temp: 31.2°C\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
orangepi.pc.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.4gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.8gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T03:23:09.508193+00:00*
@@ -0,0 +1,80 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_63960fea2661` |
| **Nom** | Playbook: Health Check |
| **Cible** | `role_sbc` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T15:26:37.036654+00:00 |
| **Fin** | 2025-12-22T15:27:00.563786+00:00 |
| **Durée** | 24.5s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [raspi.8gb.home] => {"changed": false, "ping": "pong"}
ok: [raspi.4gb.home] => {"changed": false, "ping": "pong"}
ok: [orangepi.pc.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [raspi.8gb.home]
ok: [raspi.4gb.home]
ok: [orangepi.pc.home]
TASK [Get system uptime] *******************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.008279", "end": "2025-12-22 10:26:50.756667", "msg": "", "rc": 0, "start": "2025-12-22 10:26:50.748388", "stderr": "", "stderr_lines": [], "stdout": " 10:26:50 up 199 days, 1:59, 1 user, load average: 0.03, 0.07, 0.08", "stdout_lines": [" 10:26:50 up 199 days, 1:59, 1 user, load average: 0.03, 0.07, 0.08"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.009115", "end": "2025-12-22 10:26:50.810590", "msg": "", "rc": 0, "start": "2025-12-22 10:26:50.801475", "stderr": "", "stderr_lines": [], "stdout": " 10:26:50 up 199 days, 1:59, 1 user, load average: 0.20, 0.22, 0.19", "stdout_lines": [" 10:26:50 up 199 days, 1:59, 1 user, load average: 0.20, 0.22, 0.19"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.019959", "end": "2025-12-22 10:26:51.484561", "msg": "", "rc": 0, "start": "2025-12-22 10:26:51.464602", "stderr": "", "stderr_lines": [], "stdout": " 10:26:51 up 19 days, 23:53, 1 user, load average: 0.16, 0.09, 0.03", "stdout_lines": [" 10:26:51 up 19 days, 23:53, 1 user, load average: 0.16, 0.09, 0.03"]}
TASK [Get disk usage] **********************************************************
ok: [raspi.4gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.011372", "end": "2025-12-22 10:26:52.656174", "msg": "", "rc": 0, "start": "2025-12-22 10:26:52.644802", "stderr": "", "stderr_lines": [], "stdout": "6%", "stdout_lines": ["6%"]}
ok: [raspi.8gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.010592", "end": "2025-12-22 10:26:52.660537", "msg": "", "rc": 0, "start": "2025-12-22 10:26:52.649945", "stderr": "", "stderr_lines": [], "stdout": "3%", "stdout_lines": ["3%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.025473", "end": "2025-12-22 10:26:53.236551", "msg": "", "rc": 0, "start": "2025-12-22 10:26:53.211078", "stderr": "", "stderr_lines": [], "stdout": "21%", "stdout_lines": ["21%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011943", "end": "2025-12-22 10:26:54.363957", "msg": "", "rc": 0, "start": "2025-12-22 10:26:54.352014", "stderr": "", "stderr_lines": [], "stdout": "13.0%", "stdout_lines": ["13.0%"]}
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011536", "end": "2025-12-22 10:26:54.380178", "msg": "", "rc": 0, "start": "2025-12-22 10:26:54.368642", "stderr": "", "stderr_lines": [], "stdout": "6.8%", "stdout_lines": ["6.8%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.025869", "end": "2025-12-22 10:26:55.060191", "msg": "", "rc": 0, "start": "2025-12-22 10:26:55.034322", "stderr": "", "stderr_lines": [], "stdout": "19.3%", "stdout_lines": ["19.3%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.011952", "end": "2025-12-22 10:26:56.056283", "msg": "", "rc": 0, "start": "2025-12-22 10:26:56.044331", "stderr": "", "stderr_lines": [], "stdout": "31.6°C", "stdout_lines": ["31.6°C"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.012804", "end": "2025-12-22 10:26:56.092442", "msg": "", "rc": 0, "start": "2025-12-22 10:26:56.079638", "stderr": "", "stderr_lines": [], "stdout": "36.0°C", "stdout_lines": ["36.0°C"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.028841", "end": "2025-12-22 10:26:56.739179", "msg": "", "rc": 0, "start": "2025-12-22 10:26:56.710338", "stderr": "", "stderr_lines": [], "stdout": "34.9°C", "stdout_lines": ["34.9°C"]}
TASK [Get CPU load] ************************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010256", "end": "2025-12-22 10:26:58.000512", "msg": "", "rc": 0, "start": "2025-12-22 10:26:57.990256", "stderr": "", "stderr_lines": [], "stdout": "0.02", "stdout_lines": ["0.02"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.011106", "end": "2025-12-22 10:26:58.003816", "msg": "", "rc": 0, "start": "2025-12-22 10:26:57.992710", "stderr": "", "stderr_lines": [], "stdout": "0.26", "stdout_lines": ["0.26"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.025269", "end": "2025-12-22 10:26:58.537020", "msg": "", "rc": 0, "start": "2025-12-22 10:26:58.511751", "stderr": "", "stderr_lines": [], "stdout": "0.23", "stdout_lines": ["0.23"]}
TASK [Display health status] ***************************************************
ok: [orangepi.pc.home] => {
"msg": "═══════════════════════════════════════\nHost: orangepi.pc.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:26:51 up 19 days, 23:53, 1 user, load average: 0.16, 0.09, 0.03\nDisk Usage: 21%\nMemory Usage: 19.3%\nCPU Load: 0.23\nCPU Temp: 34.9°C\n═══════════════════════════════════════\n"
}
ok: [raspi.4gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.4gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:26:50 up 199 days, 1:59, 1 user, load average: 0.20, 0.22, 0.19\nDisk Usage: 6%\nMemory Usage: 13.0%\nCPU Load: 0.26\nCPU Temp: 36.0°C\n═══════════════════════════════════════\n"
}
ok: [raspi.8gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.8gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:26:50 up 199 days, 1:59, 1 user, load average: 0.03, 0.07, 0.08\nDisk Usage: 3%\nMemory Usage: 6.8%\nCPU Load: 0.02\nCPU Temp: 31.6°C\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
orangepi.pc.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.4gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.8gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T15:27:00.582682+00:00*
@@ -0,0 +1,80 @@
# ✅ Playbook: Health Check
## Informations
| Propriété | Valeur |
|-----------|--------|
| **ID** | `pb_12d48ce532b9` |
| **Nom** | Playbook: Health Check |
| **Cible** | `role_sbc` |
| **Statut** | completed |
| **Type** | Manuel |
| **Progression** | 100% |
| **Début** | 2025-12-22T15:39:19.302097+00:00 |
| **Fin** | 2025-12-22T15:39:37.816483+00:00 |
| **Durée** | 20.2s |
## Sortie
```
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
PLAY [Health check on target host] *********************************************
TASK [Check if host is reachable (ping)] ***************************************
ok: [raspi.4gb.home] => {"changed": false, "ping": "pong"}
ok: [raspi.8gb.home] => {"changed": false, "ping": "pong"}
ok: [orangepi.pc.home] => {"changed": false, "ping": "pong"}
TASK [Gather minimal facts] ****************************************************
ok: [raspi.8gb.home]
ok: [raspi.4gb.home]
ok: [orangepi.pc.home]
TASK [Get system uptime] *******************************************************
ok: [raspi.4gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.009323", "end": "2025-12-22 10:39:29.431125", "msg": "", "rc": 0, "start": "2025-12-22 10:39:29.421802", "stderr": "", "stderr_lines": [], "stdout": " 10:39:29 up 199 days, 2:12, 1 user, load average: 0.24, 0.23, 0.23", "stdout_lines": [" 10:39:29 up 199 days, 2:12, 1 user, load average: 0.24, 0.23, 0.23"]}
ok: [raspi.8gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.008434", "end": "2025-12-22 10:39:29.468651", "msg": "", "rc": 0, "start": "2025-12-22 10:39:29.460217", "stderr": "", "stderr_lines": [], "stdout": " 10:39:29 up 199 days, 2:12, 1 user, load average: 0.38, 0.16, 0.11", "stdout_lines": [" 10:39:29 up 199 days, 2:12, 1 user, load average: 0.38, 0.16, 0.11"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.019670", "end": "2025-12-22 10:39:30.099522", "msg": "", "rc": 0, "start": "2025-12-22 10:39:30.079852", "stderr": "", "stderr_lines": [], "stdout": " 10:39:30 up 20 days, 6 min, 1 user, load average: 0.22, 0.09, 0.05", "stdout_lines": [" 10:39:30 up 20 days, 6 min, 1 user, load average: 0.22, 0.09, 0.05"]}
TASK [Get disk usage] **********************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.010719", "end": "2025-12-22 10:39:30.963692", "msg": "", "rc": 0, "start": "2025-12-22 10:39:30.952973", "stderr": "", "stderr_lines": [], "stdout": "3%", "stdout_lines": ["3%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.011443", "end": "2025-12-22 10:39:31.007648", "msg": "", "rc": 0, "start": "2025-12-22 10:39:30.996205", "stderr": "", "stderr_lines": [], "stdout": "6%", "stdout_lines": ["6%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.025340", "end": "2025-12-22 10:39:31.678391", "msg": "", "rc": 0, "start": "2025-12-22 10:39:31.653051", "stderr": "", "stderr_lines": [], "stdout": "21%", "stdout_lines": ["21%"]}
TASK [Get memory usage (Linux)] ************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.010884", "end": "2025-12-22 10:39:32.611655", "msg": "", "rc": 0, "start": "2025-12-22 10:39:32.600771", "stderr": "", "stderr_lines": [], "stdout": "6.7%", "stdout_lines": ["6.7%"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011883", "end": "2025-12-22 10:39:32.721361", "msg": "", "rc": 0, "start": "2025-12-22 10:39:32.709478", "stderr": "", "stderr_lines": [], "stdout": "12.9%", "stdout_lines": ["12.9%"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.026169", "end": "2025-12-22 10:39:33.300903", "msg": "", "rc": 0, "start": "2025-12-22 10:39:33.274734", "stderr": "", "stderr_lines": [], "stdout": "19.6%", "stdout_lines": ["19.6%"]}
TASK [Get CPU temperature (ARM/SBC)] *******************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.018230", "end": "2025-12-22 10:39:34.194334", "msg": "", "rc": 0, "start": "2025-12-22 10:39:34.176104", "stderr": "", "stderr_lines": [], "stdout": "32.6°C", "stdout_lines": ["32.6°C"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.012969", "end": "2025-12-22 10:39:34.217941", "msg": "", "rc": 0, "start": "2025-12-22 10:39:34.204972", "stderr": "", "stderr_lines": [], "stdout": "36.0°C", "stdout_lines": ["36.0°C"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.028826", "end": "2025-12-22 10:39:34.889991", "msg": "", "rc": 0, "start": "2025-12-22 10:39:34.861165", "stderr": "", "stderr_lines": [], "stdout": "35.7°C", "stdout_lines": ["35.7°C"]}
TASK [Get CPU load] ************************************************************
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010013", "end": "2025-12-22 10:39:35.806369", "msg": "", "rc": 0, "start": "2025-12-22 10:39:35.796356", "stderr": "", "stderr_lines": [], "stdout": "0.32", "stdout_lines": ["0.32"]}
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010938", "end": "2025-12-22 10:39:35.852912", "msg": "", "rc": 0, "start": "2025-12-22 10:39:35.841974", "stderr": "", "stderr_lines": [], "stdout": "0.20", "stdout_lines": ["0.20"]}
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.024336", "end": "2025-12-22 10:39:36.528145", "msg": "", "rc": 0, "start": "2025-12-22 10:39:36.503809", "stderr": "", "stderr_lines": [], "stdout": "0.29", "stdout_lines": ["0.29"]}
TASK [Display health status] ***************************************************
ok: [orangepi.pc.home] => {
"msg": "═══════════════════════════════════════\nHost: orangepi.pc.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:39:30 up 20 days, 6 min, 1 user, load average: 0.22, 0.09, 0.05\nDisk Usage: 21%\nMemory Usage: 19.6%\nCPU Load: 0.29\nCPU Temp: 35.7°C\n═══════════════════════════════════════\n"
}
ok: [raspi.4gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.4gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:39:29 up 199 days, 2:12, 1 user, load average: 0.24, 0.23, 0.23\nDisk Usage: 6%\nMemory Usage: 12.9%\nCPU Load: 0.20\nCPU Temp: 36.0°C\n═══════════════════════════════════════\n"
}
ok: [raspi.8gb.home] => {
"msg": "═══════════════════════════════════════\nHost: raspi.8gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 10:39:29 up 199 days, 2:12, 1 user, load average: 0.38, 0.16, 0.11\nDisk Usage: 3%\nMemory Usage: 6.7%\nCPU Load: 0.32\nCPU Temp: 32.6°C\n═══════════════════════════════════════\n"
}
PLAY RECAP *********************************************************************
orangepi.pc.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.4gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspi.8gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```
---
*Généré automatiquement par Homelab Automation Dashboard*
*Date: 2025-12-22T15:39:37.826449+00:00*
File diff suppressed because one or more lines are too long
+37
View File
@@ -596,6 +596,43 @@ class TestHostsFallback:
assert isinstance(response.json(), list)
class TestHostsInventoryGroupsMerge:
"""Tests pour la fusion des groupes depuis l'inventaire Ansible."""
async def test_list_hosts_merges_inventory_role_groups(
self, client: AsyncClient, db_session, host_factory
):
"""Les groups incluent les role_* depuis l'inventaire."""
await host_factory.create(
db_session,
name="mergegroups.local",
ip_address="10.10.10.10",
ansible_group="env_prod",
)
from app.schemas.host_api import AnsibleInventoryHost
with patch("app.routes.hosts.ansible_service") as mock_ansible:
mock_ansible.get_hosts_from_inventory.return_value = [
AnsibleInventoryHost(
name="mergegroups.local",
ansible_host="10.10.10.10",
group="env_prod",
groups=["env_prod", "role_sbc"],
vars={},
)
]
response = await client.get("/api/hosts")
assert response.status_code == 200
hosts = response.json()
our_host = next((h for h in hosts if h["name"] == "mergegroups.local"), None)
assert our_host is not None
assert "env_prod" in our_host["groups"]
assert "role_sbc" in our_host["groups"]
class TestHostToResponse:
"""Tests pour la fonction _host_to_response."""
+353
View File
@@ -11,6 +11,7 @@ Couvre:
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from datetime import datetime
pytestmark = pytest.mark.unit
@@ -172,6 +173,358 @@ class TestExecuteTask:
assert response.status_code in [200, 400, 422]
class TestHostHealthStatusUpdate:
"""Tests du lien health-check -> MAJ status/last_seen host en base."""
async def test_health_check_updates_host_in_db(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
# Créer un host en base, avec un nom qui correspondra au target
host = await host_factory.create(
db_session,
name="test-host-1.local",
ip_address="192.168.1.10",
status="unknown",
reachable=False,
last_seen=None,
)
# Exécuter le runner directement (le endpoint /api/tasks lance un asyncio.create_task)
from app.routes import tasks as tasks_routes
# Forcer le runner à utiliser le même engine in-memory que le reste des tests
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": True,
"return_code": 0,
"stdout": "ok",
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target=host.name,
extra_vars=None,
check_mode=False,
)
# Recharger depuis la DB et vérifier MAJ
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated = await repo.get(host.id)
assert updated is not None
assert updated.status == "online"
assert updated.reachable is True
assert updated.last_seen is not None
assert isinstance(updated.last_seen, datetime)
async def test_health_check_all_updates_each_host_from_recap(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
host1 = await host_factory.create(
db_session,
name="host1",
ip_address="192.168.1.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="host2",
ip_address="192.168.1.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
stdout = (
"PLAY RECAP\n"
"host1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"host2 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-all-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="all",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None
assert updated1.status == "online"
assert updated1.reachable is True
assert updated1.last_seen is not None
assert updated2 is not None
assert updated2.status == "offline"
assert updated2.reachable is False
assert updated2.last_seen is not None
class TestAnsibleExecuteHealthCheckStatusUpdate:
async def test_ansible_execute_health_check_updates_hosts_for_role_group(
self,
client: AsyncClient,
db_session,
host_factory,
):
host1 = await host_factory.create(
db_session,
name="orangepi.pc.home",
ip_address="10.10.0.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="raspi.4gb.home",
ip_address="10.10.0.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.schemas.host_api import AnsibleInventoryHost
inv_hosts = [
AnsibleInventoryHost(name="orangepi", ansible_host=host1.ip_address, group="env_homelab", groups=["role_sbc", "env_homelab"], vars={}),
AnsibleInventoryHost(name="raspi", ansible_host=host2.ip_address, group="env_homelab", groups=["role_sbc", "env_homelab"], vars={}),
]
stdout = (
"PLAY RECAP\n"
"orangepi : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"raspi : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch("app.routes.ansible.ansible_service.execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
"execution_time": 1.0,
})), patch("app.routes.ansible.ansible_service.get_hosts_from_inventory", new=MagicMock(return_value=inv_hosts)):
resp = await client.post(
"/api/ansible/execute",
json={
"playbook": "health-check.yml",
"target": "role_sbc",
"check_mode": False,
"verbose": True,
},
)
assert resp.status_code == 200
# Verify host statuses are updated via DB session
from app.crud.host import HostRepository
repo = HostRepository(db_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None and updated1.status == "online" and updated1.reachable is True and updated1.last_seen is not None
assert updated2 is not None and updated2.status == "offline" and updated2.reachable is False and updated2.last_seen is not None
async def test_health_check_group_updates_each_host_from_recap(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
host1 = await host_factory.create(
db_session,
name="group-host-1",
ip_address="10.0.0.1",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="group-host-2",
ip_address="10.0.0.2",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
stdout = (
"PLAY RECAP\n"
"group-host-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"group-host-2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": True,
"return_code": 0,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-group-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="env_test",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None and updated1.status == "online" and updated1.reachable is True and updated1.last_seen is not None
assert updated2 is not None and updated2.status == "online" and updated2.reachable is True and updated2.last_seen is not None
async def test_health_check_group_updates_hosts_when_recap_uses_inventory_alias(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
# DB stores hosts by their FQDN/IP, but Ansible recap may print the inventory alias.
host1 = await host_factory.create(
db_session,
name="sbc-01.local",
ip_address="10.42.0.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="sbc-02.local",
ip_address="10.42.0.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
from app.schemas.host_api import AnsibleInventoryHost
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
# Inventory aliases for the role group "role_sbc"
inv_hosts = [
AnsibleInventoryHost(name="sbc-01", ansible_host=host1.ip_address, group="env_test", groups=["role_sbc", "env_test"], vars={}),
AnsibleInventoryHost(name="sbc-02", ansible_host=host2.ip_address, group="env_test", groups=["role_sbc", "env_test"], vars={}),
]
stdout = (
"PLAY RECAP\n"
"sbc-01 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"sbc-02 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes.ansible_service, "get_hosts_from_inventory", new=MagicMock(return_value=inv_hosts)), patch.object(
tasks_routes,
"async_session_maker",
test_session_maker,
):
await tasks_routes._execute_task_playbook(
task_id="task-health-role-alias-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="role_sbc",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None
assert updated1.status == "online"
assert updated1.reachable is True
assert updated1.last_seen is not None
assert updated2 is not None
assert updated2.status == "offline"
assert updated2.reachable is False
assert updated2.last_seen is not None
class TestGetTaskById:
"""Tests pour GET /api/tasks/{task_id}."""