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 ` 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") var load, active 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 } } 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) } 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 }