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:
@@ -80,6 +80,19 @@ class Settings(BaseSettings):
|
||||
# Leave empty to keep the feature disabled (default).
|
||||
homepage_api_key: str = ""
|
||||
|
||||
# Proxmox VE import.
|
||||
# Token = a real credential → env/.env ONLY, never persisted by the app to
|
||||
# scan_config.json and never returned by the API. token_id is
|
||||
# 'user@realm!tokenname'; use a read-only PVEAuditor role.
|
||||
proxmox_token_id: str = ""
|
||||
proxmox_token_secret: str = ""
|
||||
# Non-secret connection + auto-sync config (persisted via save_overrides).
|
||||
proxmox_host: str = ""
|
||||
proxmox_port: int = 8006
|
||||
proxmox_verify_tls: bool = True
|
||||
proxmox_sync_enabled: bool = False
|
||||
proxmox_sync_interval: int = 3600 # seconds (floor 300 enforced on write)
|
||||
|
||||
def _override_path(self) -> Path:
|
||||
return Path(self.sqlite_path).parent / "scan_config.json"
|
||||
|
||||
@@ -108,6 +121,17 @@ class Settings(BaseSettings):
|
||||
self.scanner_http_probe_enabled = bool(data["scanner_http_probe_enabled"])
|
||||
if "scanner_http_verify_tls" in data:
|
||||
self.scanner_http_verify_tls = bool(data["scanner_http_verify_tls"])
|
||||
# Proxmox non-secret config (token stays env-only, never here).
|
||||
if "proxmox_host" in data:
|
||||
self.proxmox_host = str(data["proxmox_host"])
|
||||
if "proxmox_port" in data:
|
||||
self.proxmox_port = int(data["proxmox_port"])
|
||||
if "proxmox_verify_tls" in data:
|
||||
self.proxmox_verify_tls = bool(data["proxmox_verify_tls"])
|
||||
if "proxmox_sync_enabled" in data:
|
||||
self.proxmox_sync_enabled = bool(data["proxmox_sync_enabled"])
|
||||
if "proxmox_sync_interval" in data:
|
||||
self.proxmox_sync_interval = int(data["proxmox_sync_interval"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -122,6 +146,13 @@ class Settings(BaseSettings):
|
||||
"scanner_http_ranges": self.scanner_http_ranges,
|
||||
"scanner_http_probe_enabled": self.scanner_http_probe_enabled,
|
||||
"scanner_http_verify_tls": self.scanner_http_verify_tls,
|
||||
# Proxmox: only non-secret config. Token fields are intentionally
|
||||
# excluded — they must never be written to disk by the app.
|
||||
"proxmox_host": self.proxmox_host,
|
||||
"proxmox_port": self.proxmox_port,
|
||||
"proxmox_verify_tls": self.proxmox_verify_tls,
|
||||
"proxmox_sync_enabled": self.proxmox_sync_enabled,
|
||||
"proxmox_sync_interval": self.proxmox_sync_interval,
|
||||
}))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user