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:
@@ -0,0 +1,75 @@
|
||||
"""Pydantic v2 schemas for Proxmox VE import.
|
||||
|
||||
Token fields are accepted on requests only and are optional — when omitted the
|
||||
backend falls back to the server-configured token (env). No response schema ever
|
||||
carries a token; secrets are kept out of responses by structural omission.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProxmoxConnectionRequest(BaseModel):
|
||||
host: str = Field(..., description="Proxmox VE host or IP")
|
||||
port: int = Field(8006, ge=1, le=65535, description="Proxmox API port")
|
||||
token_id: str | None = Field(
|
||||
None, description="API token id 'user@realm!tokenname' (falls back to server env)"
|
||||
)
|
||||
token_secret: str | None = Field(
|
||||
None, description="API token secret (falls back to server env)"
|
||||
)
|
||||
verify_tls: bool = Field(True, description="Verify the Proxmox TLS certificate")
|
||||
|
||||
|
||||
class ProxmoxTestConnectionResponse(BaseModel):
|
||||
connected: bool
|
||||
message: str
|
||||
|
||||
|
||||
class ProxmoxNodeOut(BaseModel):
|
||||
"""A homelable-ready node representation of a Proxmox host / VM / LXC."""
|
||||
|
||||
id: str
|
||||
label: str
|
||||
type: str # proxmox | vm | lxc
|
||||
ieee_address: str
|
||||
hostname: str | None = None
|
||||
ip: str | None = None
|
||||
status: str
|
||||
cpu_count: int | None = None
|
||||
ram_gb: float | None = None
|
||||
disk_gb: float | None = None
|
||||
vendor: str | None = None
|
||||
model: str | None = None
|
||||
parent_ieee: str | None = None
|
||||
|
||||
|
||||
class ProxmoxEdgeOut(BaseModel):
|
||||
source: str
|
||||
target: str
|
||||
|
||||
|
||||
class ProxmoxImportResponse(BaseModel):
|
||||
nodes: list[ProxmoxNodeOut]
|
||||
edges: list[ProxmoxEdgeOut]
|
||||
device_count: int
|
||||
|
||||
|
||||
class ProxmoxImportPendingResponse(BaseModel):
|
||||
"""Result of importing a Proxmox inventory into the pending section."""
|
||||
|
||||
pending_created: int
|
||||
pending_updated: int
|
||||
links_recorded: int
|
||||
device_count: int
|
||||
|
||||
|
||||
class ProxmoxConfig(BaseModel):
|
||||
"""Non-secret Proxmox connection + auto-sync config. Never carries a token —
|
||||
``token_configured`` reflects whether a server-side token is present."""
|
||||
|
||||
host: str = ""
|
||||
port: int = Field(8006, ge=1, le=65535)
|
||||
verify_tls: bool = True
|
||||
sync_enabled: bool = False
|
||||
sync_interval: int = Field(3600, ge=300)
|
||||
token_configured: bool = False
|
||||
@@ -20,6 +20,9 @@ class PendingDeviceResponse(BaseModel):
|
||||
model: str | None = None
|
||||
vendor: str | None = None
|
||||
lqi: int | None = None
|
||||
# Display properties carried from discovery (e.g. Proxmox specs). Merged into
|
||||
# the node on approve; empty for scan/mesh sources that don't set them.
|
||||
properties: list[Any] = []
|
||||
discovered_at: datetime
|
||||
# Number of distinct canvases (designs) this device already appears on,
|
||||
# correlated by ip / ieee_address against existing nodes. Computed per-request.
|
||||
|
||||
Reference in New Issue
Block a user