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>
This commit is contained in:
2026-09-19 10:24:34 +03:00
co-authored by Claude Sonnet 5
commit 09159a5f31
61 changed files with 7882 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
package zfsadm
import (
"context"
"errors"
"strings"
"testing"
)
func TestValidators(t *testing.T) {
for _, s := range []string{"media", "media/video", "a.b_c:d-e", "x/y/z"} {
if err := ValidateRelName(s); err != nil {
t.Errorf("%q rejected: %v", s, err)
}
}
for _, s := range []string{"", "/media", "media/", "a//b", "..", "a/../b", ".", "-x", "a/-b", "a b", "a;b", "a/b\n", strings.Repeat("x", 201)} {
if ValidateRelName(s) == nil {
t.Errorf("%q accepted", s)
}
}
for _, s := range []string{"", "100G", "1.5T", "512M", "10k", "42"} {
if ValidateSize(s) != nil {
t.Errorf("size %q rejected", s)
}
}
for _, s := range []string{"10 G", "10GB", "G", "-1G", "1e3"} {
if ValidateSize(s) == nil {
t.Errorf("size %q accepted", s)
}
}
if ValidateCompression("gzip") == nil || ValidateCompression("lz4") != nil || ValidateCompression("") != nil {
t.Error("compression validation")
}
if ValidateOwner("ansible") == nil || ValidateOwner("1000:1000") != nil || ValidateOwner("") != nil {
t.Error("owner validation")
}
}
// fake records calls and answers by command prefix.
type fake struct {
calls []string
reply map[string]string
fail map[string]string
}
func (f *fake) run(_ context.Context, name string, args ...string) (string, error) {
c := name + " " + strings.Join(args, " ")
f.calls = append(f.calls, c)
for k, v := range f.fail {
if strings.HasPrefix(c, k) {
return v, errors.New(v)
}
}
for k, v := range f.reply {
if strings.HasPrefix(c, k) {
return v, nil
}
}
return "", nil
}
func TestCreate(t *testing.T) {
f := &fake{reply: map[string]string{"zfs get": "/tank/media"}}
a := New(f.run)
if err := a.Create("tank/media", CreateOpts{Compression: "lz4", Quota: "100G", Owner: "1000:1000"}); err != nil {
t.Fatal(err)
}
want := []string{
"zfs create -p -o compression=lz4 -o quota=100G tank/media",
"zfs get -H -o value mountpoint tank/media",
"chown 1000:1000 /tank/media",
}
if strings.Join(f.calls, "\n") != strings.Join(want, "\n") {
t.Errorf("calls:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n"))
}
// no owner, inherit everything
f = &fake{}
a = New(f.run)
if err := a.Create("tank/x", CreateOpts{}); err != nil {
t.Fatal(err)
}
if len(f.calls) != 1 || f.calls[0] != "zfs create -p tank/x" {
t.Errorf("calls = %v", f.calls)
}
// invalid input never reaches the runner
f = &fake{}
a = New(f.run)
if err := a.Create("tank/../x", CreateOpts{}); err == nil || len(f.calls) != 0 {
t.Errorf("bad name: err=%v calls=%v", err, f.calls)
}
if err := a.Create("tank/x", CreateOpts{Quota: "lots"}); err == nil || len(f.calls) != 0 {
t.Errorf("bad quota: err=%v calls=%v", err, f.calls)
}
// zfs error is surfaced verbatim
f = &fake{fail: map[string]string{"zfs create": "cannot create 'tank/x': out of space"}}
a = New(f.run)
if err := a.Create("tank/x", CreateOpts{}); err == nil || !strings.Contains(err.Error(), "out of space") {
t.Errorf("err = %v", err)
}
// unmounted dataset: created but chown skipped, reported
f = &fake{reply: map[string]string{"zfs get": "none"}}
a = New(f.run)
err := a.Create("tank/x", CreateOpts{Owner: "1000:1000"})
if err == nil || !strings.Contains(err.Error(), "chown skipped") {
t.Errorf("err = %v", err)
}
for _, c := range f.calls {
if strings.HasPrefix(c, "chown") {
t.Errorf("chown must not run: %v", f.calls)
}
}
}
func TestDestroyEmpty(t *testing.T) {
ok := map[string]string{"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": ""}
f := &fake{reply: ok}
a := New(f.run)
if err := a.DestroyEmpty("tank/x"); err != nil {
t.Fatal(err)
}
if f.calls[len(f.calls)-1] != "zfs destroy tank/x" {
t.Errorf("calls = %v", f.calls)
}
cases := map[string]map[string]string{
"children": {"zfs list -Hp -r -t filesystem": "tank/x\t98304\ntank/x/y\t98304", "zfs list -H -t snapshot": ""},
"not empty": {"zfs list -Hp -r -t filesystem": "tank/x\t5000000", "zfs list -H -t snapshot": ""},
"snapshots": {"zfs list -Hp -r -t filesystem": "tank/x\t98304", "zfs list -H -t snapshot": "tank/x@s1"},
}
for name, reply := range cases {
f := &fake{reply: reply}
a := New(f.run)
err := a.DestroyEmpty("tank/x")
if err == nil {
t.Errorf("%s: destroyed", name)
}
for _, c := range f.calls {
if strings.HasPrefix(c, "zfs destroy") {
t.Errorf("%s: destroy ran", name)
}
}
}
f = &fake{reply: ok}
a = New(f.run)
if err := a.DestroyEmpty("tank"); err == nil || len(f.calls) != 0 {
t.Errorf("pool root: err=%v calls=%v", err, f.calls)
}
}