// Package hostrun executes privileged commands on the host. nasmon runs in a // sandboxed mount namespace (ProtectSystem=strict), so anything that must // mount, write /etc or talk to other daemons goes through a transient systemd // unit (systemd-run) and thereby runs in the host's namespace. package hostrun import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "strings" ) // Runner executes a command with the given stdin and returns its combined // output; err is non-nil on a non-zero exit or when it could not be started. type Runner func(ctx context.Context, stdin string, name string, args ...string) (out string, err error) // Direct runs the command in-process (development, tests, non-systemd hosts). func Direct(ctx context.Context, stdin string, name string, args ...string) (string, error) { return capture(exec.CommandContext(ctx, name, args...), stdin) } // Host runs the command as a transient systemd unit in the host's mount // namespace. --pipe forwards stdin/stdout/stderr, --wait the exit status. func Host(ctx context.Context, stdin string, name string, args ...string) (string, error) { full := append([]string{"--wait", "--pipe", "--collect", "--quiet", "--", name}, args...) return capture(exec.CommandContext(ctx, "systemd-run", full...), stdin) } func capture(cmd *exec.Cmd, stdin string) (string, error) { var buf bytes.Buffer cmd.Stdout, cmd.Stderr = &buf, &buf if stdin != "" { cmd.Stdin = strings.NewReader(stdin) } err := cmd.Run() out := strings.TrimSpace(buf.String()) if err != nil { var ee *exec.ExitError if errors.As(err, &ee) && out != "" { return out, errors.New(out) } return out, fmt.Errorf("%s: %w", cmd.Args[0], err) } return out, nil } // Auto picks Host when systemd manages the system and systemd-run exists, // Direct otherwise. func Auto() Runner { if _, err := os.Stat("/run/systemd/system"); err == nil { if _, err := exec.LookPath("systemd-run"); err == nil { return Host } } return Direct } // WriteFile atomically replaces path with content on the host (mode 0644, // parent directory created). A previous version is kept as path.bak so the // caller can Restore it if validation fails. func WriteFile(ctx context.Context, run Runner, path, content string) error { script := `set -e; umask 022; d=$(dirname "$1"); mkdir -p "$d"; [ -f "$1" ] && cp -f "$1" "$1.bak"; cat > "$1.tmp"; mv -f "$1.tmp" "$1"` _, err := run(ctx, content, "sh", "-c", script, "sh", path) return err } // Restore puts path.bak back in place of path (no-op when there is no backup). func Restore(ctx context.Context, run Runner, path string) error { _, err := run(ctx, "", "sh", "-c", `[ -f "$1.bak" ] && mv -f "$1.bak" "$1" || true`, "sh", path) return err }