Go service (nasmon/) plus nfpm packaging (packaging/), carried over from the home_nas monorepo. Adds rpm packaging alongside the existing deb build (Fedora/RHEL/Rocky dependency names via nfpm overrides, per-format systemd unit path, dpkg+rpm-compatible maintainer scripts) and a Gitea Actions workflow that builds both formats on every push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
// 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
|
|
}
|