Show what a machine said it is made of in node, with its age (issue 160)
The architecture and kernel a machine reported were kept and read by nothing, and the facts the node-engine now says (memory, filesystems, processor, distribution, virtual or physical, uptime, timezone) would have joined them. node shows every one in one block with the age of the account, and a fact the machine could not determine as unknown with its why, never as a zero.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// MachineAccount is what a machine said it is made of (novox/hq issue 160), from the profile it last
|
||||
// reported: the architecture and kernel family the profile always carried, and the facts a node-engine that
|
||||
// knows how says beside them. Read by `node`, so what a machine says of itself is visible in one place —
|
||||
// collected and shown in the same breath, not collected and read by nothing (issue 160's pattern).
|
||||
type MachineAccount struct {
|
||||
Architecture string `json:"architecture"`
|
||||
Kernel string `json:"kernel"`
|
||||
// Machine is nil when the node-engine that reported is older than the one that says it.
|
||||
Machine *MadeOf `json:"machine"`
|
||||
}
|
||||
|
||||
// MadeOf is the node-engine's account of what it is made of, as it put it on the wire. A fact it could
|
||||
// not determine is absent here and named in Unknown with why — never a zero.
|
||||
type MadeOf struct {
|
||||
ObservedAt time.Time `json:"observed_at"`
|
||||
KernelRelease string `json:"kernel_release"`
|
||||
Distribution string `json:"distribution"`
|
||||
Version string `json:"version"`
|
||||
Processor string `json:"processor"`
|
||||
Cores int `json:"cores"`
|
||||
Threads int `json:"threads"`
|
||||
MemoryBytes int64 `json:"memory_bytes"`
|
||||
Virtual string `json:"virtual"`
|
||||
BootedAt *time.Time `json:"booted_at"`
|
||||
Timezone string `json:"timezone"`
|
||||
Filesystems []MountedFilesystem `json:"filesystems"`
|
||||
Unknown map[string]string `json:"unknown"`
|
||||
}
|
||||
|
||||
// MountedFilesystem is one local filesystem a machine has mounted.
|
||||
type MountedFilesystem struct {
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
Type string `json:"type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
FreeBytes int64 `json:"free_bytes"`
|
||||
}
|
||||
|
||||
// MachineOf is what a node last said it is made of. had is false for a node that has never reported a
|
||||
// profile at all, which is a different answer from one whose node-engine does not say the facts yet.
|
||||
func (i *Inventory) MachineOf(ctx context.Context, nodeName string) (account MachineAccount, had bool, err error) {
|
||||
var raw []byte
|
||||
err = i.store.Pool().QueryRow(ctx, `select profile from node where name = $1`, nodeName).Scan(&raw)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return MachineAccount{}, false, fmt.Errorf("%w: %s", ErrNoSuchNode, nodeName)
|
||||
}
|
||||
if err != nil {
|
||||
return MachineAccount{}, false, err
|
||||
}
|
||||
return machineFrom(raw)
|
||||
}
|
||||
|
||||
func machineFrom(raw []byte) (MachineAccount, bool, error) {
|
||||
if len(raw) == 0 {
|
||||
return MachineAccount{}, false, nil
|
||||
}
|
||||
var account MachineAccount
|
||||
if err := json.Unmarshal(raw, &account); err != nil {
|
||||
return MachineAccount{}, false, err
|
||||
}
|
||||
return account, true, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The node-engine's words for a machine's facts (novox/hq issue 160), as mesh-host's profile.Machine puts
|
||||
// them on the wire. A rename on either side fails here rather than as a blank on a real machine.
|
||||
func TestAMachinesAccountIsReadInTheNodeEnginesWords(t *testing.T) {
|
||||
raw := []byte(`{"architecture":"amd64","kernel":"linux","capabilities":[],
|
||||
"machine":{"observed_at":"2026-10-12T15:00:00Z","kernel_release":"7.2.8-arch1-2",
|
||||
"distribution":"Arch Linux","version":"rolling","processor":"AMD Ryzen 9","cores":8,"threads":16,
|
||||
"memory_bytes":32891494400,"virtual":"physical","booted_at":"2026-10-09T08:50:34Z",
|
||||
"timezone":"Europe/Brussels",
|
||||
"filesystems":[{"mount":"/","device":"/dev/nvme0n1p3","type":"ext4","size_bytes":100,"free_bytes":40}],
|
||||
"unknown":{"public address":"not decided"}}}`)
|
||||
a, had, err := machineFrom(raw)
|
||||
if err != nil || !had {
|
||||
t.Fatalf("had %v, err %v", had, err)
|
||||
}
|
||||
m := a.Machine
|
||||
if a.Architecture != "amd64" || a.Kernel != "linux" || m == nil {
|
||||
t.Fatalf("%+v", a)
|
||||
}
|
||||
if m.KernelRelease != "7.2.8-arch1-2" || m.Distribution != "Arch Linux" || m.Version != "rolling" ||
|
||||
m.Processor != "AMD Ryzen 9" || m.Cores != 8 || m.Threads != 16 || m.MemoryBytes != 32891494400 ||
|
||||
m.Virtual != "physical" || m.BootedAt == nil || m.Timezone != "Europe/Brussels" ||
|
||||
len(m.Filesystems) != 1 || m.Filesystems[0].FreeBytes != 40 || m.Unknown["public address"] == "" ||
|
||||
m.ObservedAt.IsZero() {
|
||||
t.Errorf("%+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAProfileWithoutTheFactsIsAnOlderNodeEngineNotNothing(t *testing.T) {
|
||||
a, had, err := machineFrom([]byte(`{"architecture":"amd64","kernel":"linux","capabilities":[]}`))
|
||||
if err != nil || !had || a.Machine != nil || a.Architecture != "amd64" {
|
||||
t.Errorf("%+v had %v err %v", a, had, err)
|
||||
}
|
||||
if _, had, _ := machineFrom(nil); had {
|
||||
t.Error("a node that never reported read as having reported")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user