build / build (push) Successful in 1m43s
- include SMB drop-ins from /etc/samba/smb.conf.d into the generated smb.conf - keep smbd/nfs-server running while other services (/etc/exports.d, drop-ins) share, instead of disabling them when nasmon has no shares of its own - list those shares read-only on /shares - settings: RepoISO URL; poll its /api/integration/status, dashboard card, menu link, alerts for portal down, stale worker, failed jobs, unmounted publications Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
7.1 KiB
Go
219 lines
7.1 KiB
Go
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
|
|
// IncludeDir holds Samba drop-ins of other services, included into smb.conf;
|
|
// ExportsDir is scanned for other services' NFS exports (both: "" = none).
|
|
IncludeDir string
|
|
ExportsDir 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, IncludeDir: DefaultSMBIncludeDir,
|
|
ExportsDir: DefaultExportsDir, 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()
|
|
own := c
|
|
if !c.SMB.Enabled {
|
|
own.SMB.Shares = nil // other services may keep smbd running; don't serve disabled shares
|
|
}
|
|
external := countKind(m.External(), "smb")
|
|
if err := hostrun.WriteFile(ctx, m.Run, m.SMBConf, RenderSMB(own, m.Owner, IncludeFiles(m.IncludeDir)...)); 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 := len(own.SMB.Shares) > 0 || external > 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, %d from other services", len(own.SMB.Shares), external)
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) applyNFS(c Config) error {
|
|
ctx, cancel := m.ctx()
|
|
defer cancel()
|
|
own := c
|
|
if !c.NFS.Enabled {
|
|
own.NFS.Exports = nil
|
|
}
|
|
external := countKind(m.External(), "nfs")
|
|
if err := hostrun.WriteFile(ctx, m.Run, m.Exports, RenderExports(own, m.UID, m.GID)); err != nil {
|
|
return fmt.Errorf("write %s: %w", m.Exports, err)
|
|
}
|
|
active := len(own.NFS.Exports) > 0 || external > 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, %d from other services", len(own.NFS.Exports), external)
|
|
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
|
|
}
|