From ad4aa4aba4ffe2f6f5132f575a8ec31704df4870 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 10 Jul 2026 12:47:16 +0200 Subject: [PATCH] fix: record ScanRun for scheduled Proxmox auto-sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scheduled auto-sync job called _persist_pending_import directly and never created a ScanRun, so auto-imports left no trace in Scan history (unlike manual /sync-now and /import-pending, which both record a run). Create a ScanRun(kind=proxmox) and delegate to the shared _background_proxmox_import flow — fetch, persist, mark the run done/error, and broadcast the inventory-reload signal. ha-relevant: maybe --- backend/app/core/scheduler.py | 45 ++++++++++++++++++------------- backend/tests/test_scheduler.py | 48 +++++++++++++-------------------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/backend/app/core/scheduler.py b/backend/app/core/scheduler.py index ed34c15..55e028e 100644 --- a/backend/app/core/scheduler.py +++ b/backend/app/core/scheduler.py @@ -107,32 +107,41 @@ async def _run_service_checks() -> 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: return 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") return # Lazy import to avoid a circular import at module load. - from app.api.routes.proxmox import _persist_pending_import - from app.services.proxmox_service import fetch_proxmox_inventory + from app.api.routes.proxmox import _background_proxmox_import + from app.db.models import ScanRun - try: - nodes_raw, edges_raw = await fetch_proxmox_inventory( - host=settings.proxmox_host, - port=settings.proxmox_port, - token_id=settings.proxmox_token_id, - token_secret=settings.proxmox_token_secret, - verify_tls=settings.proxmox_verify_tls, + async with AsyncSessionLocal() as db: + run = ScanRun( + status="running", + kind="proxmox", + ranges=[f"{settings.proxmox_host}:{settings.proxmox_port}"], ) - async with AsyncSessionLocal() as db: - result = await _persist_pending_import(db, nodes_raw, edges_raw) - logger.info( - "Proxmox auto-sync: %d devices (%d new, %d updated)", - result.device_count, result.pending_created, result.pending_updated, - ) - except Exception as exc: - logger.error("Proxmox auto-sync failed: %s", exc) + db.add(run) + await db.commit() + await db.refresh(run) + run_id = run.id + + # Shares the manual-sync flow: fetch + persist + mark the run done/error + + # broadcast the inventory-reload signal. + 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: diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py index 5c14e73..f6ce7c2 100644 --- a/backend/tests/test_scheduler.py +++ b/backend/tests/test_scheduler.py @@ -313,44 +313,34 @@ def _proxmox_settings(mock_settings): @pytest.mark.asyncio -async def test_run_proxmox_sync_persists_inventory(): - db = MagicMock() - session_ctx = MagicMock() - session_ctx.__aenter__ = AsyncMock(return_value=db) - session_ctx.__aexit__ = AsyncMock(return_value=False) - result = MagicMock(device_count=3, pending_created=2, pending_updated=1) +async def test_run_proxmox_sync_records_scan_run(mem_db): + """Auto-sync must create a ScanRun (kind=proxmox) so it shows in Scan + history, then delegate to the shared background import with the run id.""" + from app.db.models import ScanRun + with ( 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( - "app.services.proxmox_service.fetch_proxmox_inventory", + "app.api.routes.proxmox._background_proxmox_import", new_callable=AsyncMock, - return_value=(["node-raw"], ["edge-raw"]), - ) as mock_fetch, - patch( - "app.api.routes.proxmox._persist_pending_import", - new_callable=AsyncMock, - return_value=result, - ) as mock_persist, + ) as mock_bg, ): _proxmox_settings(mock_settings) 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 -async def test_run_proxmox_sync_logs_and_swallows_errors(): - with ( - patch("app.core.scheduler.settings") as mock_settings, - 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 + # Delegated to the same background flow the manual /sync-now uses, passing + # the run id + env connection settings. + mock_bg.assert_awaited_once_with( + run.id, "pve", 8006, "root@pam!tok", "secret", False, + ) # --- reschedule_* validation and not-running guards ---