mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-22 06:12:33 +00:00
fix(agent): report ZFS pool capacity from the root dataset (#2540)
The pool overview used `zpool list`, whose size and alloc count raw vdev space. On raidz that includes parity, so a TrueNAS pool with 65 TB usable showed 98 TB capacity, while the details page (root dataset) matched TrueNAS. Take Size, Alloc and Free from the pool's root dataset (used+avail), the same way Btrfs reports effective capacity. If the root dataset cannot be read, keep the raw figures and set Raw so the UI labels them as raw usage. Fixes #2539
This commit is contained in:
+32
-2
@@ -68,7 +68,8 @@ type Dataset struct {
|
||||
}
|
||||
|
||||
// PoolStats returns capacity and health for all pools on the system using
|
||||
// `zpool list`. Frequent health and I/O sampling uses PoolKernelStats instead.
|
||||
// `zpool list`, with capacity taken from each pool's root dataset where it can
|
||||
// be read. Frequent health and I/O sampling uses PoolKernelStats instead.
|
||||
func PoolStats() ([]PoolStat, error) {
|
||||
if err := checkZfsDevice(); err != nil {
|
||||
return nil, err
|
||||
@@ -81,7 +82,36 @@ func PoolStats() ([]PoolStat, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("zpool list: %w", err)
|
||||
}
|
||||
return parseZpoolListOutput(out)
|
||||
pools, err := parseZpoolListOutput(out)
|
||||
if err != nil || len(pools) == 0 {
|
||||
return pools, err
|
||||
}
|
||||
applyRootDatasetCapacity(pools)
|
||||
return pools, nil
|
||||
}
|
||||
|
||||
// applyRootDatasetCapacity replaces the raw `zpool list` figures, which include
|
||||
// parity on raidz and dRAID vdevs, with each pool's root dataset used and avail.
|
||||
// As with Btrfs, the effective capacity is used+avail, so Size-Alloc is what
|
||||
// applications can still write and the usage ratio matches df. A pool whose
|
||||
// root dataset cannot be read keeps its raw figures and is flagged as Raw.
|
||||
func applyRootDatasetCapacity(pools []PoolStat) {
|
||||
roots := make(map[string]Dataset)
|
||||
if out, err := commandOutput("zfs", "list", "-Hp", "-d", "0", "-o", "name,used,avail,mountpoint"); err == nil {
|
||||
if datasets, err := parseZfsListOutput(out); err == nil {
|
||||
for _, ds := range datasets {
|
||||
roots[ds.Name] = ds
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range pools {
|
||||
root, ok := roots[pools[i].Name]
|
||||
if !ok {
|
||||
pools[i].Raw = true
|
||||
continue
|
||||
}
|
||||
pools[i].Size, pools[i].Alloc, pools[i].Free = root.Used+root.Avail, root.Used, root.Avail
|
||||
}
|
||||
}
|
||||
|
||||
// Datasets returns all datasets on the system with usage and mountpoint
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package zfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -260,15 +261,55 @@ func TestPoolStatsDelegatesToZpoolWhenDevZfsPresent(t *testing.T) {
|
||||
oldCommandOutput := commandOutput
|
||||
called := false
|
||||
commandOutput = func(name string, args ...string) ([]byte, error) {
|
||||
called = true
|
||||
assert.Equal(t, "zpool", name)
|
||||
assert.Equal(t, []string{"list", "-Hp", "-o", "name,size,alloc,free,health"}, args)
|
||||
return []byte("tank\t100\t50\t50\tONLINE\n"), nil
|
||||
switch name {
|
||||
case "zpool":
|
||||
called = true
|
||||
assert.Equal(t, []string{"list", "-Hp", "-o", "name,size,alloc,free,health"}, args)
|
||||
// raidz: the raw figures include parity
|
||||
return []byte("tank\t150\t60\t90\tONLINE\n"), nil
|
||||
case "zfs":
|
||||
assert.Equal(t, []string{"list", "-Hp", "-d", "0", "-o", "name,used,avail,mountpoint"}, args)
|
||||
return []byte("tank\t40\t60\t/tank\n"), nil
|
||||
}
|
||||
t.Fatalf("unexpected %s call with %v", name, args)
|
||||
return nil, nil
|
||||
}
|
||||
t.Cleanup(func() { commandOutput = oldCommandOutput })
|
||||
|
||||
pools, err := PoolStats()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, []PoolStat{{Name: "tank", Size: 100, Alloc: 50, Free: 50, Health: "ONLINE"}}, pools)
|
||||
assert.Equal(t, []PoolStat{{Name: "tank", Size: 100, Alloc: 40, Free: 60, Health: "ONLINE"}}, pools)
|
||||
}
|
||||
|
||||
func TestPoolStatsKeepsRawCapacityWithoutRootDataset(t *testing.T) {
|
||||
devFile := filepath.Join(t.TempDir(), "zfs")
|
||||
require.NoError(t, os.WriteFile(devFile, nil, 0o644))
|
||||
oldDevZfsPath := devZfsPath
|
||||
devZfsPath = devFile
|
||||
t.Cleanup(func() { devZfsPath = oldDevZfsPath })
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
zfsOut []byte
|
||||
zfsErr error
|
||||
}{
|
||||
{"zfs list fails", nil, errors.New("zfs list failed")},
|
||||
{"root dataset missing", []byte("other\t1\t1\t/other\n"), nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
oldCommandOutput := commandOutput
|
||||
commandOutput = func(name string, args ...string) ([]byte, error) {
|
||||
if name == "zpool" {
|
||||
return []byte("tank\t150\t60\t90\tONLINE\n"), nil
|
||||
}
|
||||
return tc.zfsOut, tc.zfsErr
|
||||
}
|
||||
t.Cleanup(func() { commandOutput = oldCommandOutput })
|
||||
|
||||
pools, err := PoolStats()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []PoolStat{{Name: "tank", Size: 150, Alloc: 60, Free: 90, Health: "ONLINE", Raw: true}}, pools)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user