feat: import hosts/VMs/LXC from Proxmox VE with optional auto-sync
Add a Proxmox VE importer that reads the /api2/json REST API with a read-only
API token and drops hosts (proxmox), VMs (vm) and LXC containers (lxc) onto the
canvas as typed nodes with run state and hardware specs (vCPU/RAM/disk).
- Backend: proxmox_service (httpx) + proxmox routes (test-connection, import,
import-pending, config). Two-tier dedupe — merge onto an existing scanned node
by IP, else synthetic pve-{host}-{vmid} identity. Update-in-place, never
deletes. Host->guest rendered as a 'virtual' edge via the pending-link flow.
- Security: token is env-only (PROXMOX_TOKEN_*), never written to disk by the
app, never returned by any endpoint; errors are credential-sanitized.
- Auto-sync: optional scheduled re-import into pending (APScheduler job).
- PendingDevice.properties carries specs through approve (+ migration).
- Frontend: ProxmoxImportModal, sidebar entry, pending inventory source filter,
Settings auto-sync section, proxmoxApi client.
- Docs: docs/proxmox-import.md, README + FEATURES sections, .env.example keys.
- Tests: backend service/router/scheduler, frontend modal/client/pending.
ha-relevant: maybe
This commit is contained in:
@@ -6,8 +6,11 @@ import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.core.scheduler import (
|
||||
_run_proxmox_sync,
|
||||
_run_service_checks,
|
||||
_run_status_checks,
|
||||
reschedule_proxmox_sync,
|
||||
set_proxmox_sync_enabled,
|
||||
set_service_checks_enabled,
|
||||
start_scheduler,
|
||||
stop_scheduler,
|
||||
@@ -148,6 +151,7 @@ def test_scheduler_uses_settings_interval():
|
||||
patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched):
|
||||
mock_settings.status_checker_interval = 45
|
||||
mock_settings.service_check_enabled = False
|
||||
mock_settings.proxmox_sync_enabled = False
|
||||
start_scheduler()
|
||||
_, kwargs = mock_sched.add_job.call_args
|
||||
assert kwargs["seconds"] == 45
|
||||
@@ -156,7 +160,11 @@ def test_scheduler_uses_settings_interval():
|
||||
def test_start_and_stop_scheduler():
|
||||
"""Scheduler can be started and stopped without errors."""
|
||||
mock_sched = MagicMock()
|
||||
with patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched):
|
||||
with patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched), \
|
||||
patch("app.core.scheduler.settings") as mock_settings:
|
||||
mock_settings.status_checker_interval = 60
|
||||
mock_settings.service_check_enabled = False
|
||||
mock_settings.proxmox_sync_enabled = False
|
||||
start_scheduler()
|
||||
stop_scheduler()
|
||||
mock_sched.add_job.assert_called_once()
|
||||
@@ -245,7 +253,49 @@ def test_start_scheduler_adds_service_job_when_enabled():
|
||||
mock_settings.status_checker_interval = 60
|
||||
mock_settings.service_check_enabled = True
|
||||
mock_settings.service_check_interval = 300
|
||||
mock_settings.proxmox_sync_enabled = False
|
||||
start_scheduler()
|
||||
job_ids = [kw.get("id") for _, kw in mock_sched.add_job.call_args_list]
|
||||
assert "status_checks" in job_ids
|
||||
assert "service_checks" in job_ids
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Proxmox auto-sync job
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_set_proxmox_sync_enabled_adds_and_removes_job():
|
||||
mock_sched = MagicMock()
|
||||
mock_sched.running = True
|
||||
with patch("app.core.scheduler.scheduler", mock_sched), \
|
||||
patch("app.core.scheduler.settings") as mock_settings:
|
||||
mock_settings.proxmox_sync_interval = 3600
|
||||
mock_sched.get_job.return_value = None
|
||||
set_proxmox_sync_enabled(True)
|
||||
mock_sched.add_job.assert_called_once()
|
||||
mock_sched.get_job.return_value = MagicMock()
|
||||
set_proxmox_sync_enabled(False)
|
||||
mock_sched.remove_job.assert_called_once_with("proxmox_sync")
|
||||
|
||||
|
||||
def test_reschedule_proxmox_sync_rejects_short_interval():
|
||||
with pytest.raises(ValueError):
|
||||
reschedule_proxmox_sync(60)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_proxmox_sync_skips_when_disabled():
|
||||
with patch("app.core.scheduler.settings") as mock_settings:
|
||||
mock_settings.proxmox_sync_enabled = False
|
||||
# Must return before importing/fetching anything.
|
||||
await _run_proxmox_sync()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_proxmox_sync_skips_when_no_token():
|
||||
with patch("app.core.scheduler.settings") as mock_settings:
|
||||
mock_settings.proxmox_sync_enabled = True
|
||||
mock_settings.proxmox_host = "pve"
|
||||
mock_settings.proxmox_token_id = ""
|
||||
mock_settings.proxmox_token_secret = ""
|
||||
await _run_proxmox_sync() # no exception, no fetch
|
||||
|
||||
Reference in New Issue
Block a user