From 1a1f6998a4c259c99dcbfeaa1ec2520ca8bee7bd Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Thu, 17 Sep 2026 08:37:41 +0000 Subject: [PATCH] feat(inventory): record when a reboot became required --- server/internal/services/inventory.go | 35 ++++++++++++++++++- .../services/inventory_reboot_test.go | 24 +++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 server/internal/services/inventory_reboot_test.go diff --git a/server/internal/services/inventory.go b/server/internal/services/inventory.go index 1f0a22d..2c81352 100644 --- a/server/internal/services/inventory.go +++ b/server/internal/services/inventory.go @@ -7,6 +7,7 @@ import ( "gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db" "gitea.hostxtra.co.uk/vantage/vantage-shared/grpc/pb" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) func StoreInventory(serverID string, r *pb.InventoryReport) error { @@ -29,6 +30,7 @@ func StoreInventory(serverID string, r *pb.InventoryReport) error { set["inventory.boot_time"] = time.Unix(r.BootTimeUnix, 0).UTC() } + var unset bson.M if r.IncludeStatic { set["inventory.static_at"] = now set["inventory.swap_total_bytes"] = r.SwapTotal @@ -49,8 +51,39 @@ func StoreInventory(serverID string, r *pb.InventoryReport) error { }) } set["inventory.partitions"] = parts + + var prev struct { + Inventory struct { + RebootRequired bool `bson:"reboot_required"` + RebootRequiredSince *time.Time `bson:"reboot_required_since"` + } `bson:"inventory"` + } + _ = db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID}, + options.FindOne().SetProjection(bson.M{"inventory.reboot_required": 1, "inventory.reboot_required_since": 1})).Decode(&prev) + if since, clear := rebootSinceUpdate(prev.Inventory.RebootRequired, prev.Inventory.RebootRequiredSince, r.RebootRequired, now); since != nil { + set["inventory.reboot_required_since"] = *since + } else if clear { + unset = bson.M{"inventory.reboot_required_since": ""} + } } - _, err := db.Col("servers").UpdateOne(ctx, bson.M{"server_id": serverID}, bson.M{"$set": set}) + upd := bson.M{"$set": set} + if unset != nil { + upd["$unset"] = unset + } + _, err := db.Col("servers").UpdateOne(ctx, bson.M{"server_id": serverID}, upd) return err } + +// rebootSinceUpdate decides how reboot_required_since changes. The stamp is +// kept from the first report that needed a reboot, so "pending for 7 days" +// means seven days, not seven days since the last static report. +func rebootSinceUpdate(prevRequired bool, prevSince *time.Time, nowRequired bool, now time.Time) (*time.Time, bool) { + if !nowRequired { + return nil, prevSince != nil || prevRequired + } + if prevSince == nil { + return &now, false + } + return nil, false +} diff --git a/server/internal/services/inventory_reboot_test.go b/server/internal/services/inventory_reboot_test.go new file mode 100644 index 0000000..4c42136 --- /dev/null +++ b/server/internal/services/inventory_reboot_test.go @@ -0,0 +1,24 @@ +package services + +import ( + "testing" + "time" +) + +func TestRebootSinceUpdate(t *testing.T) { + now := time.Date(2026, 9, 17, 0, 0, 0, 0, time.UTC) + earlier := now.Add(-72 * time.Hour) + + if set, unset := rebootSinceUpdate(false, nil, true, now); set == nil || !set.Equal(now) || unset { + t.Fatal("turning on must stamp now") + } + if set, unset := rebootSinceUpdate(true, &earlier, true, now); set != nil || unset { + t.Fatal("staying on must keep the original stamp") + } + if set, _ := rebootSinceUpdate(true, nil, true, now); set == nil { + t.Fatal("on with no stamp (pre-upgrade data) must stamp now") + } + if set, unset := rebootSinceUpdate(true, &earlier, false, now); set != nil || !unset { + t.Fatal("turning off must clear the stamp") + } +}