From ec7f08dfda41381a4b6cfd4373bc264de5edc815 Mon Sep 17 00:00:00 2001 From: Carsten Date: Tue, 21 Apr 2026 22:33:27 +0200 Subject: [PATCH] feat(agent): pvesh storage collector --- agent/lib/proxmox_agent/collectors/storage.ex | 37 +++++++++++++++++++ agent/test/fixtures/pvesh/storage.json | 35 ++++++++++++++++++ .../proxmox_agent/collectors/storage_test.exs | 36 ++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 agent/lib/proxmox_agent/collectors/storage.ex create mode 100644 agent/test/fixtures/pvesh/storage.json create mode 100644 agent/test/proxmox_agent/collectors/storage_test.exs diff --git a/agent/lib/proxmox_agent/collectors/storage.ex b/agent/lib/proxmox_agent/collectors/storage.ex new file mode 100644 index 0000000..7f16a54 --- /dev/null +++ b/agent/lib/proxmox_agent/collectors/storage.ex @@ -0,0 +1,37 @@ +defmodule ProxmoxAgent.Collectors.Storage do + @moduledoc "Collects Proxmox storage summary via pvesh." + + @spec collect(keyword()) :: %{storages: [map()], errors: [map()]} + def collect(opts \\ []) do + runner = Keyword.get(opts, :runner, &ProxmoxAgent.Shell.run/2) + node = Keyword.fetch!(opts, :node) + + case runner.("pvesh", ["get", "/nodes/#{node}/storage", "--output-format", "json"]) do + {:ok, body} -> + case Jason.decode(body) do + {:ok, list} when is_list(list) -> + %{storages: Enum.map(list, &normalize/1), errors: []} + + {:error, e} -> + %{storages: [], errors: [%{tag: "decode", message: Exception.message(e)}]} + end + + {:error, reason} -> + %{storages: [], errors: [%{tag: "pvesh", message: inspect(reason)}]} + end + end + + defp normalize(entry) do + %{ + name: entry["storage"], + type: entry["type"], + content: entry["content"], + active: entry["active"] == 1, + enabled: entry["enabled"] == 1, + used_bytes: entry["used"] || 0, + total_bytes: entry["total"] || 0, + avail_bytes: entry["avail"] || 0, + used_fraction: entry["used_fraction"] || 0.0 + } + end +end diff --git a/agent/test/fixtures/pvesh/storage.json b/agent/test/fixtures/pvesh/storage.json new file mode 100644 index 0000000..f371ad0 --- /dev/null +++ b/agent/test/fixtures/pvesh/storage.json @@ -0,0 +1,35 @@ +[ + { + "storage": "local", + "type": "dir", + "content": "backup,iso,vztmpl", + "active": 1, + "enabled": 1, + "used": 50000000000, + "total": 500000000000, + "avail": 450000000000, + "used_fraction": 0.1 + }, + { + "storage": "local-zfs", + "type": "zfspool", + "content": "images,rootdir", + "active": 1, + "enabled": 1, + "used": 200000000000, + "total": 500000000000, + "avail": 300000000000, + "used_fraction": 0.4 + }, + { + "storage": "backup-nfs", + "type": "nfs", + "content": "backup", + "active": 0, + "enabled": 1, + "used": 0, + "total": 0, + "avail": 0, + "used_fraction": 0.0 + } +] diff --git a/agent/test/proxmox_agent/collectors/storage_test.exs b/agent/test/proxmox_agent/collectors/storage_test.exs new file mode 100644 index 0000000..1d38b0c --- /dev/null +++ b/agent/test/proxmox_agent/collectors/storage_test.exs @@ -0,0 +1,36 @@ +defmodule ProxmoxAgent.Collectors.StorageTest do + use ExUnit.Case, async: true + + alias ProxmoxAgent.Collectors.Storage + + @fixtures Path.expand("../../fixtures/pvesh", __DIR__) + + defp fake_runner do + fn + "pvesh", ["get", "/nodes/" <> _, "--output-format", "json"] -> + {:ok, File.read!(Path.join(@fixtures, "storage.json"))} + end + end + + test "returns one summary per storage entry" do + sample = Storage.collect(node: "pve-01", runner: fake_runner()) + + assert length(sample.storages) == 3 + local = Enum.find(sample.storages, &(&1.name == "local")) + assert local.type == "dir" + assert local.active == true + assert local.used_bytes == 50_000_000_000 + assert local.total_bytes == 500_000_000_000 + assert local.content == "backup,iso,vztmpl" + + nfs = Enum.find(sample.storages, &(&1.name == "backup-nfs")) + assert nfs.active == false + end + + test "populates errors on failure" do + failing = fn _, _ -> {:error, {:enoent, "pvesh"}} end + sample = Storage.collect(node: "pve-01", runner: failing) + assert sample.storages == [] + assert sample.errors != [] + end +end