Skip to content

Settings → Network: hostname + management interfaces (ipMGMT)

Synced read-only from /home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-06-26-settings-network-hostname-ipmgmt-design.md. Edit at the source, not here.

Settings → Network: hostname + management interfaces (ipMGMT)

Section titled “Settings → Network: hostname + management interfaces (ipMGMT)”

Date: 2026-06-26 Status: Approved (design), pending implementation plan Branch: 0.15.0

The Settings page (https://<host>/settings) already registers a network section (id network, label “IP/DNS and Hostname”) in SettingsView.vue, but it renders PlaceholderPanel — there is no panel. Operators cannot change the host hostname or the management interfaces (ipMGMT) from the dashboard; today the only path for ipMGMT is the generic networking view, and hostname has no UI or API at all.

This feature fills the network settings section with a real panel that edits:

  1. Hostname / FQDN of the appliance.
  2. Management interfaces (ipMGMT)one per vSwitch, across N vSwitches, each with its own VLAN / IP / gateway / DNS / MTU.

In scope:

  • New NetworkPanel.vue rendered for the existing network section.
  • Hostname read + edit (new backend endpoint).
  • Management-interface list: read all vSwitches that carry an ipmgmt vmkernel, edit each, and add ipmgmt to a vSwitch that lacks one. Reuses the already-wired ipMGMT apply path (commit-confirm + rollback).

Out of scope (explicitly):

  • vSwitch / uplink / nested-network management — stays in /network (NetworkingView.vue). The panel only displays which vSwitch an ipMGMT belongs to; it does not create/delete vSwitches or change uplinks.
  • Two or more ipMGMT on the same vSwitch (different VLANs). The current data model is VSwitch { ipmgmt: Option<VmKernel> } — one ipMGMT per vSwitch. Supporting multiple-per-vSwitch needs Option<VmKernel>Vec across wire format, validation, OVS apply and commit-confirm, and is deferred to a separate phase. “Different VLAN and/or different vSwitch” is satisfied here by N vSwitches each with one ipMGMT on its chosen VLAN.
  • No gating of clustered re-IP. Earlier draft gated it; corrected per owner: RAFT identity is the node id (derived from the node’s persistent fingerprint, not the IP), so changing a member’s management IP does not change its identity. The only cluster-side action needed is announcing the new address (see “Cluster / Raft awareness”). Hostname/IP change otherwise reuses the existing wiring.

Key facts grounding the design (verified in code)

Section titled “Key facts grounding the design (verified in code)”
  • Settings module pattern: SettingsView.vue has a SECTIONS registry + v-if="active === '<id>'"; each section = API client src/api/settings/<x>.ts
    • composable src/composables/useHost<X>.ts + panel src/views/settings/<X>Panel.vue. Backend: crates/daemon/src/settings/api/<x>.rs registered in settings/api/mod.rs, responses wrapped in SettingsEnvelope<T> (crates/daemon/src/settings/envelope.rs) carrying data, warnings, meta.
  • Hostname today: stored in network.yaml (NetworkYaml.hostname, validated RFC1123 via is_rfc1123_hostname in synvirt-network/src/spec/mod.rs). Read at boot into LocalNodeIdentity and exposed read-only via GET /api/v1/node/identity (crates/daemon/src/api/cluster.rs). state.node_identity is an immutable boot snapshot; many call sites read state.node_identity.hostname (cloud_health, cluster_health, hosts, inventory). No write endpoint exists.
  • ipMGMT today: VSwitch.ipmgmt: Option<VmKernel>; VmKernel { vlan: u16, mode: AddressMode, address: Option<String> (CIDR), gateway: Option<String>, dns, mtu }. Read/write already exposed:
    • GET /api/v1/vswitches/{name}/service-ports/{kind}get_vmkernel
    • PUT /api/v1/vswitches/{name}/service-ports/{kind}configure_vmkernel
    • DELETE /api/v1/vswitches/{name}/service-ports/{kind}clear_vmkernel (crates/daemon/src/api/network.rs; kindipmgmt|ipstorage|ipmigration).
  • Commit-confirm safety is already wired (server-side, synchronous): the apply path goes through synvirt-network safe_reconcile.rs + structural mgmt_readiness.rs (no ICMP): snapshot → apply → verify management readiness → auto-rollback to network.yaml.last-good on breakage. The PUT response body carries apply.status (confirmed_management | applied_non_management | rolled_back …) and apply.rollback_*. The gate protects the default vSwitch’s ipMGMT specifically.

New: Network settings section (crates/daemon/src/settings/api/network.rs)

Section titled “New: Network settings section (crates/daemon/src/settings/api/network.rs)”

GET /api/v1/settings/networkSettingsEnvelope<NetworkSettingsView>:

struct NetworkSettingsView {
hostname: String,
fqdn: String, // hostname + domain when known, else == hostname
is_cluster_member: bool, // from cluster_control.get().is_some()
mgmt_interfaces: Vec<MgmtInterfaceView>,
addable_vswitches: Vec<String>, // vSwitches without an ipmgmt (candidates for Add)
}
struct MgmtInterfaceView {
vswitch: String,
is_default: bool, // the default vSwitch's ipMGMT (mgmt-readiness gated)
vlan: u16,
mode: String, // "static" | "dhcp"
address: String, // CIDR, empty for dhcp
gateway: String,
dns: Vec<String>,
mtu: u32,
}

is_cluster_member is informational (drives a UI note: IP changes are announced to the cluster automatically). No per-interface lock — every ipMGMT is editable.

Composed internally from NetworkController reads (vswitch list + each ipmgmt). No new persistence.

PUT /api/v1/settings/network/hostname body { hostname: String }:

  1. Validate RFC1123 (reuse is_rfc1123_hostname). 400 on invalid.
  2. hostnamectl set-hostname <hostname>.
  3. Update NetworkYaml.hostname via NetworkController mutation (persist).
  4. Hot-reload identity: make the hostname behind state.node_identity updatable without a daemon restart. Chosen approach: wrap the mutable field (hostname) in an ArcSwap<String> (or store identity behind ArcSwap/RwLock) and have the node_identity read sites pull the live value. Minimal-blast alternative if wrapping the whole identity is too invasive: re-read the OS hostname at the GET /api/v1/node/identity site and update the field there. Final mechanism decided in the implementation plan; requirement: no restart.
  5. Response: SettingsEnvelope<NetworkSettingsView> (fresh read). When is_cluster_member, attach a warning (“cluster identity keys off node_id/fingerprint, not hostname; peers will display the new name after their next sync”).

Register both routes in settings/api/mod.rs. Handlers follow the existing settings handler shape (utoipa operation_id, State<AppState>, Extension<AuthUser> actor for audit, #[instrument(skip_all)]).

The panel applies each management interface through the existing configure_vmkernel / clear_vmkernel endpoints. Add/edit = PUT …/service-ports/ipmgmt; remove = DELETE …/service-ports/ipmgmt. The front interprets the existing apply.status body. The default vSwitch’s ipMGMT cannot be removed (server already validates; UI also disables it).

Cluster / Raft awareness (announce the new address)

Section titled “Cluster / Raft awareness (announce the new address)”

RAFT identity is the node id — node_id_from_fingerprint(identity.fingerprint()) (synvirt-cluster/src/federation_runtime.rs), derived from the node’s persistent ed25519 key, not the IP. So changing a member’s management IP does not change its identity; peers just need the new dial address.

Peers dial a node at the address in the Raft-replicated cluster_members table (PeerRegistry is “refreshed from applied cluster_members). The write command is ClusterCommand::AddMember { node_id, address }, an upsert (ON CONFLICT(node_id) DO UPDATE SET address). So re-issuing AddMember with this node’s (stable) id and the new address updates the cluster’s view of where to reach it — no membership change, no quorum churn.

Two concrete changes — nothing more:

  1. Mesh binds wildcard (one-time fix). Today the mesh listener binds the specific IP (TcpListener::bind((bind_address, mesh_port))), so an IP change would strand the listener. Bind 0.0.0.0:mesh_port instead while still advertising the detected IP (mesh_addr = "{bind_address}:{port}" unchanged). The socket becomes IP-agnostic; the only runtime step on an IP change is the announce.

  2. Announce on IP change. Add ClusterControl::announce_address(addr): when mode == "cluster", it submits AddMember { node_id: self, address: addr } (upsert). The daemon calls it right after a successful mgmt service-port change, with addr = "{detect_mgmt_ipv4()}:{mesh_port}". Cluster control is out-of-process (clusterd, UDS), so this is wired the same way config_put is: trait method → EmbeddedClusterControl (submit) + ClusterControlUdsClient (POST a new PATH_ANNOUNCE_ADDRESS) → clusterd route + handler (host.runtime.submit(AddMember{...})). Standalone / federated hosts: no-op.

  • Hostname: no cluster announce needed — cluster_members has no hostname column; peers display it from identity/inventory sync, which refreshes on its own. Local apply (set_hostname + live node/identity read) is the whole change. An informational note is shown on cluster members.
  • src/api/settings/network.ts: wire types mirroring the Rust DTOs + getNetworkSettings(), putHostname(body). ipMGMT add/edit/remove reuse the existing network API client’s vmkernel calls.
  • src/composables/useHostNetwork.ts: fetch on mount; state for hostname + mgmt_interfaces + addable_vswitches; methods applyHostname(), applyMgmtIp(vswitch, config), removeMgmtIp(vswitch), refresh(); loading/error/warnings; onScopeDispose cleanup.
  • src/views/settings/NetworkPanel.vue, wired with v-if="active === 'network'" in SettingsView.vue (replacing the PlaceholderPanel fallback for network):
    • Hostname card: current hostname/FQDN, edit field with inline RFC1123 validation, Apply. Cluster-member banner when applicable.
    • Management interfaces card: a list — one row per MgmtInterfaceView (vSwitch name, default badge, VLAN, mode, CIDR, gateway, DNS, MTU), each editable in place; per-row Apply. “Add management interface” picks a vSwitch from addable_vswitches and configures its ipMGMT. Remove allowed only for non-default interfaces. When is_cluster_member, an info note states that management-IP changes are announced to the cluster automatically.

Changing the ipMGMT you are currently connected through drops the session mid-request (the PUT may never return). Handling:

  • Before apply, detect whether the edited interface’s address matches the browser’s current host. If so, show a confirm modal: “This is the interface you’re connected through. Your session will drop; reconnect at https://<new-ip>/settings.”
  • After issuing the PUT, if the response arrives: render apply.statusconfirmed_management/applied_non_management = success; rolled_back = show apply.rollback_reason (the safety net reverted a broken config). If the request times out / connection drops (expected on self re-IP), surface the new-IP link and offer auto-redirect to https://<new-ip>/settings.
  • Editing a non-default ipMGMT while connected via the default one is low-risk (no session drop); still show the new-IP info for that interface.

After adding the two settings/network endpoints, re-dump crates/web-ux-v2/openapi.snapshot.json (synvirt-daemon --dump-openapi) and regenerate TS types. The reused vmkernel endpoints are already in the contract.

  • Copy: ipMGMT, vSwitch, vNIC per house style; identifiers stay lowercase. Dashboard translate=no / notranslate untouched.
  • Statics carry Cache-Control: no-cache; a hard refresh is needed after deploy.

Deploy hot to .13 (synvirt-03, a snapshotted VM, recoverable) via deploy-to-host.sh with the mandatory USER=root override. Verify:

  1. Panel reads current hostname + all ipMGMT interfaces correctly.
  2. Hostname change applies, persists in network.yaml, and GET /api/v1/node/identity reflects the new hostname without a daemon restart.
  3. ipMGMT list: every interface is editable. Exercise a real re-IP (safe because .13 is snapshotted): change an ipMGMT and observe apply.status, including a deliberately-broken config to confirm rolled_back.
  4. Cluster announce: after re-IP’ing .13’s management interface, confirm a peer (.11 or .12) sees .13’s updated address in cluster_members and Raft re-converges (GET /api/v1/cluster/health healthy; .13 still a voter at the new IP). The mesh listener (now wildcard) accepts on the new IP without a restart.
  5. Frontend: vitest judged diff-vs-baseline (baseline is not 100% green); Playwright for the panel flow where practical.
  • Hostname hot-reload blast radius: touching how node_identity.hostname is stored affects several read sites. Mitigation: prefer the smallest mutable wrapper; the implementation plan picks ArcSwap vs re-read and lists every touched call site.
  • Self re-IP lockout: mitigated by the existing commit-confirm rollback (broken configs revert) + the new-IP redirect UX (intentional moves). Tested on the snapshotted .13, never live on .11.
  • Clustered re-IP reachability: after the IP changes, peers must learn the new address. Mitigation: the announce (AddMember upsert) updates cluster_members → peers refresh PeerRegistry; the wildcard mesh bind means the listener already accepts on the new IP. Identity (node id) is unchanged, so there is no membership/quorum churn. If the announce submit fails (e.g. no leader reachable mid-change), it is logged at warn and the next successful apply / a daemon restart re-announces — the IP change itself still succeeded. Verified on .13 (snapshotted): re-IP a member and confirm peers re-converge.
  • Cluster member hostname change: cluster identity is node_id/fingerprint based, so cosmetic to membership; surfaced as a warning, not blocked.