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
Problem
Section titled “Problem”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:
- Hostname / FQDN of the appliance.
- Management interfaces (ipMGMT) — one per vSwitch, across N vSwitches, each with its own VLAN / IP / gateway / DNS / MTU.
In scope:
- New
NetworkPanel.vuerendered for the existingnetworksection. - Hostname read + edit (new backend endpoint).
- Management-interface list: read all vSwitches that carry an
ipmgmtvmkernel, edit each, and addipmgmtto 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 needsOption<VmKernel>→Vecacross 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.vuehas aSECTIONSregistry +v-if="active === '<id>'"; each section = API clientsrc/api/settings/<x>.ts- composable
src/composables/useHost<X>.ts+ panelsrc/views/settings/<X>Panel.vue. Backend:crates/daemon/src/settings/api/<x>.rsregistered insettings/api/mod.rs, responses wrapped inSettingsEnvelope<T>(crates/daemon/src/settings/envelope.rs) carryingdata,warnings,meta.
- composable
- Hostname today: stored in
network.yaml(NetworkYaml.hostname, validated RFC1123 viais_rfc1123_hostnameinsynvirt-network/src/spec/mod.rs). Read at boot intoLocalNodeIdentityand exposed read-only viaGET /api/v1/node/identity(crates/daemon/src/api/cluster.rs).state.node_identityis an immutable boot snapshot; many call sites readstate.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_vmkernelPUT /api/v1/vswitches/{name}/service-ports/{kind}→configure_vmkernelDELETE /api/v1/vswitches/{name}/service-ports/{kind}→clear_vmkernel(crates/daemon/src/api/network.rs;kind∈ipmgmt|ipstorage|ipmigration).
- Commit-confirm safety is already wired (server-side, synchronous): the
apply path goes through
synvirt-networksafe_reconcile.rs+ structuralmgmt_readiness.rs(no ICMP): snapshot → apply → verify management readiness → auto-rollback tonetwork.yaml.last-goodon breakage. The PUT response body carriesapply.status(confirmed_management|applied_non_management|rolled_back…) andapply.rollback_*. The gate protects the default vSwitch’s ipMGMT specifically.
Backend design
Section titled “Backend design”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/network → SettingsEnvelope<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 }:
- Validate RFC1123 (reuse
is_rfc1123_hostname). 400 on invalid. hostnamectl set-hostname <hostname>.- Update
NetworkYaml.hostnameviaNetworkControllermutation (persist). - Hot-reload identity: make the hostname behind
state.node_identityupdatable without a daemon restart. Chosen approach: wrap the mutable field (hostname) in anArcSwap<String>(or store identity behindArcSwap/RwLock) and have thenode_identityread sites pull the live value. Minimal-blast alternative if wrapping the whole identity is too invasive: re-read the OS hostname at theGET /api/v1/node/identitysite and update the field there. Final mechanism decided in the implementation plan; requirement: no restart. - Response:
SettingsEnvelope<NetworkSettingsView>(fresh read). Whenis_cluster_member, attach awarning(“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)]).
Reused (no change): ipMGMT apply
Section titled “Reused (no change): ipMGMT apply”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:
-
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. Bind0.0.0.0:mesh_portinstead 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. -
Announce on IP change. Add
ClusterControl::announce_address(addr): whenmode == "cluster", it submitsAddMember { node_id: self, address: addr }(upsert). The daemon calls it right after a successfulmgmtservice-port change, withaddr = "{detect_mgmt_ipv4()}:{mesh_port}". Cluster control is out-of-process (clusterd, UDS), so this is wired the same wayconfig_putis: trait method →EmbeddedClusterControl(submit) +ClusterControlUdsClient(POST a newPATH_ANNOUNCE_ADDRESS) → clusterd route + handler (host.runtime.submit(AddMember{...})). Standalone / federated hosts: no-op.
- Hostname: no cluster announce needed —
cluster_membershas no hostname column; peers display it from identity/inventory sync, which refreshes on its own. Local apply (set_hostname+ livenode/identityread) is the whole change. An informational note is shown on cluster members.
Frontend design
Section titled “Frontend design”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; methodsapplyHostname(),applyMgmtIp(vswitch, config),removeMgmtIp(vswitch),refresh(); loading/error/warnings;onScopeDisposecleanup.src/views/settings/NetworkPanel.vue, wired withv-if="active === 'network'"inSettingsView.vue(replacing the PlaceholderPanel fallback fornetwork):- 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 fromaddable_vswitchesand configures its ipMGMT. Remove allowed only for non-default interfaces. Whenis_cluster_member, an info note states that management-IP changes are announced to the cluster automatically.
Re-IP safety UX (the load-bearing detail)
Section titled “Re-IP safety UX (the load-bearing detail)”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.status—confirmed_management/applied_non_management= success;rolled_back= showapply.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 tohttps://<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.
OpenAPI
Section titled “OpenAPI”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.
Brand / i18n / conventions
Section titled “Brand / i18n / conventions”- Copy: ipMGMT, vSwitch, vNIC per house style; identifiers stay
lowercase. Dashboard
translate=no/notranslateuntouched. - Statics carry
Cache-Control: no-cache; a hard refresh is needed after deploy.
Verification plan
Section titled “Verification plan”Deploy hot to .13 (synvirt-03, a snapshotted VM, recoverable) via
deploy-to-host.sh with the mandatory USER=root override. Verify:
- Panel reads current hostname + all ipMGMT interfaces correctly.
- Hostname change applies, persists in
network.yaml, andGET /api/v1/node/identityreflects the new hostname without a daemon restart. - 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 confirmrolled_back. - Cluster announce: after re-IP’ing .13’s management interface, confirm a peer
(.11 or .12) sees .13’s updated address in
cluster_membersand Raft re-converges (GET /api/v1/cluster/healthhealthy; .13 still a voter at the new IP). The mesh listener (now wildcard) accepts on the new IP without a restart. - 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.hostnameis stored affects several read sites. Mitigation: prefer the smallest mutable wrapper; the implementation plan picksArcSwapvs 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 (
AddMemberupsert) updatescluster_members→ peers refreshPeerRegistry; 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 atwarnand 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.