feat(agent): vms/lxc collectors for runtime and detail with fixtures
This commit is contained in:
parent
ec7f08dfda
commit
da5ed6cd08
6 changed files with 299 additions and 0 deletions
164
agent/lib/proxmox_agent/collectors/vms.ex
Normal file
164
agent/lib/proxmox_agent/collectors/vms.ex
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
defmodule ProxmoxAgent.Collectors.Vms do
|
||||||
|
@moduledoc """
|
||||||
|
Collects VM/LXC runtime (fast path) and per-VM detail incl. IPs (medium path).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@spec collect_runtime(keyword()) :: %{vms: [map()], errors: [map()]}
|
||||||
|
def collect_runtime(opts) do
|
||||||
|
runner = runner(opts)
|
||||||
|
node = Keyword.fetch!(opts, :node)
|
||||||
|
|
||||||
|
{qemu, e1} = list(runner, node, "qemu")
|
||||||
|
{lxc, e2} = list(runner, node, "lxc")
|
||||||
|
|
||||||
|
vms =
|
||||||
|
Enum.map(qemu, &normalize_runtime(&1, "qemu")) ++
|
||||||
|
Enum.map(lxc, &normalize_runtime(&1, "lxc"))
|
||||||
|
|
||||||
|
%{vms: vms, errors: Enum.filter([e1, e2], & &1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec collect_detail(keyword()) :: %{vms: [map()], errors: [map()]}
|
||||||
|
def collect_detail(opts) do
|
||||||
|
runner = runner(opts)
|
||||||
|
node = Keyword.fetch!(opts, :node)
|
||||||
|
|
||||||
|
{qemu, e1} = list(runner, node, "qemu")
|
||||||
|
{lxc, e2} = list(runner, node, "lxc")
|
||||||
|
|
||||||
|
qemu_details = Enum.map(qemu, &qemu_detail(runner, node, &1))
|
||||||
|
lxc_details = Enum.map(lxc, &lxc_detail(runner, node, &1))
|
||||||
|
|
||||||
|
%{vms: qemu_details ++ lxc_details, errors: Enum.filter([e1, e2], & &1)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp runner(opts), do: Keyword.get(opts, :runner, &ProxmoxAgent.Shell.run/2)
|
||||||
|
|
||||||
|
defp list(runner, node, type) do
|
||||||
|
case runner.("pvesh", ["get", "/nodes/#{node}/#{type}", "--output-format", "json"]) do
|
||||||
|
{:ok, body} ->
|
||||||
|
case Jason.decode(body) do
|
||||||
|
{:ok, list} when is_list(list) -> {list, nil}
|
||||||
|
{:error, e} -> {[], %{tag: "decode_#{type}", message: Exception.message(e)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{[], %{tag: "list_#{type}", message: inspect(reason)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_runtime(entry, type) do
|
||||||
|
%{
|
||||||
|
vmid: entry["vmid"],
|
||||||
|
type: type,
|
||||||
|
name: entry["name"] || entry["hostname"],
|
||||||
|
status: entry["status"],
|
||||||
|
uptime_seconds: entry["uptime"] || 0,
|
||||||
|
cpu_usage: entry["cpu"] || 0.0,
|
||||||
|
mem_bytes: entry["mem"] || 0,
|
||||||
|
max_mem_bytes: entry["maxmem"] || 0,
|
||||||
|
tags: parse_tags(entry["tags"])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_tags(nil), do: []
|
||||||
|
defp parse_tags(""), do: []
|
||||||
|
|
||||||
|
defp parse_tags(str) when is_binary(str) do
|
||||||
|
str |> String.split([";", ","], trim: true) |> Enum.map(&String.trim/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp qemu_detail(runner, node, entry) do
|
||||||
|
vmid = entry["vmid"]
|
||||||
|
{config, cfg_err} = fetch_json(runner, "/nodes/#{node}/qemu/#{vmid}/config")
|
||||||
|
{ips, ip_err} = fetch_qemu_agent_ips(runner, node, vmid)
|
||||||
|
|
||||||
|
%{
|
||||||
|
vmid: vmid,
|
||||||
|
type: "qemu",
|
||||||
|
name: entry["name"],
|
||||||
|
config: config || %{},
|
||||||
|
ips: ips,
|
||||||
|
errors: Enum.filter([cfg_err, ip_err], & &1)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp lxc_detail(runner, node, entry) do
|
||||||
|
vmid = entry["vmid"]
|
||||||
|
{config, cfg_err} = fetch_json(runner, "/nodes/#{node}/lxc/#{vmid}/config")
|
||||||
|
ips = extract_lxc_ips(config || %{})
|
||||||
|
|
||||||
|
%{
|
||||||
|
vmid: vmid,
|
||||||
|
type: "lxc",
|
||||||
|
name: entry["name"],
|
||||||
|
config: config || %{},
|
||||||
|
ips: ips,
|
||||||
|
errors: Enum.filter([cfg_err], & &1)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_json(runner, path) do
|
||||||
|
case runner.("pvesh", ["get", path, "--output-format", "json"]) do
|
||||||
|
{:ok, body} ->
|
||||||
|
case Jason.decode(body) do
|
||||||
|
{:ok, map} -> {map, nil}
|
||||||
|
{:error, e} -> {nil, %{tag: "decode", message: Exception.message(e)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{nil, %{tag: "pvesh", message: inspect(reason)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_qemu_agent_ips(runner, node, vmid) do
|
||||||
|
case runner.("pvesh", [
|
||||||
|
"get",
|
||||||
|
"/nodes/#{node}/qemu/#{vmid}/agent/network-get-interfaces",
|
||||||
|
"--output-format",
|
||||||
|
"json"
|
||||||
|
]) do
|
||||||
|
{:ok, body} ->
|
||||||
|
case Jason.decode(body) do
|
||||||
|
{:ok, %{"result" => interfaces}} ->
|
||||||
|
ips =
|
||||||
|
interfaces
|
||||||
|
|> Enum.reject(&(&1["name"] == "lo"))
|
||||||
|
|> Enum.flat_map(&Map.get(&1, "ip-addresses", []))
|
||||||
|
|> Enum.filter(&(&1["ip-address-type"] == "ipv4"))
|
||||||
|
|> Enum.map(& &1["ip-address"])
|
||||||
|
|
||||||
|
{ips, nil}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{[], %{tag: "agent_ips", message: "unexpected shape"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, _reason} ->
|
||||||
|
{[], nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_lxc_ips(config) do
|
||||||
|
config
|
||||||
|
|> Enum.filter(fn {k, _} -> String.starts_with?(to_string(k), "net") end)
|
||||||
|
|> Enum.flat_map(fn {_, val} -> parse_lxc_net(val) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_lxc_net(val) when is_binary(val) do
|
||||||
|
val
|
||||||
|
|> String.split(",")
|
||||||
|
|> Enum.find_value([], fn pair ->
|
||||||
|
case String.split(pair, "=", parts: 2) do
|
||||||
|
["ip", ip] ->
|
||||||
|
ip = ip |> String.split("/") |> hd()
|
||||||
|
if ip == "dhcp", do: [], else: [ip]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_lxc_net(_), do: []
|
||||||
|
end
|
||||||
12
agent/test/fixtures/pvesh/lxc.json
vendored
Normal file
12
agent/test/fixtures/pvesh/lxc.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"vmid": 200,
|
||||||
|
"name": "minecraft",
|
||||||
|
"status": "running",
|
||||||
|
"uptime": 3600,
|
||||||
|
"cpu": 0.15,
|
||||||
|
"mem": 2147483648,
|
||||||
|
"maxmem": 4294967296,
|
||||||
|
"tags": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
22
agent/test/fixtures/pvesh/qemu.json
vendored
Normal file
22
agent/test/fixtures/pvesh/qemu.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"vmid": 100,
|
||||||
|
"name": "nginx-proxy",
|
||||||
|
"status": "running",
|
||||||
|
"uptime": 86400,
|
||||||
|
"cpu": 0.05,
|
||||||
|
"mem": 536870912,
|
||||||
|
"maxmem": 2147483648,
|
||||||
|
"tags": "web;production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vmid": 101,
|
||||||
|
"name": "db-backup",
|
||||||
|
"status": "stopped",
|
||||||
|
"uptime": 0,
|
||||||
|
"cpu": 0,
|
||||||
|
"mem": 0,
|
||||||
|
"maxmem": 4294967296,
|
||||||
|
"tags": "db"
|
||||||
|
}
|
||||||
|
]
|
||||||
17
agent/test/fixtures/pvesh/qemu_100_agent_interfaces.json
vendored
Normal file
17
agent/test/fixtures/pvesh/qemu_100_agent_interfaces.json
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"name": "lo",
|
||||||
|
"ip-addresses": [
|
||||||
|
{ "ip-address": "127.0.0.1", "ip-address-type": "ipv4" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eth0",
|
||||||
|
"ip-addresses": [
|
||||||
|
{ "ip-address": "192.168.1.10", "ip-address-type": "ipv4" },
|
||||||
|
{ "ip-address": "fe80::a", "ip-address-type": "ipv6" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
agent/test/fixtures/pvesh/qemu_100_config.json
vendored
Normal file
8
agent/test/fixtures/pvesh/qemu_100_config.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "nginx-proxy",
|
||||||
|
"cores": 2,
|
||||||
|
"memory": 2048,
|
||||||
|
"onboot": 1,
|
||||||
|
"scsi0": "local-zfs:vm-100-disk-0,size=32G",
|
||||||
|
"net0": "virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0"
|
||||||
|
}
|
||||||
76
agent/test/proxmox_agent/collectors/vms_test.exs
Normal file
76
agent/test/proxmox_agent/collectors/vms_test.exs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
defmodule ProxmoxAgent.Collectors.VmsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias ProxmoxAgent.Collectors.Vms
|
||||||
|
|
||||||
|
@fixtures Path.expand("../../fixtures/pvesh", __DIR__)
|
||||||
|
|
||||||
|
defp read!(name), do: File.read!(Path.join(@fixtures, name))
|
||||||
|
|
||||||
|
defp fake_runner do
|
||||||
|
fn
|
||||||
|
"pvesh", ["get", "/nodes/" <> rest, "--output-format", "json"] ->
|
||||||
|
cond do
|
||||||
|
String.ends_with?(rest, "/qemu") ->
|
||||||
|
{:ok, read!("qemu.json")}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/lxc") ->
|
||||||
|
{:ok, read!("lxc.json")}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/qemu/100/config") ->
|
||||||
|
{:ok, read!("qemu_100_config.json")}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/qemu/100/agent/network-get-interfaces") ->
|
||||||
|
{:ok, read!("qemu_100_agent_interfaces.json")}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/qemu/101/config") ->
|
||||||
|
{:ok, ~s({"name":"db-backup","cores":4,"memory":4096})}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/qemu/101/agent/network-get-interfaces") ->
|
||||||
|
{:error, {:nonzero_exit, 1, "QEMU guest agent is not running"}}
|
||||||
|
|
||||||
|
String.ends_with?(rest, "/lxc/200/config") ->
|
||||||
|
{:ok,
|
||||||
|
~s({"hostname":"minecraft","cores":4,"memory":4096,"net0":"name=eth0,ip=10.0.0.5/24"})}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "collect_runtime/1" do
|
||||||
|
test "returns qemu + lxc runtime info" do
|
||||||
|
sample = Vms.collect_runtime(node: "pve-01", runner: fake_runner())
|
||||||
|
|
||||||
|
assert length(sample.vms) == 3
|
||||||
|
nginx = Enum.find(sample.vms, &(&1.vmid == 100))
|
||||||
|
assert nginx.type == "qemu"
|
||||||
|
assert nginx.name == "nginx-proxy"
|
||||||
|
assert nginx.status == "running"
|
||||||
|
assert nginx.cpu_usage == 0.05
|
||||||
|
assert nginx.mem_bytes == 536_870_912
|
||||||
|
assert nginx.max_mem_bytes == 2_147_483_648
|
||||||
|
assert nginx.tags == ["web", "production"]
|
||||||
|
|
||||||
|
mc = Enum.find(sample.vms, &(&1.vmid == 200))
|
||||||
|
assert mc.type == "lxc"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "collect_detail/1" do
|
||||||
|
test "returns per-VM config + IPs" do
|
||||||
|
sample = Vms.collect_detail(node: "pve-01", runner: fake_runner())
|
||||||
|
|
||||||
|
nginx = Enum.find(sample.vms, &(&1.vmid == 100))
|
||||||
|
assert nginx.config["cores"] == 2
|
||||||
|
assert nginx.config["memory"] == 2048
|
||||||
|
assert "192.168.1.10" in nginx.ips
|
||||||
|
|
||||||
|
db = Enum.find(sample.vms, &(&1.vmid == 101))
|
||||||
|
assert db.config["cores"] == 4
|
||||||
|
assert db.ips == []
|
||||||
|
|
||||||
|
mc = Enum.find(sample.vms, &(&1.vmid == 200))
|
||||||
|
assert mc.config["hostname"] == "minecraft"
|
||||||
|
assert "10.0.0.5" in mc.ips
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue