Skip to content

VM NIC surfaces present Networks, not vSwitches — design

Synced read-only from /home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-07-10-vm-nic-network-picker-design.md. Edit at the source, not here.

VM NIC surfaces present Networks, not vSwitches — design

Section titled “VM NIC surfaces present Networks, not vSwitches — design”

Date: 2026-07-10 · Status: approved (UI + backend scope)

The VM-facing NIC editors added with the Network-tab feature (5c86ff56) present the wrong abstraction: AddNicModal and ChangeNetworkModal offer a vSwitch selector plus a raw VLAN slider (0-4094), and the Network-tab rows label each adapter with the bridge name. Product doctrine is the opposite — a VM maps to a Network (a named slice of a vSwitch: NetworkInfo { name, vlan, default }), never to a bare vSwitch. The doctrine is already encoded elsewhere:

  • useNetworks.ts: “A VM is mapped to a Network, not to a bare vSwitch.”
  • The create-VM wizard (NetworkInterfacesConfigurator) and the hardware editor’s NicCard both present declared Networks by name.
  • The vocabulary table (CLAUDE.md §4.2): vSwitch/bridge → Network, VLAN ID → Network tag; raw terms only in Advanced/tooltips.
  • The daemon’s PUT /vms/{name}/nics/{mac} already rejects a (virtswitch, virtlan_tag) pair that no declared Network covers (E_VIRTLAN_NOT_DECLARED).

A related API-level hole: POST /vms/{name}/nics (attach) performs no such validation — any tag on any switch attaches successfully, so the API can create adapters on undeclared networks that the PUT would refuse.

One selector listing the flat declared Networks from useNetworks (all vSwitches): Network name as primary text, vSwitch · VLAN n (or untagged) as secondary. The chosen row projects to the existing {virtswitch, virtlan_tag} body — no API contract change.

  • AddNicModal: picker replaces the vSwitch control + untagged checkbox + VLAN slider. The host’s default Network is preselected. Model + MAC controls stay as-is.
  • ChangeNetworkModal: same picker, seeded with the NIC’s current Network (matched by bridge+tag). If the current combo matches no declared Network (legacy adapter), a fallback row labelled bridge · VLAN n represents the current state so it is always visible; every other option is a declared Network.
  • No raw-VLAN escape hatch (YAGNI): the daemon refuses undeclared combos; declaring a Network in the Networking view is the workflow.
  • Empty state: “No networks declared” + pointer to the Networking view.

Primary chip = resolved Network name (same resolution NicCard already uses: match bridge+tag against useNetworks rows; fallback bridge · VLAN n). vSwitch/VLAN demoted to secondary text. Copy in the tab (empty state, modal subtitles) stops saying “vSwitch”.

vm_edit::nics::attach_nic gains the same gate as vm_nic_edit::edit_nic: resolve the NetworkController snapshot, 404 E_VIRTSWITCH_NOT_FOUND when the switch does not exist, 400 E_VIRTLAN_NOT_DECLARED when no declared Network on it carries the tag. Unit tests mirror the PUT’s cases.

  • Cascading vSwitch → Network picker: reintroduces the vSwitch as the primary decision — the exact error being fixed.
  • New flat /api/v1/networks endpoint: unnecessary; useNetworks already flattens the nested /api/v1/vswitches shape client-side.
  • Cargo: attach validation cases (missing switch, undeclared tag, declared tag OK) alongside the existing 18 vm_nic_edit tests.
  • Vitest: modal specs updated to drive the Network picker; NetworkTab row-label spec. Judge by diff-vs-baseline (baseline is not green).
  • E2E e2e/vm-network-tab.spec.ts: drive the picker by Network name against a deployed host; fleet deploy lockstep afterwards.