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
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:
@@ -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."""
|
||||
|
||||
|
||||
@@ -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}."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user