mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +02:00
feat(agent): report btrfs filesystems as storage pools (#2315)
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -32,13 +32,12 @@ func (sys *System) FetchAndSaveZfsPools(force bool) error {
|
||||
sys.recordZfsFetchResult(err, 0)
|
||||
return err
|
||||
}
|
||||
if zfsData == nil || !zfsData.Complete {
|
||||
err = errIncompleteZfsData
|
||||
sys.recordZfsFetchResult(err, 0)
|
||||
return err
|
||||
}
|
||||
err = sys.saveZfsPools(zfsData)
|
||||
sys.recordZfsFetchResult(err, len(zfsData.Pools))
|
||||
poolCount := 0
|
||||
if zfsData != nil {
|
||||
poolCount = len(zfsData.Pools)
|
||||
}
|
||||
sys.recordZfsFetchResult(err, poolCount)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,7 +78,7 @@ func (sys *System) zfsFetchInterval() time.Duration {
|
||||
// saveZfsPools saves ZFS pool detail data to the zfs_pools collection and
|
||||
// removes records for pools no longer reported by a complete agent inventory.
|
||||
func (sys *System) saveZfsPools(zfsData *zfs.ZfsData) error {
|
||||
if zfsData == nil || !zfsData.Complete {
|
||||
if zfsData == nil || (!zfsData.CanRefreshPool("zfs") && !zfsData.CanRefreshPool("b:")) {
|
||||
return errIncompleteZfsData
|
||||
}
|
||||
|
||||
@@ -89,10 +88,10 @@ func (sys *System) saveZfsPools(zfsData *zfs.ZfsData) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return hub.RunInTransaction(func(txApp core.App) error {
|
||||
err = hub.RunInTransaction(func(txApp core.App) error {
|
||||
alive := make(map[string]bool, len(zfsData.Pools))
|
||||
for _, pool := range zfsData.Pools {
|
||||
if pool == nil {
|
||||
if pool == nil || !zfsData.CanRefreshPool(pool.Name) {
|
||||
continue
|
||||
}
|
||||
alive[pool.Name] = true
|
||||
@@ -111,7 +110,7 @@ func (sys *System) saveZfsPools(zfsData *zfs.ZfsData) error {
|
||||
return err
|
||||
}
|
||||
for _, record := range existing {
|
||||
if !alive[record.GetString("name")] {
|
||||
if name := record.GetString("name"); zfsData.CanRefreshPool(name) && !alive[name] {
|
||||
if err := txApp.Delete(record); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,6 +118,14 @@ func (sys *System) saveZfsPools(zfsData *zfs.ZfsData) error {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Report partial failure only after committing healthy backend updates.
|
||||
if !zfsData.Complete {
|
||||
return errIncompleteZfsData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sys *System) upsertZfsPoolRecord(app core.App, collection *core.Collection, pool *zfs.PoolDetail) error {
|
||||
@@ -135,10 +142,12 @@ func (sys *System) upsertZfsPoolRecord(app core.App, collection *core.Collection
|
||||
|
||||
record.Set("system", sys.Id)
|
||||
record.Set("name", pool.Name)
|
||||
record.Set("display_name", pool.DisplayName)
|
||||
record.Set("health", pool.Health)
|
||||
record.Set("size", pool.Size)
|
||||
record.Set("alloc", pool.Alloc)
|
||||
record.Set("free", pool.Free)
|
||||
record.Set("raw", pool.Raw)
|
||||
record.Set("scrub", pool.Scrub)
|
||||
record.Set("vdevs", pool.Vdevs)
|
||||
record.Set("datasets", pool.Datasets)
|
||||
@@ -172,7 +181,9 @@ func (sys *System) syncZfsPoolHealth(app core.App, pools map[string]*system.ZfsP
|
||||
record.Set("id", recordID)
|
||||
record.Set("system", sys.Id)
|
||||
record.Set("name", name)
|
||||
record.Set("display_name", pool.DisplayName)
|
||||
record.Set("health", pool.Health)
|
||||
record.Set("raw", pool.Raw)
|
||||
record.Set("size", uint64(pool.Total*gib))
|
||||
record.Set("alloc", uint64(pool.Used*gib))
|
||||
record.Set("free", uint64(max(pool.Total-pool.Used, 0)*gib))
|
||||
@@ -181,10 +192,15 @@ func (sys *System) syncZfsPoolHealth(app core.App, pools map[string]*system.ZfsP
|
||||
}
|
||||
continue
|
||||
}
|
||||
if record.GetString("health") == pool.Health {
|
||||
if record.GetString("health") == pool.Health && record.GetBool("raw") == pool.Raw && record.GetString("display_name") == pool.DisplayName {
|
||||
continue
|
||||
}
|
||||
record.Set("display_name", pool.DisplayName)
|
||||
record.Set("health", pool.Health)
|
||||
record.Set("raw", pool.Raw)
|
||||
record.Set("size", uint64(pool.Total*gib))
|
||||
record.Set("alloc", uint64(pool.Used*gib))
|
||||
record.Set("free", uint64(max(pool.Total-pool.Used, 0)*gib))
|
||||
if err := app.SaveNoValidate(record); err != nil {
|
||||
return fmt.Errorf("updating ZFS pool health %q: %w", name, err)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,44 @@ func TestSaveZfsPoolsIncompletePreservesRecords(t *testing.T) {
|
||||
assert.Len(t, records, 1)
|
||||
}
|
||||
|
||||
func TestSavePartialBackendInventory(t *testing.T) {
|
||||
for _, healthy := range []string{"zfs", "btrfs"} {
|
||||
t.Run(healthy, func(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
healthyKey, failedKey := "tank", "b:uuid"
|
||||
if healthy == "btrfs" {
|
||||
healthyKey, failedKey = failedKey, healthyKey
|
||||
}
|
||||
initial := &zfs.ZfsData{Complete: true, Pools: []*zfs.PoolDetail{
|
||||
{Name: healthyKey, Alloc: 10}, {Name: failedKey, Alloc: 10},
|
||||
}}
|
||||
require.NoError(t, sys.saveZfsPools(initial))
|
||||
failedID := makeStableHashId(sys.Id, failedKey)
|
||||
before, err := app.FindRecordById("zfs_pools", failedID)
|
||||
require.NoError(t, err)
|
||||
partial := &zfs.ZfsData{CompleteBackends: []string{healthy}, Pools: []*zfs.PoolDetail{
|
||||
{Name: healthyKey, Alloc: 20}, {Name: failedKey, Alloc: 99},
|
||||
}}
|
||||
assert.ErrorIs(t, sys.saveZfsPools(partial), errIncompleteZfsData)
|
||||
fresh, err := app.FindRecordById("zfs_pools", makeStableHashId(sys.Id, healthyKey))
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 20, fresh.GetInt("alloc"))
|
||||
cached, err := app.FindRecordById("zfs_pools", failedID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 10, cached.GetInt("alloc"))
|
||||
assert.Equal(t, before.GetDateTime("details_updated"), cached.GetDateTime("details_updated"))
|
||||
// An empty successful backend can prune, even while the other fails.
|
||||
partial.Pools = nil
|
||||
assert.ErrorIs(t, sys.saveZfsPools(partial), errIncompleteZfsData)
|
||||
records, err := app.FindRecordsByFilter("zfs_pools", "system={:system}", "", 0, 0, map[string]any{"system": sys.Id})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 1)
|
||||
assert.Equal(t, failedKey, records[0].GetString("name"))
|
||||
require.NoError(t, sys.saveZfsPools(&zfs.ZfsData{Complete: true}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncZfsPoolHealthWritesOnlyTransitions(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
collection, err := app.FindCachedCollectionByNameOrId("zfs_pools")
|
||||
@@ -151,3 +189,42 @@ func TestSyncZfsPoolHealthWritesOnlyTransitions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "DEGRADED", record.GetString("health"))
|
||||
}
|
||||
|
||||
func TestZfsRawCapacityPersistence(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
require.NoError(t, sys.saveZfsPools(&zfs.ZfsData{Complete: true, Pools: []*zfs.PoolDetail{{Name: "btrfs", Size: 200, Alloc: 10, Raw: true}}}))
|
||||
record, err := app.FindRecordById("zfs_pools", makeStableHashId(sys.Id, "btrfs"))
|
||||
require.NoError(t, err)
|
||||
require.True(t, record.GetBool("raw"))
|
||||
require.NoError(t, sys.syncZfsPoolHealth(app, map[string]*system.ZfsPool{"btrfs": {Total: 1, Used: 0.25}}))
|
||||
record, err = app.FindRecordById("zfs_pools", record.Id)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, record.GetBool("raw"))
|
||||
assert.EqualValues(t, 1024*1024*1024, record.GetInt("size"))
|
||||
}
|
||||
|
||||
func TestBtrfsDisplayNameKeepsRecordIdentity(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
key := "b:11111111-1111-4111-8111-111111111111"
|
||||
require.NoError(t, sys.syncZfsPoolHealth(app, map[string]*system.ZfsPool{
|
||||
key: {DisplayName: "tank", Health: "ONLINE"},
|
||||
"tank": {Health: "ONLINE"},
|
||||
}))
|
||||
id := makeStableHashId(sys.Id, key)
|
||||
record, err := app.FindRecordById("zfs_pools", id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "tank", record.GetString("display_name"))
|
||||
require.NoError(t, sys.syncZfsPoolHealth(app, map[string]*system.ZfsPool{key: {DisplayName: "renamed", Health: "ONLINE"}}))
|
||||
record, err = app.FindRecordById("zfs_pools", id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key, record.GetString("name"))
|
||||
assert.Equal(t, "renamed", record.GetString("display_name"))
|
||||
require.NoError(t, sys.saveZfsPools(&zfs.ZfsData{Complete: true, Pools: []*zfs.PoolDetail{
|
||||
{Name: key, DisplayName: "detail name", Health: "ONLINE"}, {Name: "tank", Health: "ONLINE"},
|
||||
}}))
|
||||
record, err = app.FindRecordById("zfs_pools", id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "detail name", record.GetString("display_name"))
|
||||
_, err = app.FindRecordById("zfs_pools", makeStableHashId(sys.Id, "tank"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user