From e763ea96bd5fd8e3aa4a2e5fd8837578decd78cf Mon Sep 17 00:00:00 2001 From: Carsten Date: Wed, 22 Apr 2026 17:44:07 +0200 Subject: [PATCH] feat(agent): enrich zpool summary with type, scan state, vdev list --- agent/lib/proxmox_agent/collectors/zfs.ex | 55 +++++++++++++++++-- agent/test/fixtures/zfs/zpool_status.json | 2 +- .../proxmox_agent/collectors/zfs_test.exs | 9 +++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/agent/lib/proxmox_agent/collectors/zfs.ex b/agent/lib/proxmox_agent/collectors/zfs.ex index 933ba70..4e8b4da 100644 --- a/agent/lib/proxmox_agent/collectors/zfs.ex +++ b/agent/lib/proxmox_agent/collectors/zfs.ex @@ -4,6 +4,15 @@ defmodule ProxmoxAgent.Collectors.Zfs do Delegates shelling out to an injectable runner so tests can supply fixtures. """ + @type vdev_summary :: %{ + name: String.t(), + type: String.t(), + state: String.t(), + read_errors: non_neg_integer(), + write_errors: non_neg_integer(), + checksum_errors: non_neg_integer() + } + @type pool_summary :: %{ name: String.t(), health: String.t(), @@ -15,7 +24,11 @@ defmodule ProxmoxAgent.Collectors.Zfs do error_count: non_neg_integer(), vdev_count: non_neg_integer(), degraded_vdev_count: non_neg_integer(), - last_scrub_end: String.t() | nil + pool_type: String.t(), + scan_function: String.t() | nil, + scan_state: String.t() | nil, + last_scrub_end: String.t() | nil, + vdevs: [vdev_summary()] } @spec collect_pools(keyword()) :: %{pools: [pool_summary()], errors: [map()]} @@ -71,7 +84,8 @@ defmodule ProxmoxAgent.Collectors.Zfs do defp merge_pools(%{"pools" => list_pools}, %{"pools" => status_pools}) do Enum.map(list_pools, fn {name, list_info} -> status_info = Map.get(status_pools, name, %{}) - vdevs = Map.get(status_info, "vdevs", %{}) |> Map.values() + raw_vdevs = Map.get(status_info, "vdevs", %{}) |> Map.values() + vdevs = Enum.map(raw_vdevs, &vdev_summary/1) %{ name: name, @@ -83,12 +97,45 @@ defmodule ProxmoxAgent.Collectors.Zfs do capacity_percent: Map.get(list_info, "cap", 0), error_count: to_int(Map.get(status_info, "error_count", "0")), vdev_count: length(vdevs), - degraded_vdev_count: Enum.count(vdevs, &(&1["state"] != "ONLINE")), - last_scrub_end: get_in(status_info, ["scan", "end_time"]) + degraded_vdev_count: Enum.count(vdevs, &(&1.state != "ONLINE")), + pool_type: derive_pool_type(vdevs), + scan_function: get_in(status_info, ["scan", "function"]), + scan_state: get_in(status_info, ["scan", "state"]), + last_scrub_end: get_in(status_info, ["scan", "end_time"]), + vdevs: vdevs } end) end + defp vdev_summary(v) do + %{ + name: Map.get(v, "name"), + type: Map.get(v, "vdev_type"), + state: Map.get(v, "state"), + read_errors: to_int(Map.get(v, "read_errors", "0")), + write_errors: to_int(Map.get(v, "write_errors", "0")), + checksum_errors: to_int(Map.get(v, "checksum_errors", "0")) + } + end + + @data_vdev_types ~w(mirror raidz1 raidz2 raidz3 disk) + @special_vdev_types ~w(log cache spare dedup special) + + defp derive_pool_type(vdevs) do + data_types = + vdevs + |> Enum.map(& &1.type) + |> Enum.reject(&(&1 in @special_vdev_types)) + |> Enum.uniq() + + case data_types do + [] -> "unknown" + ["disk"] -> "stripe" + [t] when t in @data_vdev_types -> t + _ -> "mixed" + end + end + defp summarize_datasets(nil), do: [] defp summarize_datasets(%{"datasets" => datasets}) do diff --git a/agent/test/fixtures/zfs/zpool_status.json b/agent/test/fixtures/zfs/zpool_status.json index b066e78..693a779 100644 --- a/agent/test/fixtures/zfs/zpool_status.json +++ b/agent/test/fixtures/zfs/zpool_status.json @@ -26,7 +26,7 @@ "state": "DEGRADED", "scan": { "function": "scrub", - "state": "FINISHED", + "state": "SCANNING", "end_time": "Tue Mar 01 08:00:00 2026" }, "error_count": "2", diff --git a/agent/test/proxmox_agent/collectors/zfs_test.exs b/agent/test/proxmox_agent/collectors/zfs_test.exs index e3220d1..0a8bbdd 100644 --- a/agent/test/proxmox_agent/collectors/zfs_test.exs +++ b/agent/test/proxmox_agent/collectors/zfs_test.exs @@ -32,10 +32,19 @@ defmodule ProxmoxAgent.Collectors.ZfsTest do assert rpool.size_bytes == 500_000_000_000 assert rpool.error_count == 0 assert rpool.degraded_vdev_count == 0 + assert rpool.pool_type == "mirror" + assert rpool.scan_function == "scrub" + assert rpool.scan_state == "FINISHED" + assert [%{name: "mirror-0", type: "mirror", state: "ONLINE", + read_errors: 0, write_errors: 0, checksum_errors: 0}] = rpool.vdevs assert tank.health == "DEGRADED" assert tank.error_count == 2 assert tank.degraded_vdev_count == 1 + assert tank.pool_type == "raidz2" + assert tank.scan_state == "SCANNING" + assert [%{name: "raidz2-0", type: "raidz2", state: "DEGRADED", + checksum_errors: 2}] = tank.vdevs end test "populates errors list when zpool fails" do