mounts: take an SMB source's username as a setting and its password as its own secret, and wait for them
mesh/merge-gate fail: builds mounts → shanks; no bus step; a manifest the change touches fails the module check: modules/mounts/module.json: this manifest …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 24
mesh/delivery superseded: a newer head of the same pull request

The one INI secret smb-credentials could not be given through the desk
prompt, which takes one hidden line, and a missing login was said as a
fault: the first-node gate put the build back and the operator was told
nothing was theirs to do. The username is now the setting smb-users and
each password the one-line secret smb-password-<source>; a source missing
either answers waits through mesh-sdk's health answer (hq ADR 0283,
issue 386, issue 379).
This commit is contained in:
jochen
2026-10-10 21:11:16 +02:00
parent a14fa306f2
commit bbb3161ade
11 changed files with 328 additions and 346 deletions
+65 -21
View File
@@ -19,7 +19,9 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"os"
"os/user"
@@ -28,6 +30,8 @@ import (
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/health"
)
// LookEvery is how often the process looks.
@@ -48,6 +52,9 @@ const (
StateAbsent = "absent" // an occasional source that is not there: a state, not a fault
StateUnreachable = "unreachable" // should be there and is not
StateFailed = "failed" // tried and refused, or not set up; why says which
// StateWaiting is a source that cannot be set up until the operator gives its username or password
// (novox/hq ADR 0283): not a fault, said with what it waits for.
StateWaiting = "waiting"
)
// ExportsAnswer is what the bundle left from the sharing machine's `node-nfs-server.exports`.
@@ -74,6 +81,8 @@ type ShareStatus struct {
Why string `json:"why,omitempty"`
MountUnit string `json:"mount_unit,omitempty"`
AutoUnit string `json:"automount_unit,omitempty"`
// Waits is what a waiting source waits for: the setting smb-users, or its secret smb-password-<source>.
Waits []health.Wait `json:"waits,omitempty"`
}
// Status is what one look found.
@@ -272,23 +281,6 @@ func (a *Applier) Look(ctx context.Context) Status {
}
}
var creds map[string]SMBCredential
needCreds := false
for _, s := range cfg.Sources {
needCreds = needCreds || s.Kind == "smb"
}
if needCreds {
secret, err := a.M.read(cfg.Secret)
if cfg.Secret == "" || err != nil {
st.Problems = append(st.Problems, "the module's secret smb-credentials is not on this machine")
} else {
var p []string
creds, p = ParseSMBCredentials(secret)
for _, x := range p {
st.Problems = append(st.Problems, "smb-credentials: "+x)
}
}
}
uid, gid := -1, -1
if cfg.Account != "" && a.Lookup != nil {
if u, g, err := a.Lookup(cfg.Account); err == nil {
@@ -300,11 +292,22 @@ func (a *Applier) Look(ctx context.Context) Status {
case "disk":
pairs = append(pairs, DiskPair(s))
case "smb":
c, ok := creds[s.Name]
if !ok || cfg.State == "" {
c, waits, why := a.login(cfg, s.Name)
switch {
case why != "":
notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateFailed, Why: why}
continue
case len(waits) > 0:
var gives []string
for _, w := range waits {
gives = append(gives, w.What+" ("+w.Names()+")")
}
notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateWaiting,
Why: "it waits for the operator to give " + strings.Join(gives, " and "), Waits: waits}
continue
case cfg.State == "":
notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateFailed,
Why: fmt.Sprintf("its login is not given: at the desk, `secret ask <node> mounts smb-credentials` and type one line `[%s] username=… password=…`; "+
"or put a [%s] section in a file and `secret accept <node> mounts smb-credentials --from <file>`", s.Name, s.Name)}
Why: "the module's state directory is not named in its configuration, so no credentials file can be written"}
continue
}
file := filepath.Join(cfg.State, "smb-"+s.Name+".credentials")
@@ -701,3 +704,44 @@ func orInterval(d, otherwise time.Duration) time.Duration {
}
return otherwise
}
// login is an SMB source's login, or what it waits for, or why it cannot be read. The password is read as
// one line from the source's own secret; it is never said, and neither is anything the file holds.
func (a *Applier) login(cfg Config, source string) (SMBCredential, []health.Wait, string) {
var waits []health.Wait
part := "the source " + source
user, hasUser := cfg.SMBUsers[source]
if !hasUser {
waits = append(waits, health.Wait{Part: part, Setting: "smb-users", What: "the username of the source " + source})
}
var password string
raw, err := a.M.read(PasswordFile(cfg.Passwords, source))
switch {
case cfg.Passwords == "" || errors.Is(err, fs.ErrNotExist):
waits = append(waits, health.Wait{Part: part, Secret: PasswordSecret(source), What: "the password of the source " + source})
case err != nil:
return SMBCredential{}, nil, "its password " + PasswordSecret(source) + " could not be read: " + bare(err)
default:
pw, given, perr := ReadPassword(raw)
if perr != nil {
return SMBCredential{}, nil, "its password " + PasswordSecret(source) + " " + perr.Error() + ": give it again"
}
if !given {
waits = append(waits, health.Wait{Part: part, Secret: PasswordSecret(source), What: "the password of the source " + source})
}
password = pw
}
if len(waits) > 0 {
return SMBCredential{}, waits, ""
}
return SMBCredential{Username: user, Password: password}, nil, ""
}
// bare is an error without the path it names: the reason alone.
func bare(err error) string {
var pe *fs.PathError
if errors.As(err, &pe) {
return pe.Err.Error()
}
return err.Error()
}