Files
mesh-host/internal/system/arch.go
T
jschoubben e7f94e0402 A one-shot service that finished is not stopped, and a container is what it reads
Two faults that both reported success while being wrong, found while proving
the firewall module actually delivers.

A unit whose job is to apply something and exit — load a rule set, set a
sysctl — is inactive the instant it succeeds. Reading that as stopped made it
permanently unsatisfiable: the host started it, it worked, the host read back
stopped and reported the machine as not doing what it was told, on every apply,
for ever, with the rules correctly in place the whole time. That is what the
firewall has been doing on every machine it was assigned to, and why the
four-machine bed was red.

And a container took its identity from its own fields, not from the files it
reads. A file written in an earlier apply — or before the container declared it
as a dependency — left a process holding a credential the mesh had already
replaced, with everything reporting success (novox/hq 04-ISSUES/045). What a
container reads is now part of what it is, so the comparison is a standing one
rather than a tripwire that fires during one apply and never again.
2026-09-14 16:51:57 +02:00

249 lines
9.9 KiB
Go

package system
import (
"context"
"fmt"
"strings"
"github.com/novox/mesh-host/internal/declaration"
)
// arch is pacman and systemd.
type arch struct{}
func (arch) Name() string { return "arch" }
func (arch) Shapes() []declaration.Type { return everyShape() }
func (a arch) Confirm(ctx context.Context, run Runner) error {
if _, err := run(ctx, "pacman", "-Q", "pacman"); err != nil {
return fmt.Errorf(
"this is the arch host and pacman does not answer here. Either this machine is not "+
"Arch, or its package database is broken: %w", err)
}
return nil
}
// PackageInstalled asks the package database, having first established that it answers.
//
// The two-step is the trap this file exists to remember. `pacman -Q name` exits non-zero for a
// package that is not installed AND for a database that cannot be read, so believing the first
// answer reports a broken package manager as "nothing is installed" — absence read as fact.
// Proving the tool answers about something that certainly exists separates them.
func (a arch) PackageInstalled(ctx context.Context, run Runner, name string) (bool, error) {
if err := a.Confirm(ctx, run); err != nil {
return false, fmt.Errorf("nothing can be said about %q: %w", name, err)
}
if _, err := run(ctx, "pacman", "-Q", name); err != nil {
return false, nil
}
return true, nil
}
func (arch) InstallPackage(ctx context.Context, run Runner, name string) error {
out, err := run(ctx, "pacman", "-S", "--noconfirm", "--needed", name)
if err == nil {
return nil
}
// **The package manager's own words, and a name for the case that looks like a bug in the
// declaration and is not.** A stale index asks the mirrors for a version they have already
// superseded and gets a 404 from every one of them — so the package exists, the declaration is
// correct, and the machine's idea of what exists is old (novox/hq 04-ISSUES/002).
//
// **It is not fixed by syncing here.** `pacman -Sy <pkg>` installs a package built against
// libraries this machine does not have: a partial upgrade, which Arch does not support and
// which breaks the machine in a way that surfaces much later as something unrelated. The
// remedy is a full upgrade, and it is a decision about the whole machine rather than
// something to do silently in the middle of applying one resource.
//
// So this says which of the two it is looking at. A declaration that is wrong and a machine
// that is out of date fail identically otherwise, and they are fixed in completely different
// places.
if staleIndex(out) {
return fmt.Errorf(
"%s could not be fetched from any mirror, which is what a stale package index looks "+
"like: this machine is asking for a version the mirrors have replaced. The "+
"package and the declaration are probably both fine. It is fixed by upgrading "+
"the machine, not by this host syncing one package — that would be a partial "+
"upgrade, which this distribution does not support.\n\n%s",
name, strings.TrimSpace(out))
}
return fmt.Errorf("%w\n\n%s", err, strings.TrimSpace(out))
}
// staleIndex reports whether a failed install looks like the machine's view being old rather than
// the package being wrong.
//
// By what the package manager said, because there is nothing else to go on: the exit code is the
// same for both.
func staleIndex(out string) bool {
said := strings.ToLower(out)
if !strings.Contains(said, "failed retrieving file") && !strings.Contains(said, "404") {
return false
}
// Every mirror, not one. A single mirror failing is an ordinary transient thing and retrying
// is the answer; every one of them saying the file is gone is the index being old.
return strings.Contains(said, "error") || strings.Count(said, "404") > 1
}
// ServiceState reads what systemd says about a unit.
//
// Two traps, and both were hit before this read what it now reads.
//
// The exit code is not the answer: `is-active` exits non-zero for every state except active.
//
// And "inactive" does not mean stopped. `systemctl is-active` says "inactive" for a unit that
// DOES NOT EXIST exactly as it does for one installed and stopped, so declaring a unit stopped
// reported success for a unit the host cannot manage at all. LoadState is what separates them,
// so LoadState is what is read — and it is the thing an interface spanning systemd and OpenRC
// would have had to drop.
func (arch) ServiceState(ctx context.Context, run Runner, unit string) (string, error) {
out, _ := run(ctx, "systemctl", "show", unit,
"--property=LoadState", "--property=ActiveState", "--property=Type",
"--property=RemainAfterExit", "--property=ExecMainStatus")
var load, active, kind, remains, exited string
for _, line := range strings.Split(out, "\n") {
key, value, found := strings.Cut(strings.TrimSpace(line), "=")
if !found {
continue
}
switch key {
case "LoadState":
load = value
case "ActiveState":
active = value
case "Type":
kind = value
case "RemainAfterExit":
remains = value
case "ExecMainStatus":
exited = value
}
}
switch load {
case "":
return "", fmt.Errorf("the service manager said nothing about %s", unit)
case "not-found":
return "", fmt.Errorf(
"%s does not exist on this machine. A declaration naming a unit that is not "+
"installed cannot be satisfied, and reporting it stopped would be reporting "+
"absence as success", unit)
case "masked":
return "", fmt.Errorf("%s is masked, so its state cannot be declared", unit)
case "error", "bad-setting":
return "", fmt.Errorf("%s is installed but its unit file cannot be loaded (%s)", unit, load)
}
// **A one-shot that finished is not stopped.** A unit whose whole job is to apply something
// and exit — load a rule set, set a sysctl — is reported inactive the moment it succeeds, and
// unless it is told to linger there is no state in which it is ever "active". Reading that as
// "stopped" makes such a unit permanently unsatisfiable: the host starts it, it does its work,
// it exits, the host reads back "stopped" and reports failure — for ever, on every apply,
// while the thing it configured is in place and working.
//
// That is not hypothetical. It is what the firewall did on every machine it was ever assigned
// to: rules loaded, service reported failed, the mesh reported a machine not doing what it was
// told, and the only visible symptom was a red line about a unit nobody could see anything
// wrong with.
//
// So for that shape, what "running" means is "it ran, and it worked".
if kind == "oneshot" && remains != "yes" && active == "inactive" {
if exited == "0" || exited == "" {
return "running", nil
}
return "stopped", nil
}
switch active {
case "active", "activating", "reloading":
return "running", nil
case "inactive", "failed", "deactivating":
return "stopped", nil
default:
return "", fmt.Errorf(
"the service manager reports %s as %q, which is neither running nor stopped", unit, active)
}
}
func (arch) SetServiceState(ctx context.Context, run Runner, unit, state string) error {
verb := "start"
if state == "stopped" {
verb = "stop"
}
_, err := run(ctx, "systemctl", verb, unit)
return err
}
// ServiceBoot reads whether a unit starts at boot.
//
// `is-enabled` has more than two answers, and `static` is the one that matters: the unit has no
// install section and CANNOT be enabled. Reading it as "disabled" would have the host try, fail,
// and blame the wrong thing — the same shape as reading a missing unit as "stopped".
func (arch) ServiceBoot(ctx context.Context, run Runner, unit string) (string, error) {
out, _ := run(ctx, "systemctl", "is-enabled", unit)
switch state := strings.TrimSpace(out); state {
case "enabled", "enabled-runtime", "alias":
return "enabled", nil
case "disabled":
return "disabled", nil
case "":
return "", fmt.Errorf("the service manager said nothing about whether %s starts at boot", unit)
case "static":
return "", fmt.Errorf(
"%s is static — it has no install section, so it cannot be enabled or disabled. "+
"Something else pulls it in, and that is what a declaration should name", unit)
case "masked", "masked-runtime":
return "", fmt.Errorf("%s is masked, so its boot state cannot be declared", unit)
default:
return "", fmt.Errorf(
"the service manager reports %s as %q at boot, which is neither enabled nor disabled",
unit, state)
}
}
func (arch) SetServiceBoot(ctx context.Context, run Runner, unit, boot string) error {
verb := "enable"
if boot == "disabled" {
verb = "disable"
}
_, err := run(ctx, "systemctl", verb, unit)
return err
}
// CreateUser makes a login with useradd.
//
// `--create-home` because a user whose home does not exist is a user nothing can be delivered
// to, and delivering a shell's configuration is most of why the mesh knows about users at all.
func (arch) CreateUser(ctx context.Context, run Runner, name, home, shell string) error {
args := []string{"--create-home"}
if home != "" {
args = append(args, "--home-dir", home)
}
if shell != "" {
args = append(args, "--shell", shell)
}
if _, err := run(ctx, "useradd", append(args, name)...); err != nil {
return fmt.Errorf("cannot create the user %q: %w", name, err)
}
return nil
}
func (arch) SetUserShell(ctx context.Context, run Runner, name, shell string) error {
if _, err := run(ctx, "usermod", "--shell", shell, name); err != nil {
return fmt.Errorf("cannot set %q's shell to %q: %w", name, shell, err)
}
return nil
}
// AddUserToGroup appends, and `--append` is the whole point: without it usermod REPLACES the
// user's supplementary groups, so a declaration naming one group would silently remove every
// other — including the ones that make a login able to use a machine at all.
func (arch) AddUserToGroup(ctx context.Context, run Runner, name, group string) error {
if _, err := run(ctx, "usermod", "--append", "--groups", group, name); err != nil {
return fmt.Errorf("cannot put %q in the group %q: %w", name, group, err)
}
return nil
}