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
+152
View File
@@ -0,0 +1,152 @@
package shares
import (
"errors"
"fmt"
"net"
"path"
"regexp"
"strings"
)
var (
reShareName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$`)
reWorkgroup = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$`)
reUser = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`)
reHost = regexp.MustCompile(`^[A-Za-z0-9*][A-Za-z0-9*.-]{0,253}$`)
reComment = regexp.MustCompile(`^[^\r\n\[\]]{0,120}$`)
)
// reserved Samba section names that must not be used as share names.
var reservedShares = map[string]bool{"global": true, "homes": true, "printers": true, "print$": true}
func ValidateShareName(s string) error {
if !reShareName.MatchString(s) {
return errors.New("share name: 1-80 characters, letters, digits, _ . - and must start with a letter or digit")
}
if reservedShares[strings.ToLower(s)] {
return fmt.Errorf("share name %q is reserved by Samba", s)
}
return nil
}
func ValidateWorkgroup(s string) error {
if !reWorkgroup.MatchString(s) {
return errors.New("workgroup: 1-15 characters, letters, digits, _ -")
}
return nil
}
func ValidateUserName(s string) error {
if !reUser.MatchString(s) {
return errors.New("user name: lowercase letters, digits, _ -, up to 32 characters")
}
if s == "root" || s == "nobody" {
return fmt.Errorf("user %s cannot be an SMB user", s)
}
return nil
}
func ValidateComment(s string) error {
if !reComment.MatchString(s) {
return errors.New("comment: up to 120 characters, no [ ] or line breaks")
}
return nil
}
// ValidatePath accepts an absolute, clean path without control characters.
// Whether it lies under a pool is checked by the caller (needs live state).
func ValidatePath(p string) error {
if p == "" {
return errors.New("path is required")
}
if !strings.HasPrefix(p, "/") || path.Clean(p) != p || strings.ContainsAny(p, "\"\\\r\n\t") {
return errors.New("path must be absolute and normalised (no .., trailing / or quotes)")
}
if p == "/" {
return errors.New("refusing to share the root filesystem")
}
return nil
}
// UnderRoots reports whether p equals or lies below one of the roots.
func UnderRoots(p string, roots []string) bool {
for _, r := range roots {
if r == "" || r == "/" {
continue
}
if p == r || strings.HasPrefix(p, r+"/") {
return true
}
}
return false
}
// ValidateClient accepts *, a host name (wildcards allowed), an IP or a CIDR.
func ValidateClient(c string) error {
if c == "*" {
return nil
}
if strings.Contains(c, "/") {
if _, _, err := net.ParseCIDR(c); err != nil {
return fmt.Errorf("client %q: bad network, use e.g. 192.168.1.0/24", c)
}
return nil
}
if net.ParseIP(c) != nil || reHost.MatchString(c) {
return nil
}
return fmt.Errorf("client %q: use a host name, IP address, network (CIDR) or *", c)
}
func (s SMBShare) Validate() error {
if err := ValidateShareName(s.Name); err != nil {
return err
}
if err := ValidatePath(s.Path); err != nil {
return err
}
if err := ValidateComment(s.Comment); err != nil {
return err
}
if !s.Guest && len(s.Users) == 0 {
return fmt.Errorf("share %s: allow guests or pick at least one user", s.Name)
}
for _, u := range s.Users {
if err := ValidateUserName(u); err != nil {
return err
}
}
return nil
}
func (e NFSExport) Validate() error {
if err := ValidatePath(e.Path); err != nil {
return err
}
if len(e.Clients) == 0 {
return fmt.Errorf("export %s: at least one client is required (or *)", e.Path)
}
for _, c := range e.Clients {
if err := ValidateClient(c); err != nil {
return err
}
}
switch e.Squash {
case "root", "all", "none":
default:
return errors.New("squash must be root, all or none")
}
return nil
}
// SplitList turns "a, b;c\nd" into ["a","b","c","d"].
func SplitList(s string) []string {
var out []string
for _, f := range strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == ';' || r == '\n' || r == ' ' || r == '\t' || r == '\r' }) {
if f = strings.TrimSpace(f); f != "" {
out = append(out, f)
}
}
return out
}