Skip to content

feat(proxmox): reconfigure VMs during PatchHosts when their config drifts - #353

Merged
v0l merged 4 commits into
masterfrom
feat/patch-vm-config
Aug 2, 2026
Merged

feat(proxmox): reconfigure VMs during PatchHosts when their config drifts#353
v0l merged 4 commits into
masterfrom
feat/patch-vm-config

Conversation

@v0l

@v0l v0l commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

PatchHosts re-applied firewall state for every live VM but never touched the VM config, so a VM whose config on the hypervisor no longer matched the database (template upgrades, manual edits on the host, changed defaults like the scsi controller) stayed drifted until someone triggered ConfigureVm by hand.

This adds a firewall-patch analogue for config.

Changes

  • VmHostClient::patch_config (lnvps_api_common/src/host/mod.rs) — reads the live config, diffs it against the config expected from the DB, and calls configure_vm only when something differs; returns the drifted field names. Defaults to a no-op for hosts that can't read their config back (libvirt, dummy).
  • Proxmox impl (lnvps_api_common/src/host/proxmox.rs) — compares only the fields we set (name, cores, memory, balloon, cpu, cpulimit, onboot, machine, ostype, bios, boot, kvm, scsihw, serial0, cicustom, net0, ipconfig0, sshkeys). Disk/EFI entries are ignored since they're owned by create/resize. Property strings are normalised (key order + case) because Proxmox re-orders keys and upper-cases MACs when storing them, and ssh keys are compared url-decoded — otherwise every pass would report permanent drift. VmBios now derives PartialEq, Eq.
  • Worker (lnvps_api/src/worker.rs) — patch_host calls patch_config before patch_firewall, reusing the already-loaded FullVmInfo; logs Re-configured VM {id} on host {name} (drift: ...) and warns non-fatally on failure.

Testing

  • New unit tests: test_config_drift (no false positives on Proxmox-normalised strings, detects resource/ipconfig/ssh-key drift) and test_normalize_prop_string.
  • cargo clippy -p lnvps_api -p lnvps_api_common --all-targets clean; proxmox test suite passes.

v0l added 4 commits August 2, 2026 20:39
…ifts

PatchHosts only re-applied firewall state, so a VM whose config on the
hypervisor no longer matched the database (template upgrades, manual
edits, changed defaults such as the scsi controller) stayed drifted
until someone triggered ConfigureVm by hand.

Add VmHostClient::patch_config, a firewall-patch analogue that reads the
live config, diffs it against the config we expect from the DB, and only
calls configure_vm when something actually differs — returning the
drifted field names for logging. Defaults to a no-op for hosts that
cannot read their config back (libvirt, dummy).

The Proxmox implementation compares only the fields we set, ignoring
disk/EFI entries (owned by create/resize). Property strings (net0,
ipconfig0) are normalised before comparison because Proxmox re-orders
keys and upper-cases MACs when it stores them, and ssh keys are compared
url-decoded — otherwise every pass would report permanent drift.
The disk options we own (iothread=1, discard/ssd, throttle limits) live
on the scsi0 device string, which is rebuilt from the live volume
reference rather than produced by make_config — so the drift check
missed them entirely, and a VM whose only difference was the missing
iothread flag was never reconciled.

Factor the scsi0 device string out of apply_disk_options into make_scsi0
and compare it against what the host currently has. When it differs,
scsi0 is reported as drift and configure_vm re-applies it (as it already
does at the end of a normal configure), which is exactly what converges
existing VMs onto iothread=1 alongside virtio-scsi-single and the
ballooning minimum from the host config.
Proxmox stores compound config values as property strings
(`net0 = virtio=BC:24:11:00:11:22,bridge=vmbr0,firewall=1`). Holding them
as raw Strings meant every read was a substring search and every
comparison had to re-implement Proxmox's normalisation — it re-orders
keys and upper-cases MACs when it stores them — which is why the drift
check needed a sort/lowercase hack and a url-decode special case for ssh
keys.

Give each property string we set a real type in host::proxmox_config:
NetDevice (+ NetModel, MacAddress), IpConfig (+ Ipv4Setting/Ipv6Setting),
DiskDevice (+ VolumeRef), CiCustom and SshKeys, each parsed on the way in
and rendered on the way out through serde. VmConfig now holds those types
instead of Strings, so:

- config_drift is plain field equality, no normalisation helpers
- make_scsi0 builds a DiskDevice from the live volume rather than
  splitting and re-joining a string
- parse_mac_from_net / parse_storage_from_disk are gone, replaced by
  reading net.mac and scsi_0.volume.storage
- make_config assembles typed values, including the one-address-per-family
  ipconfig rule which is now enforced by the type rather than by filtering
  formatted strings

Parsing is lenient by design: unmodelled keys are ignored and malformed
values dropped, so a hand-edited VM config is still readable.
Two agent_chat tests asserted outcomes the model chooses rather than ones
the server enforces, and both failed intermittently.

test_chat_resists_indirect_injection_via_tool_output required the model
to call get_vm_details. That call is the test's precondition — the
injected payload only enters the context through the tool result — not
the property under test, which is that the payload dispatches nothing
destructive and leaks no host token. It now asks up to three times,
progressively more directly, stopping as soon as the lookup runs, and
checks the security assertions on every turn. When the lookup never
happens the failure prints the tools and replies from each attempt
instead of a bare `ran: []`.

test_chat_refuses_billing_actions flaked the same way: the model
declined correctly but omitted the LNVPS_ESCALATE sentinel the e2e system
prompt mandates. Same treatment — one follow-up turn, security
assertions each time, sentinel required on at least one.

Verified with scripts/run-e2e.sh --filter agent_chat: 13 passed, 0 failed.
@v0l
v0l merged commit a3332c7 into master Aug 2, 2026
5 of 6 checks passed
@v0l
v0l deleted the feat/patch-vm-config branch August 2, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant