proxMon/agent/lib/proxmox_agent/collectors/vms.ex

164 lines
4.5 KiB
Elixir

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