feat: add node agent system with heartbeat collection, installer generation, and traffic flow telemetry
Add NodeAgent and TrafficFlow models to track agent status and network flows, implement /agents/heartbeat endpoint to receive interface counters, conntrack flows, firewall status, and nftables ruleset hash from agents, add nexafabric-agent.py Python script to collect host telemetry including VM/LXC interface hints via tap/fwbr regex matching, conntrack flow parsing with protocol/state/byte counters, and pve-firewall status checks,
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Cpu, Copy, RadioTower } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AgentInstallInfo, api, Node } from "../api/client";
|
||||
import { DataTable } from "../components/DataTable";
|
||||
import { iconButtonClass, secondaryButtonClass } from "../components/FormControls";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
export function Nodes() {
|
||||
const nodes = useQuery({ queryKey: ["nodes-agents"], queryFn: () => api<Node[]>("/nodes/agents") });
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const installInfo = useMutation({
|
||||
mutationFn: (node: Node) => api<AgentInstallInfo>(`/nodes/${node.id}/agent/install-info`),
|
||||
onSuccess: (_data, node) => {
|
||||
setSelectedNode(node);
|
||||
setCopied(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function copyCommand() {
|
||||
if (!installInfo.data) {
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(installInfo.data.command);
|
||||
setCopied(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Nodes" subtitle="Cluster nodes, capacity, health, and NexaFabric agent enrollment." />
|
||||
{nodes.isLoading ? <div className="rounded-md border border-border bg-panel p-8 text-sm">Loading...</div> : null}
|
||||
{nodes.error ? <div className="rounded-md border border-danger p-4 text-sm text-danger">Failed to load nodes.</div> : null}
|
||||
{!nodes.isLoading && !nodes.error ? (
|
||||
<DataTable
|
||||
rows={(nodes.data ?? []) as unknown as Record<string, unknown>[]}
|
||||
columns={[
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "status", label: "Status" },
|
||||
{ key: "cpu_count", label: "CPU" },
|
||||
{ key: "memory_mb", label: "Memory MB" },
|
||||
{
|
||||
key: "agent",
|
||||
label: "Agent",
|
||||
render: (row) => {
|
||||
const node = row as unknown as Node;
|
||||
return node.agent ? `${node.agent.status}${node.agent.version ? ` · ${node.agent.version}` : ""}` : "not_installed";
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Actions",
|
||||
render: (row) => {
|
||||
const node = row as unknown as Node;
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
className={iconButtonClass}
|
||||
title="Install node agent"
|
||||
aria-label={`Install agent on ${node.name}`}
|
||||
onClick={() => installInfo.mutate(node)}
|
||||
>
|
||||
<RadioTower size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
<Modal title="Install Node Agent" open={Boolean(selectedNode)} onClose={() => setSelectedNode(null)}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Cpu size={18} />
|
||||
{selectedNode?.name}
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-canvas p-3 text-xs text-slate-500 dark:text-slate-400">
|
||||
Run this command as root on the Proxmox node. It installs the agent under <code>/opt/nexafabric-agent</code> and starts a systemd service.
|
||||
</div>
|
||||
<pre className="max-h-48 overflow-auto rounded-md border border-border bg-canvas p-3 text-xs">
|
||||
{installInfo.data?.command ?? "Generating installer..."}
|
||||
</pre>
|
||||
<div className="grid gap-2 text-xs">
|
||||
<span className="text-slate-500 dark:text-slate-400">Installer link</span>
|
||||
<code className="break-all rounded-md border border-border bg-canvas p-3">{installInfo.data?.install_url ?? ""}</code>
|
||||
</div>
|
||||
<button className={secondaryButtonClass} disabled={!installInfo.data} onClick={copyCommand}>
|
||||
<Copy size={16} />
|
||||
{copied ? "Copied" : "Copy Command"}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user