The file on PATH was the engine a person copied at installation, and every delivered engine ran from the versions directory: the operator typed hand-over and a build from before it answered "unexpected argument". A proved engine now copies itself over that file when the bytes differ (never a link). And the hand-over of a directory used as found (issue 339) gets its channel: the controller's terminal asks this node's engine on mesh.node.<node>.ask.hand-over; the engine judges every value — the ask's size and shape, who asked, the path as it states it, a directory and no link, through no link — and records it or refuses and records nothing. Every text the engine addresses to the operator names the nox line (ADR 0272); the engine's own subcommands are install-time and rescue, and its usage says so.
214 lines
8.5 KiB
Go
214 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A directory used as found is never quiet (novox/hq issue 339).
|
|
//
|
|
// The engine leaves the owner and mode of a directory it did not make: changing them would be root handing a
|
|
// directory it never made to whichever account a declaration named. That is safe and, said nowhere, a module
|
|
// whose data directory it cannot write would simply fail. So each such directory is said three ways: a WARNING
|
|
// line in every apply's log; a resource of its module of kind `directory`, stated unhealthy on every look,
|
|
// which the controller raises as that module's condition naming the directory and its owner; and a way out,
|
|
// which is the operator's line at the controller's terminal — `nox node hand-over <node> <path>` on the
|
|
// control-node (novox/hq ADR 0272, issue 356) — after which the next apply gives it the declared owner and
|
|
// mode like any directory of the mesh's. The controller carries that line to this engine over the bus
|
|
// (link.AskHandOverSubject); the engine's own `hand-over` subcommand is the same act for a machine with no
|
|
// mesh to reach.
|
|
|
|
// foundState is where the node's state is, for the looks to read the directories used as found from; empty
|
|
// until the engine runs.
|
|
var foundState atomic.Value
|
|
|
|
// foundNode is this node's name in the mesh, for the way out to name it; empty until the engine is enrolled.
|
|
var foundNode atomic.Value
|
|
|
|
// handedOverFrom has the apply read the directories handed over from beside the state at statePath.
|
|
func handedOverFrom(statePath string) {
|
|
apply.HandedOver = func() map[string]bool {
|
|
all, err := store.LoadHandedOver(statePath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-host: %v; no directory counts as handed over until it reads\n", err)
|
|
return nil
|
|
}
|
|
out := map[string]bool{}
|
|
for _, h := range all {
|
|
out[apply.HandOverKey(h.ID, h.Path)] = true
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
// foundDirectories is every directory the node's state records as used as found, sorted by id.
|
|
func foundDirectories(known store.State) []store.Applied {
|
|
var out []store.Applied
|
|
for _, r := range known.Resources {
|
|
if r.Type == "directory" && r.AsFound {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
sort.Slice(out, func(a, b int) bool { return out[a].ID < out[b].ID })
|
|
return out
|
|
}
|
|
|
|
// handOverLine is the operator's line that hands a directory to the mesh, with this node's name when it is
|
|
// known (novox/hq ADR 0272): typed on the control-node, from the operator's own account. The directory's
|
|
// path is the resource's target beside the reason, never in it: the controller says a reason on the
|
|
// operator's channel, which withholds a path (ADR 0234 §6), and keeps the target as evidence.
|
|
func handOverLine() string {
|
|
node, _ := foundNode.Load().(string)
|
|
if node == "" {
|
|
node = "<node>"
|
|
}
|
|
return fmt.Sprintf("`nox node hand-over %s <directory>` on the control-node", node)
|
|
}
|
|
|
|
// withFoundDirectories adds to a statement every directory used as found, as its module's resource of kind
|
|
// directory, unhealthy: the reason names its owner and mode as they are now, what was declared, and the line
|
|
// that hands it over. The path is the target, which the controller keeps as evidence.
|
|
func withFoundDirectories(h *link.Health, found []store.Applied) *link.Health {
|
|
for _, r := range found {
|
|
at := strings.LastIndex(r.ID, ".")
|
|
if at <= 0 {
|
|
continue
|
|
}
|
|
owner, mode := "unknown", "unknown"
|
|
if info, err := os.Lstat(r.Target); err == nil {
|
|
mode = fmt.Sprintf("%o", info.Mode().Perm())
|
|
if uid, gid, ok := apply.OwnerOf(info); ok {
|
|
owner = fmt.Sprintf("%d:%d", uid, gid)
|
|
}
|
|
}
|
|
declared := r.Declared
|
|
if declared == "" {
|
|
declared = "another owner or mode"
|
|
}
|
|
h.Resources = append(h.Resources, link.ResourceHealth{Module: r.ID[:at], Resource: r.ID,
|
|
Kind: link.KindDirectory, Target: r.Target, State: link.StateUnhealthy,
|
|
Reason: fmt.Sprintf("%s owned by %s, mode %s, as found; %s was declared and not given it — %s, with its "+
|
|
"path, hands it to the mesh", link.ReasonUsedAsFound, owner, mode, declared, handOverLine()),
|
|
Since: foundSince(r).UTC()})
|
|
}
|
|
return h
|
|
}
|
|
|
|
// foundNow is the directories used as found, read from the node's state; none when it cannot be read, which
|
|
// the apply says on its own.
|
|
func foundNow() []store.Applied {
|
|
path, _ := foundState.Load().(string)
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
known, err := store.Load(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return foundDirectories(known)
|
|
}
|
|
|
|
// handOverFound hands one directory used as found to the mesh, on the operator's word carried by the
|
|
// controller (link.HandOverFunc) or typed at this machine. It changes nothing on disk itself; the next apply
|
|
// gives the directory its declared owner and mode, and says so.
|
|
//
|
|
// **Every value is judged, and a refusal records nothing.** The path is the directory's absolute path exactly
|
|
// as the engine states it — clean, so `..` and a doubled separator never name a directory; one this machine's
|
|
// state records as used as found; on disk a directory and not a link, reached through no link. The engine
|
|
// never follows a path it did not state: a hand-over is root giving a directory to an account at the next
|
|
// apply, and a path that resolves elsewhere would give that elsewhere.
|
|
func handOverFound(statePath, path, by string) (string, error) {
|
|
if !filepath.IsAbs(path) {
|
|
return "", fmt.Errorf("hand-over takes the directory's absolute path, and %q is not one; nothing was handed over", path)
|
|
}
|
|
if filepath.Clean(path) != path {
|
|
return "", fmt.Errorf("%q is not the directory's path as the engine states it (no `..`, no doubled or trailing "+
|
|
"separator); nothing was handed over", path)
|
|
}
|
|
known, err := store.Load(statePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var found *store.Applied
|
|
for _, r := range foundDirectories(known) {
|
|
if filepath.Clean(r.Target) == path {
|
|
r := r
|
|
found = &r
|
|
break
|
|
}
|
|
}
|
|
if found == nil {
|
|
return "", errors.New(path + " is not a directory this machine uses as found (the module's condition names " +
|
|
"the ones that are); nothing was handed over")
|
|
}
|
|
info, err := os.Lstat(path)
|
|
switch {
|
|
case err != nil:
|
|
return "", fmt.Errorf("%s cannot be read on this machine: %v; nothing was handed over", path, err)
|
|
case info.Mode()&os.ModeSymlink != 0:
|
|
return "", fmt.Errorf("%s is a link, and the mesh hands over no link (novox/hq ADR 0012); nothing was handed over", path)
|
|
case !info.IsDir():
|
|
return "", fmt.Errorf("%s is not a directory on this machine; nothing was handed over", path)
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%s cannot be resolved on this machine: %v; nothing was handed over", path, err)
|
|
}
|
|
if resolved != path {
|
|
return "", fmt.Errorf("%s is reached through a link (it is %s), and the mesh hands over nothing through one "+
|
|
"(novox/hq issue 339); nothing was handed over", path, resolved)
|
|
}
|
|
if err := store.AddHandedOver(statePath, store.HandOver{ID: found.ID, Path: path, At: time.Now().UTC(), By: by}); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%s (%s) is handed to the mesh by %s: the next apply gives it %s, and it is the mesh's from "+
|
|
"then on — removed when empty and no longer declared", path, found.ID, by, orUnsaid(found.Declared)), nil
|
|
}
|
|
|
|
// handOverFrom is the engine's hand-over for the controller's ask: the state beside statePath, the path as asked.
|
|
func handOverFrom(statePath string) link.HandOverFunc {
|
|
return func(path, by string) (string, error) { return handOverFound(statePath, path, by) }
|
|
}
|
|
|
|
// handOverCommand is `nox-mesh-host hand-over <dir>` at this machine, as root: the same act, for a machine
|
|
// with no mesh to reach. The operator's line is `nox node hand-over <node> <dir>` on the control-node.
|
|
func handOverCommand(opts options) error {
|
|
by := "a person at the machine"
|
|
if u, err := user.Current(); err == nil {
|
|
by = u.Username + " at the machine"
|
|
}
|
|
said, err := handOverFound(opts.state, filepath.Clean(opts.file), by)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(opts.out, said)
|
|
return nil
|
|
}
|
|
|
|
// foundSince is when a directory was first found as it is: kept across applies, so the controller can tell one
|
|
// a send found from one found before it. A record from before that was kept says when it was last applied.
|
|
func foundSince(r store.Applied) time.Time {
|
|
if !r.FoundAt.IsZero() {
|
|
return r.FoundAt
|
|
}
|
|
return r.AppliedAt
|
|
}
|
|
|
|
func orUnsaid(s string) string {
|
|
if s == "" {
|
|
return "the declared owner and mode"
|
|
}
|
|
return s
|
|
}
|