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 }