Addresses final code review: - to_int/1 now returns 0 on nil or unparseable strings instead of crashing - remove unused .pool-row CSS (superseded by .pool-block) - clamp capacity bar width to [0, 100] to prevent visual overflow - pool_scrub_line/1 uses scan_function so resilver shows as "resilver..."
159 lines
5.8 KiB
Elixir
159 lines
5.8 KiB
Elixir
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
|