Merge pull request #269 from Pouzor/fix/proxmox-autosync-scan-history
fix: record ScanRun for scheduled Proxmox auto-sync
This commit is contained in:
@@ -107,32 +107,41 @@ async def _run_service_checks() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def _run_proxmox_sync() -> None:
|
async def _run_proxmox_sync() -> None:
|
||||||
"""Fetch the Proxmox inventory and upsert it into pending (auto-sync)."""
|
"""Fetch the Proxmox inventory and upsert it into pending (auto-sync).
|
||||||
|
|
||||||
|
Records a ScanRun (kind=proxmox) so the scheduled sync shows in Scan
|
||||||
|
history, exactly like the manual /sync-now and /import-pending paths.
|
||||||
|
"""
|
||||||
if not settings.proxmox_sync_enabled:
|
if not settings.proxmox_sync_enabled:
|
||||||
return
|
return
|
||||||
if not (settings.proxmox_host and settings.proxmox_token_id and settings.proxmox_token_secret):
|
if not (settings.proxmox_host and settings.proxmox_token_id and settings.proxmox_token_secret):
|
||||||
logger.warning("Proxmox auto-sync enabled but host/token not configured — skipping")
|
logger.warning("Proxmox auto-sync enabled but host/token not configured — skipping")
|
||||||
return
|
return
|
||||||
# Lazy import to avoid a circular import at module load.
|
# Lazy import to avoid a circular import at module load.
|
||||||
from app.api.routes.proxmox import _persist_pending_import
|
from app.api.routes.proxmox import _background_proxmox_import
|
||||||
from app.services.proxmox_service import fetch_proxmox_inventory
|
from app.db.models import ScanRun
|
||||||
|
|
||||||
try:
|
async with AsyncSessionLocal() as db:
|
||||||
nodes_raw, edges_raw = await fetch_proxmox_inventory(
|
run = ScanRun(
|
||||||
host=settings.proxmox_host,
|
status="running",
|
||||||
port=settings.proxmox_port,
|
kind="proxmox",
|
||||||
token_id=settings.proxmox_token_id,
|
ranges=[f"{settings.proxmox_host}:{settings.proxmox_port}"],
|
||||||
token_secret=settings.proxmox_token_secret,
|
|
||||||
verify_tls=settings.proxmox_verify_tls,
|
|
||||||
)
|
)
|
||||||
async with AsyncSessionLocal() as db:
|
db.add(run)
|
||||||
result = await _persist_pending_import(db, nodes_raw, edges_raw)
|
await db.commit()
|
||||||
logger.info(
|
await db.refresh(run)
|
||||||
"Proxmox auto-sync: %d devices (%d new, %d updated)",
|
run_id = run.id
|
||||||
result.device_count, result.pending_created, result.pending_updated,
|
|
||||||
)
|
# Shares the manual-sync flow: fetch + persist + mark the run done/error +
|
||||||
except Exception as exc:
|
# broadcast the inventory-reload signal.
|
||||||
logger.error("Proxmox auto-sync failed: %s", exc)
|
await _background_proxmox_import(
|
||||||
|
run_id,
|
||||||
|
settings.proxmox_host,
|
||||||
|
settings.proxmox_port,
|
||||||
|
settings.proxmox_token_id,
|
||||||
|
settings.proxmox_token_secret,
|
||||||
|
settings.proxmox_verify_tls,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_service_check_job() -> None:
|
def _add_service_check_job() -> None:
|
||||||
|
|||||||
@@ -313,44 +313,34 @@ def _proxmox_settings(mock_settings):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_run_proxmox_sync_persists_inventory():
|
async def test_run_proxmox_sync_records_scan_run(mem_db):
|
||||||
db = MagicMock()
|
"""Auto-sync must create a ScanRun (kind=proxmox) so it shows in Scan
|
||||||
session_ctx = MagicMock()
|
history, then delegate to the shared background import with the run id."""
|
||||||
session_ctx.__aenter__ = AsyncMock(return_value=db)
|
from app.db.models import ScanRun
|
||||||
session_ctx.__aexit__ = AsyncMock(return_value=False)
|
|
||||||
result = MagicMock(device_count=3, pending_created=2, pending_updated=1)
|
|
||||||
with (
|
with (
|
||||||
patch("app.core.scheduler.settings") as mock_settings,
|
patch("app.core.scheduler.settings") as mock_settings,
|
||||||
patch("app.core.scheduler.AsyncSessionLocal", MagicMock(return_value=session_ctx)),
|
patch("app.core.scheduler.AsyncSessionLocal", mem_db),
|
||||||
patch(
|
patch(
|
||||||
"app.services.proxmox_service.fetch_proxmox_inventory",
|
"app.api.routes.proxmox._background_proxmox_import",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
return_value=(["node-raw"], ["edge-raw"]),
|
) as mock_bg,
|
||||||
) as mock_fetch,
|
|
||||||
patch(
|
|
||||||
"app.api.routes.proxmox._persist_pending_import",
|
|
||||||
new_callable=AsyncMock,
|
|
||||||
return_value=result,
|
|
||||||
) as mock_persist,
|
|
||||||
):
|
):
|
||||||
_proxmox_settings(mock_settings)
|
_proxmox_settings(mock_settings)
|
||||||
await _run_proxmox_sync()
|
await _run_proxmox_sync()
|
||||||
mock_fetch.assert_awaited_once()
|
|
||||||
mock_persist.assert_awaited_once_with(db, ["node-raw"], ["edge-raw"])
|
|
||||||
|
|
||||||
|
# A ScanRun row exists — the missing scan-history trace.
|
||||||
|
async with mem_db() as db:
|
||||||
|
from sqlalchemy import select
|
||||||
|
run = (await db.execute(select(ScanRun))).scalars().one()
|
||||||
|
assert run.kind == "proxmox"
|
||||||
|
assert run.ranges == ["pve:8006"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
# Delegated to the same background flow the manual /sync-now uses, passing
|
||||||
async def test_run_proxmox_sync_logs_and_swallows_errors():
|
# the run id + env connection settings.
|
||||||
with (
|
mock_bg.assert_awaited_once_with(
|
||||||
patch("app.core.scheduler.settings") as mock_settings,
|
run.id, "pve", 8006, "root@pam!tok", "secret", False,
|
||||||
patch(
|
)
|
||||||
"app.services.proxmox_service.fetch_proxmox_inventory",
|
|
||||||
new_callable=AsyncMock,
|
|
||||||
side_effect=RuntimeError("proxmox api down"),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
_proxmox_settings(mock_settings)
|
|
||||||
await _run_proxmox_sync() # exception is logged, never propagated
|
|
||||||
|
|
||||||
|
|
||||||
# --- reschedule_* validation and not-running guards ---
|
# --- reschedule_* validation and not-running guards ---
|
||||||
|
|||||||
Reference in New Issue
Block a user