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.
167 lines
6.2 KiB
Go
167 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/upgrade"
|
|
"github.com/novox/mesh-host/internal/witness"
|
|
)
|
|
|
|
// The node-engine's part in core upgrades that roll back (novox/hq to-be 45 §8, ADR 0227 rule 8):
|
|
// proving its own build to the launcher that witnesses it, witnessing the controller and the node
|
|
// tools it places, and saying every verdict on its reports.
|
|
|
|
// standingRollbacks is every verdict that still stands on this machine: the launcher's about this
|
|
// host, and the engine's about the processes it witnesses. Said on every report.
|
|
func standingRollbacks(statePath string) []link.Rollback {
|
|
var out []link.Rollback
|
|
verdicts, err := upgrade.ReadRolledBack(upgrade.RolledBackPath(statePath))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-host: %v\n", err)
|
|
}
|
|
for _, v := range verdicts {
|
|
out = append(out, link.Rollback{Component: link.ComponentEngine, From: v.From, To: v.To,
|
|
Outcome: v.Outcome, Why: v.Why, At: v.At})
|
|
}
|
|
return append(out, witness.Standing(apply.DaemonRoot())...)
|
|
}
|
|
|
|
// rolledBackHosts are the host versions the launcher will not start again, so this host never stands
|
|
// aside for one.
|
|
func rolledBackHosts(statePath string) []string {
|
|
verdicts, _ := upgrade.ReadRolledBack(upgrade.RolledBackPath(statePath))
|
|
return upgrade.RolledBackVersions(verdicts)
|
|
}
|
|
|
|
// proving is this host waiting for the mesh to take a report it made under its own build — the
|
|
// evidence its launcher's trial waits for — and acting on it once.
|
|
type proving struct {
|
|
statePath string
|
|
version string
|
|
say func(string)
|
|
once sync.Once
|
|
}
|
|
|
|
// unproved says whether this host's version is not yet known-good here: a host on trial.
|
|
func unproved(statePath, version string) bool {
|
|
known, err := upgrade.ReadKnownGood(upgrade.KnownGoodPath(statePath))
|
|
return err != nil || known != version
|
|
}
|
|
|
|
// heard is told every account of the machine the mesh has taken. The first one about a declaration,
|
|
// made by this build, proves it.
|
|
func (p *proving) heard(r link.Report) {
|
|
if r.Declared == "" || r.Refused != "" || r.Host != p.version {
|
|
return
|
|
}
|
|
p.once.Do(func() {
|
|
retired, err := upgrade.Proved(p.statePath, upgrade.VersionsDir(""), p.version)
|
|
if err != nil {
|
|
p.say(fmt.Sprintf("this host reported under its own build %s, and proving it was not complete: %v", p.version, err))
|
|
}
|
|
if len(retired) > 0 {
|
|
p.say(fmt.Sprintf("host %s is proved; retired the host version(s) %s", p.version, strings.Join(retired, ", ")))
|
|
}
|
|
renewFallback(p.version, p.say)
|
|
renewLauncher(p.say)
|
|
})
|
|
}
|
|
|
|
// renewFallback makes the engine placed by hand — the launcher's fallback, and the `nox-mesh-host` on this
|
|
// machine's PATH — a copy of this proved build when it is not one (novox/hq issue 356). On the same
|
|
// evidence known-good is written on, and said when it happens or cannot: a stale command that answers
|
|
// for a build that is not running would otherwise stay until somebody typed it.
|
|
func renewFallback(version string, say func(string)) {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
say("the engine placed by hand could not be renewed: this build does not know its own path: " + err.Error())
|
|
return
|
|
}
|
|
fallback := upgrade.FallbackPath()
|
|
renewed, err := upgrade.RenewFallback(self, fallback)
|
|
switch {
|
|
case err != nil:
|
|
say(fmt.Sprintf("the engine placed by hand at %s is not this build, and could not be renewed: %v", fallback, err))
|
|
case renewed:
|
|
say(fmt.Sprintf("renewed the engine placed by hand at %s to this build, %s: the command on this machine is the "+
|
|
"engine that runs it", fallback, version))
|
|
}
|
|
}
|
|
|
|
// The launcher, as the service manager runs it.
|
|
const (
|
|
launcherPath = upgrade.DefaultLibexec + "/launch"
|
|
hostUnit = "nox-mesh-host.service"
|
|
)
|
|
|
|
// renewLauncher asks the service manager to restart this host's service when the launcher running
|
|
// it was replaced on disk since it started (novox/hq to-be 45 §8): a delivered launcher otherwise
|
|
// takes effect only at the next boot, and the witness it carries with it. A launcher from
|
|
// this one on runs its successor itself, at the host's next clean exit; this is for the launcher
|
|
// before it. Said, and only when it is certain: the parent is the launcher, and the file it reads
|
|
// is gone from under it.
|
|
func renewLauncher(say func(string)) {
|
|
if _, err := os.Stat("/run/systemd/system"); err != nil {
|
|
return
|
|
}
|
|
if !launcherReplaced(os.Getppid(), "/proc", launcherPath) {
|
|
return
|
|
}
|
|
say("the launcher running this host was replaced on disk; asking the service manager to run the new one")
|
|
if _, err := apply.ExecRunner(context.Background(), "systemctl", "restart", "--no-block", hostUnit); err != nil {
|
|
say("could not ask for the new launcher, so it runs from the next boot: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// launcherReplaced is whether process pid is a shell reading the launcher at path, from a file that
|
|
// is no longer there — the one a delivery renamed a new launcher over.
|
|
func launcherReplaced(pid int, proc, path string) bool {
|
|
if pid <= 1 {
|
|
return false
|
|
}
|
|
base := filepath.Join(proc, strconv.Itoa(pid))
|
|
cmdline, err := os.ReadFile(filepath.Join(base, "cmdline"))
|
|
if err != nil || !strings.Contains(string(cmdline), path) {
|
|
return false
|
|
}
|
|
fds, err := os.ReadDir(filepath.Join(base, "fd"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, fd := range fds {
|
|
target, err := os.Readlink(filepath.Join(base, "fd", fd.Name()))
|
|
if err == nil && target == path+" (deleted)" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// watchWhatThisHostPlaced judges the core builds this host placed, until ctx ends (to-be 45 §8).
|
|
func watchWhatThisHostPlaced(ctx context.Context, node string, queue *link.Queue, say func(string)) {
|
|
host, _ := os.Hostname()
|
|
w := &witness.Watcher{
|
|
Root: apply.DaemonRoot(), Node: node, Host: host,
|
|
Asker: func() link.Asker { return queue.Asker() },
|
|
Run: witness.Runner(apply.ExecRunner),
|
|
Hold: func(f func()) {
|
|
applying.Lock()
|
|
defer applying.Unlock()
|
|
f()
|
|
},
|
|
// Said now, not at the next report: the verdict is on every report from here, and the mesh
|
|
// is asked for one at once.
|
|
Concluded: func(link.Rollback) { queue.ReportAsked() },
|
|
Say: say,
|
|
}
|
|
w.Watch(ctx)
|
|
}
|