Files
a.gritsenkoandClaude Sonnet 5 09159a5f31 Initial import: nasmon health monitor, split out of home_nas
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>
2026-09-19 10:24:34 +03:00

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
}