HA / VIP (Keepalived)
} onClick={fetchVips}>Refresh
} onClick={openCreate}>Create VIP
{/* The cloud caveat is rarely relevant for the on-prem target audience, so it's a
subtle, collapsed-by-default info note (not a prominent yellow warning). */}
} style={{ paddingLeft: 0 }}
onClick={() => setShowL2Note((v) => !v)}>
Network requirements (on-prem / L2)
{showL2Note && (
)}
setModalVisible(false)}
onOk={submit}
okText={editing ? 'Save (PENDING)' : 'Create (PENDING)'}
width={880}
destroyOnClose
>
{!editing && (
)}
Participating nodes
These are the HAProxy servers (agents) already enrolled in this pool — toggle which join the VIP.
Pick exactly one MASTER (highest priority); the rest are BACKUP. A single node is allowed
(a keepalived-managed VIP without failover) — add a second node for real HA. Add new servers from
the standard Agent Management install flow.
On Apply, any participating node that doesn’t already run keepalived will install it from the node’s OS package repositories (apt/dnf/yum/zypper/apk) — make sure the node can reach its repos (internet or an internal mirror). A node already running a hand-managed keepalived is left untouched (reported as “externally managed”).>}
/>
{/* Delete confirmation with opt-in package uninstall. Enterprise-safe DEFAULT keeps the
package (just stop/disable + remove our config + release the VIP). */}
setDeleteTarget(null)}
onOk={() => deleteVip(deleteTarget, false)}
okText="Stage deletion for approval"
okButtonProps={{ danger: true }}
>
{deleteTarget && (
It stages the deletion for approval. The VIP keeps running, untouched, on its
member node(s) until you Approve it on the Apply Management page — and you can
Reject it there to keep it. The node is changed only after approval, so an
accidental click can't tear down a production VIP.>}
/>
Stage deletion of {deleteTarget.name} ({deleteTarget.virtual_ip}/{deleteTarget.prefix_length})?
When approved, the member node(s) stop & disable keepalived, remove the config we manage, and release the VIP. The keepalived package itself is left installed, so re-adding a VIP later is instant.
)}
{/* Per-node keepalived deploy diagnostics + node-side log commands (esp. during SYNCING). */}
{ setDiagVip(null); setDiagData(null); }}
width={780}
footer={[
} onClick={() => diagVip && openDiagnostics(diagVip)}>Refresh,
,
]}
>
{diagLoading &&
}
{!diagLoading && diagData && (
Staging {diagData.last_config_status} — each node reports its deploy state after every poll; a fresh keepalived install can take ~30s.
m.agent_name} pagination={false}
dataSource={diagData.members || []}
columns={[
{ title: 'Node', key: 'node', render: (_, m) => (
{m.agent_name}
{m.role === 'MASTER'
? } style={{ marginInlineEnd: 0 }}>MASTER
: BACKUP}
) },
{ title: 'Agent', dataIndex: 'agent_status', key: 'agent',
render: (s) => {s || 'offline'} },
{ title: 'Live VRRP', dataIndex: 'keepalive_state', key: 'live',
render: (s) => (s && s !== 'NONE')
? {s}
: — },
{ title: 'Deploy', key: 'deploy', render: (_, m) => {
const st = m.deploy_state;
const color = st === 'enabled' ? 'green' : st === 'error' ? 'red'
: st === 'externally_managed' ? 'gold' : 'blue';
return {m.convergence || st || 'pending'};
} },
{ title: 'Last ack', dataIndex: 'deploy_at', key: 'ack',
render: (t) => t ? {new Date(t).toLocaleString()} : — },
]}
/>
{(diagData.members || []).some((m) => m.deploy_message) && (
{(diagData.members || []).filter((m) => m.deploy_message).map((m) => (
{m.agent_name}: {m.deploy_message}
))}
)}
{`systemctl status keepalived --no-pager
journalctl -u keepalived --no-pager -n 50
tail -n 100 /var/log/haproxy-agent/agent.log | grep -i keepalived`}
}
/>
)}
{!diagLoading && !diagData && No diagnostics available.}
);
};
export default VIPManagement;