Tier 0's first slice, per novox/hq 03-DESIGN/01-to-be/05-the-node-host.md. It applies nothing, connects to nothing, listens on nothing. 2.9 MB, static, no dynamic dependencies: copy it onto a machine and run it is the whole install, which is the property ADR 0041 rests on. A capability is detected, never assumed. Every detector runs something that only succeeds if the thing FUNCTIONS — the daemon is asked for its version, the package database is queried, the firewall is asked to list a ruleset, which needs the privilege as well as the tool. 04-ISSUES/007 is the fault this prevents: a client on disk with its daemon down looks exactly like a working runtime, and a node assigned work on that basis fails when the work arrives. Every verdict carries the reason and the method. A capability reported absent with no reason is the same fault in a new place: something nobody can act on. Two bugs found by running rather than reasoning, both silent: systemctl is-system-running exits non-zero for every state except `running` — including `degraded`, which means units failed and the init is emphatically there. Reading the exit code reported NO service manager on a machine whose init it was. That is 007 in the mirror, and both directions place work wrongly. A verdict now reads what a tool says about itself, not only how it exited. And `mesh-host inventory --json` printed text: the standard library stops parsing at the first non-flag argument, so the flag sat unread and the command exited 0 having ignored what was asked. The parser now takes the subcommand off the front, and a stray or mistyped argument is refused rather than dropped. Detection deliberately does NOT follow ADR 0008. That rule governs applying state, where a failed step means the machine is not what was asked for. A failed probe is a finding — "absent, because the probe failed" — and aborting would replace one legible absence with total ignorance of the rest. 25 tests: structure and logic with a fake runner, and the same detectors against this machine, because a test that fakes the system under detection asserts only that the fake behaves as expected.
119 lines
4.1 KiB
Go
119 lines
4.1 KiB
Go
// Package profile answers one question: what can this machine be asked to do?
|
|
//
|
|
// A capability is DETECTED, never assumed. That distinction is the whole point of this
|
|
// package and it is not pedantry — novox/hq 04-ISSUES/007 records the fault it exists to
|
|
// prevent: an installed package was treated as a capability, and a node was assigned work it
|
|
// could not perform because the package was present and the thing was not running.
|
|
//
|
|
// So a capability here is not "is it installed". It is "does it work", and every detector
|
|
// says how it knows.
|
|
package profile
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Verdict is what a detector concluded, and why.
|
|
//
|
|
// Why is not decoration. A capability reported absent with no reason is the same problem in a
|
|
// new place: something that cannot be acted on. The reason is what a person reads when a node
|
|
// will not take work they expected it to take.
|
|
type Verdict struct {
|
|
Name string `json:"name"`
|
|
// Present is true only when the capability is usable, not merely installed.
|
|
Present bool `json:"present"`
|
|
// Detail says what was observed — a version, a path, or why it is absent.
|
|
Detail string `json:"detail"`
|
|
// How names the check that produced this, so a wrong answer can be found.
|
|
How string `json:"how"`
|
|
}
|
|
|
|
// Detector decides one capability. It is given a context so a hung probe cannot hang the host.
|
|
type Detector interface {
|
|
Name() string
|
|
Detect(ctx context.Context) Verdict
|
|
}
|
|
|
|
// Runner executes a command. Replaceable in tests for the pure-logic layer ONLY — every
|
|
// detector in this package is exercised against the real machine as well, because a test that
|
|
// fakes the system under detection asserts that the fake behaves as expected
|
|
// (novox/hq ADR 0034).
|
|
type Runner func(ctx context.Context, name string, args ...string) (stdout string, err error)
|
|
|
|
// ExecRunner runs a real command, with output captured and stdin closed.
|
|
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
cmd.Stdin = nil
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
var exit *exec.ExitError
|
|
if errors.As(err, &exit) {
|
|
return string(out), fmt.Errorf("%s exited %d: %s",
|
|
name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr)))
|
|
}
|
|
return string(out), fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// Profile is every verdict, in a stable order.
|
|
type Profile struct {
|
|
Architecture string `json:"architecture"`
|
|
Kernel string `json:"kernel"`
|
|
Capabilities []Verdict `json:"capabilities"`
|
|
}
|
|
|
|
// Has reports whether a named capability is present. Unknown names are absent, not an error:
|
|
// asking about a capability nothing detects is a question with a true answer.
|
|
func (p Profile) Has(name string) bool {
|
|
for _, v := range p.Capabilities {
|
|
if v.Name == name {
|
|
return v.Present
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Missing lists the names that are not present, in order. What a node cannot do is the half
|
|
// that decides whether work may be placed on it.
|
|
func (p Profile) Missing() []string {
|
|
var out []string
|
|
for _, v := range p.Capabilities {
|
|
if !v.Present {
|
|
out = append(out, v.Name)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Detect runs every detector and collects the verdicts.
|
|
//
|
|
// A detector that fails does not fail the profile. This is deliberately NOT the rule in
|
|
// novox/hq ADR 0008: that rule governs applying state, where a failed step means the machine
|
|
// is not what was asked for. Detection is the opposite — a failed probe is a finding, and the
|
|
// finding is "absent, because the probe failed", which is exactly what a caller needs to know.
|
|
// Aborting would replace one legible absence with total ignorance.
|
|
func Detect(ctx context.Context, detectors []Detector, timeout time.Duration) Profile {
|
|
p := Profile{
|
|
Architecture: runtime.GOARCH,
|
|
Kernel: runtime.GOOS,
|
|
}
|
|
for _, d := range detectors {
|
|
probeCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
p.Capabilities = append(p.Capabilities, d.Detect(probeCtx))
|
|
cancel()
|
|
}
|
|
sort.Slice(p.Capabilities, func(i, j int) bool {
|
|
return p.Capabilities[i].Name < p.Capabilities[j].Name
|
|
})
|
|
return p
|
|
}
|