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:
@@ -0,0 +1,203 @@
|
||||
package shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nasmon/internal/hostrun"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSMBConf = "/etc/samba/smb.conf"
|
||||
DefaultExports = "/etc/exports.d/nasmon.exports"
|
||||
// DefaultOwnerUID is the account SMB sessions and squashed NFS clients act
|
||||
// as: the user that owns the data and runs the containers on this NAS.
|
||||
DefaultOwnerUID = "1000"
|
||||
)
|
||||
|
||||
// Manager writes the generated config files on the host and reloads the daemons.
|
||||
type Manager struct {
|
||||
Run hostrun.Runner
|
||||
LookPath func(string) (string, error)
|
||||
Owner string // Unix user name for force user
|
||||
UID, GID string
|
||||
SMBConf string
|
||||
Exports string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func New(run hostrun.Runner) *Manager {
|
||||
m := &Manager{Run: run, LookPath: exec.LookPath, UID: DefaultOwnerUID, GID: DefaultOwnerUID,
|
||||
SMBConf: DefaultSMBConf, Exports: DefaultExports, Timeout: 90 * time.Second}
|
||||
m.Owner = m.UID
|
||||
if u, err := user.LookupId(m.UID); err == nil {
|
||||
m.Owner, m.GID = u.Username, u.Gid
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) ctx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), m.Timeout)
|
||||
}
|
||||
|
||||
func (m *Manager) installed(bin string) bool {
|
||||
_, err := m.LookPath(bin)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m *Manager) SMBInstalled() bool { return m.installed("smbd") }
|
||||
func (m *Manager) NFSInstalled() bool { return m.installed("exportfs") }
|
||||
|
||||
// Apply regenerates both config files and reloads/enables/disables the
|
||||
// services. SMB and NFS are applied independently; errors are joined.
|
||||
func (m *Manager) Apply(c Config) error {
|
||||
var errs []error
|
||||
if m.SMBInstalled() {
|
||||
if err := m.applySMB(c); err != nil {
|
||||
errs = append(errs, fmt.Errorf("SMB: %w", err))
|
||||
}
|
||||
}
|
||||
if m.NFSInstalled() {
|
||||
if err := m.applyNFS(c); err != nil {
|
||||
errs = append(errs, fmt.Errorf("NFS: %w", err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (m *Manager) applySMB(c Config) error {
|
||||
ctx, cancel := m.ctx()
|
||||
defer cancel()
|
||||
if err := hostrun.WriteFile(ctx, m.Run, m.SMBConf, RenderSMB(c, m.Owner)); err != nil {
|
||||
return fmt.Errorf("write %s: %w", m.SMBConf, err)
|
||||
}
|
||||
if out, err := m.Run(ctx, "", "testparm", "-s", "--suppress-prompt", m.SMBConf); err != nil {
|
||||
_ = hostrun.Restore(ctx, m.Run, m.SMBConf)
|
||||
return fmt.Errorf("testparm rejected the config, previous file restored: %s", firstLines(out, 4))
|
||||
}
|
||||
active := c.SMB.Enabled && len(c.SMB.Shares) > 0
|
||||
if !active {
|
||||
_, err := m.Run(ctx, "", "systemctl", "disable", "--now", "smbd", "nmbd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop smbd: %w", err)
|
||||
}
|
||||
log.Printf("shares: SMB disabled (%d shares, enabled=%v)", len(c.SMB.Shares), c.SMB.Enabled)
|
||||
return nil
|
||||
}
|
||||
if _, err := m.Run(ctx, "", "systemctl", "enable", "--now", "smbd", "nmbd"); err != nil {
|
||||
return fmt.Errorf("start smbd: %w", err)
|
||||
}
|
||||
if out, err := m.Run(ctx, "", "smbcontrol", "all", "reload-config"); err != nil {
|
||||
return fmt.Errorf("reload smbd: %s", firstLines(out, 2))
|
||||
}
|
||||
log.Printf("shares: SMB config applied, %d shares", len(c.SMB.Shares))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) applyNFS(c Config) error {
|
||||
ctx, cancel := m.ctx()
|
||||
defer cancel()
|
||||
if err := hostrun.WriteFile(ctx, m.Run, m.Exports, RenderExports(c, m.UID, m.GID)); err != nil {
|
||||
return fmt.Errorf("write %s: %w", m.Exports, err)
|
||||
}
|
||||
active := c.NFS.Enabled && len(c.NFS.Exports) > 0
|
||||
if !active {
|
||||
if _, err := m.Run(ctx, "", "systemctl", "disable", "--now", "nfs-server"); err != nil {
|
||||
return fmt.Errorf("stop nfs-server: %w", err)
|
||||
}
|
||||
log.Printf("shares: NFS disabled (%d exports, enabled=%v)", len(c.NFS.Exports), c.NFS.Enabled)
|
||||
return nil
|
||||
}
|
||||
if _, err := m.Run(ctx, "", "systemctl", "enable", "--now", "nfs-server"); err != nil {
|
||||
return fmt.Errorf("start nfs-server: %w", err)
|
||||
}
|
||||
if out, err := m.Run(ctx, "", "exportfs", "-ra"); err != nil {
|
||||
_ = hostrun.Restore(ctx, m.Run, m.Exports)
|
||||
_, _ = m.Run(ctx, "", "exportfs", "-ra")
|
||||
return fmt.Errorf("exportfs rejected the exports, previous file restored: %s", firstLines(out, 4))
|
||||
}
|
||||
log.Printf("shares: NFS exports applied, %d exports", len(c.NFS.Exports))
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstLines(s string, n int) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
if len(lines) > n {
|
||||
lines = lines[:n]
|
||||
}
|
||||
return strings.Join(lines, " | ")
|
||||
}
|
||||
|
||||
// ---- SMB users
|
||||
|
||||
// ListUsers returns the Samba accounts (pdbedit -L: "name:uid:full name").
|
||||
func (m *Manager) ListUsers() ([]string, error) {
|
||||
if !m.SMBInstalled() {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, cancel := m.ctx()
|
||||
defer cancel()
|
||||
out, err := m.Run(ctx, "", "pdbedit", "-L")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var users []string
|
||||
for _, l := range strings.Split(out, "\n") {
|
||||
name, _, ok := strings.Cut(strings.TrimSpace(l), ":")
|
||||
if ok && name != "" {
|
||||
users = append(users, name)
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// SetPassword creates the Samba account (and a login-less Unix account if
|
||||
// needed) or changes the password of an existing one.
|
||||
func (m *Manager) SetPassword(name, pass string) error {
|
||||
if err := ValidateUserName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pass) < 4 || strings.ContainsAny(pass, "\n\r") {
|
||||
return errors.New("password must be at least 4 characters")
|
||||
}
|
||||
ctx, cancel := m.ctx()
|
||||
defer cancel()
|
||||
if _, err := m.Run(ctx, "", "getent", "passwd", name); err != nil {
|
||||
// Unix account is required by Samba; no shell, no home, data owner's group
|
||||
if out, err := m.Run(ctx, "", "useradd", "-M", "-N", "-g", m.GID, "-s", "/usr/sbin/nologin", "-c", "nasmon SMB user", name); err != nil {
|
||||
return fmt.Errorf("create system user: %s", firstLines(out, 2))
|
||||
}
|
||||
log.Printf("shares: system user %s created for SMB", name)
|
||||
}
|
||||
if out, err := m.Run(ctx, pass+"\n"+pass+"\n", "smbpasswd", "-s", "-a", name); err != nil {
|
||||
return fmt.Errorf("smbpasswd: %s", firstLines(out, 2))
|
||||
}
|
||||
log.Printf("shares: SMB password set for %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser removes the Samba account; the Unix account is removed too when
|
||||
// it is one nasmon created (nologin shell).
|
||||
func (m *Manager) DeleteUser(name string) error {
|
||||
if err := ValidateUserName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := m.ctx()
|
||||
defer cancel()
|
||||
if out, err := m.Run(ctx, "", "smbpasswd", "-x", name); err != nil {
|
||||
return fmt.Errorf("smbpasswd -x: %s", firstLines(out, 2))
|
||||
}
|
||||
if ent, err := m.Run(ctx, "", "getent", "passwd", name); err == nil && strings.HasSuffix(strings.TrimSpace(ent), "/usr/sbin/nologin") && name != m.Owner {
|
||||
if out, err := m.Run(ctx, "", "userdel", name); err != nil {
|
||||
return fmt.Errorf("SMB account removed, but userdel failed: %s", firstLines(out, 2))
|
||||
}
|
||||
}
|
||||
log.Printf("shares: SMB user %s deleted", name)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user