A machine mounts what its assignment names: a node's share from the provision nfs-share, whose path it reads from nfs-server's state on the bus, and occasional sources (an SMB device, a disk) whose absence is a state, not a fault. apply writes and arms the unit pairs as root; watch tries each source and judges the health; the operator's fstab lines are taken over only through adopt, on their word, once the replacement is armed. SMB logins are an outside party's secret, given with secret accept.
119 lines
4.2 KiB
Go
119 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CommandTimeout bounds every command this module runs. No command it runs touches a mounted
|
|
// filesystem, so none should block on a server that is gone; the bound is there in case one does.
|
|
const CommandTimeout = 5 * time.Second
|
|
|
|
// Runner runs one command and answers its standard output. It is injected so that the tools are tested
|
|
// against recorded answers rather than this machine's service manager.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// ExecRunner runs a command on the machine, bounded by CommandTimeout.
|
|
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, CommandTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
said := strings.TrimSpace(stderr.String())
|
|
if said != "" {
|
|
return stdout.String(), fmt.Errorf("%s: %w: %s", name, err, said)
|
|
}
|
|
return stdout.String(), fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// Machine is what the module reads: a filesystem root (the real one, or a test's tree standing in for
|
|
// /etc, /proc and /var/lib), a way to run a command, and a way to look a command up.
|
|
type Machine struct {
|
|
Root string
|
|
Run Runner
|
|
LookPath func(name string) bool
|
|
// Escalate runs a command as root: as given where this process is root, else through `sudo -n`, as the
|
|
// bundle runs as the operator account (novox/hq ADR 0175 §4) and the sudo module lets it.
|
|
Escalate Runner
|
|
}
|
|
|
|
// Here is the machine this process runs on.
|
|
func Here() *Machine {
|
|
return &Machine{Root: "/", Run: ExecRunner, Escalate: EscalatedRunner,
|
|
LookPath: func(n string) bool { _, err := exec.LookPath(n); return err == nil }}
|
|
}
|
|
|
|
// read answers one file's text under the root. Only regular files the module names are read; a
|
|
// mountpoint is never opened, so a network filesystem whose server is gone cannot block a call.
|
|
func (m *Machine) read(path string) (string, error) {
|
|
b, err := os.ReadFile(filepath.Join(m.Root, path))
|
|
return string(b), err
|
|
}
|
|
|
|
// glob answers the files under the root matching a pattern, as paths seen from the machine's root.
|
|
func (m *Machine) glob(pattern string) []string {
|
|
found, _ := filepath.Glob(filepath.Join(m.Root, pattern))
|
|
out := make([]string, 0, len(found))
|
|
for _, f := range found {
|
|
rel, err := filepath.Rel(m.Root, f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, "/"+filepath.ToSlash(rel))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unreadable says why a file could not be read, in the module's words.
|
|
func unreadable(path string, err error) string {
|
|
switch {
|
|
case os.IsNotExist(err):
|
|
return path + ": not present"
|
|
case os.IsPermission(err):
|
|
return path + ": not readable by the tool runner's account"
|
|
default:
|
|
return fmt.Sprintf("%s: not read (%v)", path, err)
|
|
}
|
|
}
|
|
|
|
// EscalatedRunner runs a command as root, through sudo without a prompt where this process is not root.
|
|
// A refusal by sudo is said as one, never read as an empty answer.
|
|
func EscalatedRunner(ctx context.Context, name string, args ...string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 25*time.Second)
|
|
defer cancel()
|
|
program, argv := name, args
|
|
if os.Getuid() != 0 {
|
|
program, argv = "sudo", append([]string{"-n", name}, args...)
|
|
}
|
|
cmd := exec.CommandContext(ctx, program, argv...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
said := strings.TrimSpace(stderr.String())
|
|
if program == "sudo" && strings.HasPrefix(said, "sudo:") {
|
|
return stdout.String(), fmt.Errorf("%s needs root, and the tool runner's account may not run it without a prompt: %s", name, said)
|
|
}
|
|
if said != "" {
|
|
return stdout.String(), fmt.Errorf("%s: %w: %s", name, err, said)
|
|
}
|
|
return stdout.String(), fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// present says whether a device or a file is there. Never used on a mount point.
|
|
func (m *Machine) present(p string) bool {
|
|
_, err := os.Stat(filepath.Join(m.Root, p))
|
|
return err == nil
|
|
}
|