defmodule ProxmoxAgent.Collectors.ZfsTest do use ExUnit.Case, async: true alias ProxmoxAgent.Collectors.Zfs @fixtures Path.expand("../../fixtures/zfs", __DIR__) defp fake_runner do fn "zpool", ["list", "-j", "--json-int"] -> {:ok, File.read!(Path.join(@fixtures, "zpool_list.json"))} "zpool", ["status", "-j", "--json-flat-vdevs", "--json-int"] -> {:ok, File.read!(Path.join(@fixtures, "zpool_status.json"))} "zfs", ["list", "-j", "--json-int", "-t", "all"] -> {:ok, File.read!(Path.join(@fixtures, "zfs_list.json"))} end end describe "collect_pools/1" do test "returns a summary per pool" do sample = Zfs.collect_pools(runner: fake_runner()) assert is_list(sample.pools) assert length(sample.pools) == 2 rpool = Enum.find(sample.pools, &(&1.name == "rpool")) tank = Enum.find(sample.pools, &(&1.name == "tank")) assert rpool.health == "ONLINE" assert rpool.capacity_percent == 40 assert rpool.fragmentation_percent == 17 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 failing = fn _, _ -> {:error, {:enoent, "zpool"}} end sample = Zfs.collect_pools(runner: failing) assert sample.pools == [] assert length(sample.errors) >= 1 end test "classifies pool_type for stripe, mixed, and special vdevs" do list_json = Jason.encode!(%{ "pools" => %{ "stripe" => %{"name" => "stripe", "size" => 1, "alloc" => 0, "free" => 1, "frag" => 0, "cap" => 0, "health" => "ONLINE"}, "mixed" => %{"name" => "mixed", "size" => 1, "alloc" => 0, "free" => 1, "frag" => 0, "cap" => 0, "health" => "ONLINE"}, "mirror_with_log" => %{"name" => "mirror_with_log", "size" => 1, "alloc" => 0, "free" => 1, "frag" => 0, "cap" => 0, "health" => "ONLINE"} } }) vdev = fn name, type -> {name, %{"name" => name, "vdev_type" => type, "state" => "ONLINE", "read_errors" => "0", "write_errors" => "0", "checksum_errors" => "0"}} end status_json = Jason.encode!(%{ "pools" => %{ "stripe" => %{ "name" => "stripe", "state" => "ONLINE", "error_count" => "0", "vdevs" => Map.new([vdev.("sda", "disk"), vdev.("sdb", "disk")]) }, "mixed" => %{ "name" => "mixed", "state" => "ONLINE", "error_count" => "0", "vdevs" => Map.new([vdev.("mirror-0", "mirror"), vdev.("raidz1-1", "raidz1")]) }, "mirror_with_log" => %{ "name" => "mirror_with_log", "state" => "ONLINE", "error_count" => "0", "vdevs" => Map.new([vdev.("mirror-0", "mirror"), vdev.("log-0", "log")]) } } }) runner = fn "zpool", ["list" | _] -> {:ok, list_json} "zpool", ["status" | _] -> {:ok, status_json} end sample = Zfs.collect_pools(runner: runner) by_name = Map.new(sample.pools, &{&1.name, &1}) assert by_name["stripe"].pool_type == "stripe" assert by_name["mixed"].pool_type == "mixed" assert by_name["mirror_with_log"].pool_type == "mirror" # log vdev is retained in the per-pool vdevs list even though it's ignored for layout classification assert Enum.any?(by_name["mirror_with_log"].vdevs, &(&1.type == "log")) end test "tolerates nil and non-integer error counters" do list_json = Jason.encode!(%{ "pools" => %{ "weird" => %{"name" => "weird", "size" => 1, "alloc" => 0, "free" => 1, "frag" => 0, "cap" => 0, "health" => "ONLINE"} } }) status_json = Jason.encode!(%{ "pools" => %{ "weird" => %{ "name" => "weird", "state" => "ONLINE", "error_count" => nil, "vdevs" => %{ "disk-0" => %{"name" => "disk-0", "vdev_type" => "disk", "state" => "ONLINE", "read_errors" => nil, "write_errors" => "abc", "checksum_errors" => "0"} } } } }) runner = fn "zpool", ["list" | _] -> {:ok, list_json} "zpool", ["status" | _] -> {:ok, status_json} end sample = Zfs.collect_pools(runner: runner) [pool] = sample.pools assert pool.error_count == 0 [vdev] = pool.vdevs assert vdev.read_errors == 0 assert vdev.write_errors == 0 assert vdev.checksum_errors == 0 end end describe "collect_datasets/1" do test "returns datasets and per-dataset snapshot summary" do sample = Zfs.collect_datasets(runner: fake_runner()) assert length(sample.datasets) == 2 rpool_data = Enum.find(sample.datasets, &(&1.name == "rpool/data")) assert rpool_data.used_bytes == 100_000_000_000 assert rpool_data.usedbysnapshots_bytes == 2_000_000_000 assert rpool_data.snapshot_count == 2 assert rpool_data.newest_snapshot_unix == 1_745_193_600 assert rpool_data.oldest_snapshot_unix == 1_745_107_200 end end end