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
Some checks failed
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:
Bruno Charest 2025-12-22 10:43:17 -05:00
parent 6c51fb5c75
commit 46823eb42d
15 changed files with 1161 additions and 8 deletions

View File

@ -999,6 +999,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';
const isSuccess = status === 'completed';

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

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

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,6 +480,67 @@ async def _execute_task_playbook(
error_message=mem_task.error,
result_data={"output": result.get("stdout", "")[:5000]}
)
# 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

File diff suppressed because one or more lines are too long

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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

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."""

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}."""