mounts: take an SMB source's username from its entry of sources, not a setting of its own

The live module check refused smb-users: a key naming a user is the
operator's value and may have no default, and a setting without one would
refuse to compose mounts on every node until set. The username now goes in
the source's URL (smb://<user>@<host>/<share>), percent-decoded; a
password there is refused unsaid, and a source without a user waits for
sources (hq ADR 0283, issue 386).
This commit is contained in:
jochen
2026-10-10 21:30:31 +02:00
parent c620173966
commit 6f78899827
8 changed files with 117 additions and 89 deletions
+11 -11
View File
@@ -12,7 +12,7 @@ provision `nfs-share`, which `nfs-server` provides on the machine that shares.
- The packages `nfs-utils` and `cifs-utils`.
- `/etc/mesh-mounts/mounts.conf`, placed by the mesh from the assignment: the settings `shares`,
`sources` and `smb-users`, the binding of `nfs-share` (`from`, the sharing machine, and `at`, where it is
and `sources`, the binding of `nfs-share` (`from`, the sharing machine, and `at`, where it is
reached on the private network), the operator account, and where the module's secrets are placed.
- One `.mount` and one `.automount` unit per share or source, in `/etc/systemd/system`, named as
`systemd-escape --path` names the mount point and opened by the marker
@@ -59,23 +59,23 @@ read-write share is mounted hard, so a write is never dropped while the server i
## Settings
All three are preferences whose default, `none`, names nothing (ADR 0262 allows no empty default).
Both are preferences whose default, `none`, names nothing (ADR 0262 allows no empty default).
- `shares`: space-separated `name=mountpoint`, each optionally `:ro`. The name is the share's on the
sharing machine; its path there comes from that machine over the bus, never from this setting.
For example `library=/srv/library downloads=/srv/downloads:ro`.
- `sources`: space-separated occasional sources, each optionally `:ro`:
`name=smb://<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
For example `games=smb://192.0.2.76/Share@/mnt/games`.
- `smb-users`: the username each SMB source logs in with, space-separated `name=username`, for example
`games=root`. A username is not a secret, so it is a setting (novox/hq ADR 0283). It holds no space,
comma or `=`. A device that needs a domain takes it in the username, as `DOMAIN\user` or `user@domain`:
the credentials file has no `domain=` line.
`name=smb://[<user>@]<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
For example `games=smb://root@192.0.2.76/Share@/mnt/games`. An SMB source's username is in its entry: it
is not a secret (novox/hq ADR 0283). It is percent-decoded, so a device that needs a domain takes it as
`WORKGROUP%5Cuser` (for `WORKGROUP\user`), and an `@` in it is written `%40`; it holds no space, comma or
`=`. A password never goes in the entry: `user:password@` is refused, saying so and never what was
written. An SMB entry that cannot be read is said without what it holds.
A mount point is an absolute, plainly written path, not a directory of the system's own (`/etc`, `/usr`,
`/run`, …). An entry that cannot be read is said, and only that entry is left out.
All three are trusted settings: an agent proposes them (`settings propose mounts … --node <node>`) and the
Both are trusted settings: an agent proposes them (`settings propose mounts … --node <node>`) and the
operator approves the change on their phone; it is set on that approval and pushed after (ADR 0277).
## The secrets
@@ -154,8 +154,8 @@ password of more than one line), or when `watch` has not looked for two minutes.
is absent is healthy, and said as not present since when.
A source waiting for its username or password is not a fault. With nothing else found, the answer is
*waits*, with one wait per thing not given: the setting `smb-users` ("the username of the source games")
or the secret `smb-password-games` ("the password of the source games"). The node-engine states the
*waits*, with one wait per thing not given: the setting `sources` ("the username of the source games, in
its entry of sources") or the secret `smb-password-games` ("the password of the source games"). The node-engine states the
resource `waiting`, the controller says it to the operator as needing them, naming the act, and the
first-node gate does not fail a build for it. Beside a fault, the waits are said and the answer is
unhealthy.
+8 -6
View File
@@ -81,7 +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 is what a waiting source waits for: its username in the setting sources, or its secret
// smb-password-<source>.
Waits []health.Wait `json:"waits,omitempty"`
}
@@ -292,7 +293,7 @@ func (a *Applier) Look(ctx context.Context) Status {
case "disk":
pairs = append(pairs, DiskPair(s))
case "smb":
c, waits, why := a.login(cfg, s.Name)
c, waits, why := a.login(cfg, s)
switch {
case why != "":
notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateFailed, Why: why}
@@ -707,12 +708,13 @@ func orInterval(d, otherwise time.Duration) time.Duration {
// 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) {
func (a *Applier) login(cfg Config, s Source) (SMBCredential, []health.Wait, string) {
var waits []health.Wait
source, user := s.Name, s.User
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})
if user == "" {
waits = append(waits, health.Wait{Part: part, Setting: "sources",
What: "the username of the source " + source + ", in its entry of sources"})
}
// Never a relative path: without the directory the mesh places the passwords in, nothing is read.
if !filepath.IsAbs(cfg.Passwords) {
+47 -40
View File
@@ -6,11 +6,12 @@ package main
// The mesh renders ConfigFile from the assignment: the settings `shares` and `sources`, and the binding
// of the provision `nfs-share` (`from`, the machine that shares, and `at`, where it is reached). The
// long-running process reads it as root and writes the mount units; the bundle reads it as the operator
// account to read what the sharing machine exports from the bus. Nothing in it is secret: an SMB source's username is the
// setting smb-users, and its password the module's own secret smb-password-<source>, in a file only root reads.
// account to read what the sharing machine exports from the bus. Nothing in it is secret: an SMB source's username is in its
// entry of sources, and its password the module's own secret smb-password-<source>, in a file only root reads.
import (
"fmt"
"net/url"
"path/filepath"
"regexp"
"strings"
@@ -41,13 +42,12 @@ type Config struct {
// reached on the private network (the binding's `at`).
Server string `json:"server,omitempty"`
At string `json:"at,omitempty"`
// SMBUsers is the setting `smb-users`: each SMB source's username. Passwords is the directory where
// the mesh places each source's password, `smb-password-<source>.secret` (ADR 0283); State is the
// module's state directory, where the per-source credentials files are written.
SMBUsers map[string]string `json:"smb_users,omitempty"`
Passwords string `json:"passwords,omitempty"`
State string `json:"state,omitempty"`
Account string `json:"account,omitempty"`
// Passwords is the directory where the mesh places each SMB source's password,
// `smb-password-<source>.secret` (ADR 0283); State is the module's state directory, where the
// per-source credentials files are written.
Passwords string `json:"passwords,omitempty"`
State string `json:"state,omitempty"`
Account string `json:"account,omitempty"`
// Node is this machine's name: a share this machine serves itself is never mounted onto it.
Node string `json:"node,omitempty"`
// Problems are what in the file could not be read; each is said, and its share is left out.
@@ -67,10 +67,13 @@ type Wanted struct {
// Source is one occasional source: a device that is not a node and is not always there.
type Source struct {
Name string `json:"name"`
Kind string `json:"kind"` // smb or disk
Host string `json:"host,omitempty"`
Share string `json:"share,omitempty"`
Name string `json:"name"`
Kind string `json:"kind"` // smb or disk
Host string `json:"host,omitempty"`
Share string `json:"share,omitempty"`
// User is an SMB source's username, from its entry (`smb://<user>@<host>/<share>`), percent-decoded;
// empty when the entry names none, and the source then waits for it (ADR 0283). Not a secret.
User string `json:"user,omitempty"`
UUID string `json:"uuid,omitempty"`
Mountpoint string `json:"mountpoint"`
ReadOnly bool `json:"read_only"`
@@ -162,7 +165,11 @@ func ParseSources(v string) ([]Source, []string) {
for _, e := range entries(v) {
name, rest, ok := strings.Cut(e, "=")
if !ok {
problems = append(problems, fmt.Sprintf("the source %q is not name=<where>@<mountpoint>", e))
if strings.Contains(e, "://") {
problems = append(problems, "an entry of sources is not name=<where>@<mountpoint>")
} else {
problems = append(problems, fmt.Sprintf("the source %q is not name=<where>@<mountpoint>", e))
}
continue
}
if !nameRe.MatchString(name) {
@@ -183,12 +190,13 @@ func ParseSources(v string) ([]Source, []string) {
s := Source{Name: name, Mountpoint: point, ReadOnly: ro}
switch {
case strings.HasPrefix(where, "smb://"):
host, share, ok := strings.Cut(strings.TrimPrefix(where, "smb://"), "/")
if !ok || !hostRe.MatchString(host) || !shareRe.MatchString(share) {
problems = append(problems, fmt.Sprintf("the source %s: %q is not smb://<host>/<share>", name, where))
// Nothing of an SMB entry is said back: it may carry a password where the user goes.
user, host, share, problem := parseSMB(strings.TrimPrefix(where, "smb://"))
if problem != "" {
problems = append(problems, fmt.Sprintf("the source %s: %s", name, problem))
continue
}
s.Kind, s.Host, s.Share = "smb", host, share
s.Kind, s.User, s.Host, s.Share = "smb", user, host, share
case strings.HasPrefix(where, "disk:"):
uuid := strings.TrimPrefix(where, "disk:")
if !uuidRe.MatchString(uuid) {
@@ -231,8 +239,6 @@ func ParseConfig(text string) Config {
c.Sources, p = ParseSources(kv["sources"])
c.Problems = append(c.Problems, p...)
c.Unreadable = append(c.Unreadable, p...)
c.SMBUsers, p = ParseSMBUsers(kv["smb-users"])
c.Problems = append(c.Problems, p...)
c.Server, c.At, c.Passwords, c.State, c.Account = kv["server"], kv["at"], kv["passwords"], kv["state"], kv["account"]
if c.Passwords == "" {
c.Passwords = c.State
@@ -270,7 +276,7 @@ func (c Config) ServerNode() string {
return first
}
// SMBCredential is one source's login: the username from the setting `smb-users`, the password from the
// SMBCredential is one source's login: the username from its entry of `sources`, the password from the
// source's own secret `smb-password-<source>` (novox/hq ADR 0283).
type SMBCredential struct {
Username string
@@ -290,27 +296,28 @@ func PasswordFile(dir, source string) string {
// would end the line early.
var userRe = regexp.MustCompile(`^[^\s=,]{1,64}$`)
// ParseSMBUsers reads the setting `smb-users`: space-separated `name=username`, one per SMB source. Not a
// secret, so it is a setting (ADR 0283). What cannot be read is said, and only that entry is left out.
func ParseSMBUsers(v string) (map[string]string, []string) {
out := map[string]string{}
var problems []string
for _, e := range entries(v) {
name, user, ok := strings.Cut(e, "=")
switch {
case !ok:
problems = append(problems, fmt.Sprintf("the smb-users entry %q is not name=username", e))
case !nameRe.MatchString(name):
problems = append(problems, fmt.Sprintf("the smb-users name %q is not lower-case letters, digits and dashes", name))
case !userRe.MatchString(user):
problems = append(problems, fmt.Sprintf("the smb-users entry for %s is not a username without spaces, commas or =", name))
case out[name] != "":
problems = append(problems, fmt.Sprintf("the source %s is named twice in smb-users; the first is kept", name))
default:
out[name] = user
// parseSMB reads what follows smb:// in a source's entry: `[<user>@]<host>/<share>`. The user is
// percent-decoded, so `DOMAIN%5Cuser` or a user holding `@` can be written; a password in its place
// (`user:pass@`) is refused, and no problem says anything of what was written.
func parseSMB(body string) (user, host, share, problem string) {
const form = "is not smb://[<user>@]<host>/<share>"
if at := strings.LastIndex(body, "@"); at >= 0 {
raw := body[:at]
body = body[at+1:]
if strings.Contains(raw, ":") {
return "", "", "", "its entry carries a password in the URL; a password is given as the secret smb-password-<source>, never in a setting"
}
decoded, err := url.PathUnescape(raw)
if err != nil || !userRe.MatchString(decoded) {
return "", "", "", "its user is not a username without spaces, commas or = (percent-encode a \\ or an @ in it)"
}
user = decoded
}
return out, problems
host, share, ok := strings.Cut(body, "/")
if !ok || !hostRe.MatchString(host) || !shareRe.MatchString(share) {
return "", "", "", form
}
return user, host, share, ""
}
// ReadPassword reads a source's password secret as one line, as typed at the desk's hidden prompt (ADR
+2 -2
View File
@@ -41,9 +41,9 @@ func TestTheFailureOfTheUnitTheModuleTakesOverIsCleared(t *testing.T) {
func TestAMountThatFailedBecauseItsSourceWasAwayIsCleared(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nsources=games=smb://192.0.2.76/Share@/mnt/games\n"+
f.write(ConfigFile, "shares=library=/srv/library\nsources=games=smb://root@192.0.2.76/Share@/mnt/games\n"+
"server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n"+
"smb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
"passwords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write(ExportsFile, exportsAnswer(t, library))
f.write("/var/lib/mesh/mounts/smb-password-games.secret", testPassword+"\n")
a := f.applier(time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC))
+5 -2
View File
@@ -91,7 +91,10 @@ func TestTheManifestNamesWhatTheBundleServesAndWhatItNeeds(t *testing.T) {
if got := PasswordFile("${dir:state}", "games"); got != strings.Replace(sec["path"], "*", "games", 1) {
t.Errorf("the process reads %s, the mesh places %s", got, sec["path"])
}
for _, key := range []string{"shares", "sources", "smb-users"} {
if _, has := m.Settings["smb-users"]; has || len(m.Settings) != 2 {
t.Errorf("an SMB source's username is in its entry of sources, not a setting of its own: %v", m.Settings)
}
for _, key := range []string{"shares", "sources"} {
s := m.Settings[key]
if s["kind"] != "preference" || s["default"] != None || s["why"] == "" || !strings.Contains(raw, "${setting:"+key+"}") {
t.Errorf("setting %s: %v", key, s)
@@ -139,7 +142,7 @@ func TestTheProcessesAreRootAndJudgedByTheirUnitAndTheHealthTool(t *testing.T) {
if len(cfg.Problems) != 0 && !strings.Contains(strings.Join(cfg.Problems, " "), "${") {
t.Errorf("the config's template reads: %v", cfg.Problems)
}
for _, want := range []string{"server=${bound:nfs-share:from}", "at=${bound:nfs-share:at}", "passwords=${dir:state}", "smb-users=${setting:smb-users}"} {
for _, want := range []string{"server=${bound:nfs-share:from}", "at=${bound:nfs-share:at}", "passwords=${dir:state}"} {
if !strings.Contains(content, want) {
t.Errorf("the config lacks %s", want)
}
+1 -1
View File
@@ -146,7 +146,7 @@ func Verbs(m *Machine) []stdio.Tool {
"unhealthy when a share that should always be there has been unreachable or failed for five minutes, an automount " +
"is not armed, something the settings name cannot be set up (a unit file that is not the module's), or the " +
"module's process has not looked for two minutes; with why, one sentence per finding. An occasional source that " +
"is absent is healthy, and said as not present since when. A source whose username (the setting smb-users) or " +
"is absent is healthy, and said as not present since when. A source whose username (in its entry of the setting sources) or " +
"password (the secret smb-password-<source>) is not given waits for the operator: with nothing else found the " +
"answer is waits, naming each, never unhealthy. (r)",
Run: func(map[string]any) (any, error) { return m.Health(time.Now()), nil },
+41 -20
View File
@@ -149,9 +149,9 @@ func (f *fakeSystem) everything(except ...string) string {
}
const config = "shares=library=/srv/library downloads=/srv/downloads:ro\n" +
"sources=games=smb://192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"sources=games=smb://root@192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n" +
"smb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n"
"passwords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n"
func exportsAnswer(t *testing.T, shares ...ServerShare) string {
raw, err := json.Marshal(ExportsAnswer{Server: "homeserver", At: "192.0.2.10", Shares: shares})
@@ -365,18 +365,18 @@ func TestASilentServerRemovesNoMount(t *testing.T) {
func TestASourceWithoutItsLoginWaitsForTheOperatorAndIsNotMounted(t *testing.T) {
for name, c := range map[string]struct {
users, password string
writePassword bool
waitsFor []string
user, password string
writePassword bool
waitsFor []string
}{
"no username": {"", testPassword, true, []string{"setting smb-users"}},
"no password": {"games=root", "", false, []string{"secret smb-password-games"}},
"an empty password file": {"games=root", "", true, []string{"secret smb-password-games"}},
"a line break only": {"games=root", "\n", true, []string{"secret smb-password-games"}},
"neither": {"other=x", "", false, []string{"setting smb-users", "secret smb-password-games"}},
"no username": {"", testPassword, true, []string{"setting sources"}},
"no password": {"root@", "", false, []string{"secret smb-password-games"}},
"an empty password file": {"root@", "", true, []string{"secret smb-password-games"}},
"a line break only": {"root@", "\n", true, []string{"secret smb-password-games"}},
"neither": {"", "", false, []string{"setting sources", "secret smb-password-games"}},
} {
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users="+c.users+"\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write(ConfigFile, "sources=games=smb://"+c.user+"192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
if c.writePassword {
f.write("/var/lib/mesh/mounts/smb-password-games.secret", c.password)
}
@@ -426,7 +426,7 @@ func TestAPasswordIsOneLineKeptAsTyped(t *testing.T) {
t.Errorf("two lines are refused, saying nothing typed: %v", err)
}
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write("/var/lib/mesh/mounts/smb-password-games.secret", pw+"\n")
f.applier(time.Now()).Look(context.Background())
creds := filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials")
@@ -449,13 +449,34 @@ func TestAPasswordIsOneLineKeptAsTyped(t *testing.T) {
}
}
func TestTheUsersSettingIsReadAndWhatCannotBeIsSaid(t *testing.T) {
users, problems := ParseSMBUsers("games=root nas=media-user bad Nas=x nas=again weird=a=b")
if users["games"] != "root" || users["nas"] != "media-user" || len(users) != 2 || len(problems) != 4 {
func TestAnSMBSourcesUserIsInItsEntryAndAPasswordThereIsRefusedUnsaid(t *testing.T) {
sources, problems := ParseSources("a=smb://root@192.0.2.76/Share@/mnt/a b=smb://192.0.2.76/Share@/mnt/b " +
"c=smb://WORKGROUP%5Cme@nas/Media@/mnt/c d=smb://me%40example@nas/Media@/mnt/d:ro")
users := map[string]string{}
for _, s := range sources {
users[s.Name] = s.User
if s.Host == "" || s.Share == "" {
t.Errorf("%s: %+v", s.Name, s)
}
}
if len(problems) != 0 || users["a"] != "root" || users["b"] != "" || users["c"] != `WORKGROUP\me` || users["d"] != "me@example" {
t.Errorf("users %v problems %q", users, problems)
}
if users, problems := ParseSMBUsers(None); len(users) != 0 || len(problems) != 0 {
t.Errorf("none: %v %q", users, problems)
for what, entry := range map[string]string{
"a password in the URL": "e=smb://root:" + testPassword + "@nas/Media@/mnt/e",
"a password with a slash": "e=smb://root:a/" + testPassword + "@nas/Media@/mnt/e",
"a user with a space, encoded": "e=smb://a%20" + testPassword + "@nas/Media@/mnt/e",
"a bad percent escape": "e=smb://%zz" + testPassword + "@nas/Media@/mnt/e",
"no mount point, with a password": "smb://root:" + testPassword + "@nas/Media",
} {
got, problems := ParseSources(entry)
said := strings.Join(problems, " ")
if len(got) != 0 || len(problems) != 1 || strings.Contains(said, testPassword) {
t.Errorf("%s: %+v %q", what, got, said)
}
}
if _, problems := ParseSources("e=smb://root:" + testPassword + "@nas/Media@/mnt/e"); !strings.Contains(problems[0], "smb-password-<source>") {
t.Errorf("a password in the URL is said with where it goes: %q", problems)
}
}
@@ -687,7 +708,7 @@ func TestAWaitBesideAFaultIsUnhealthy(t *testing.T) {
func TestAPasswordThatCannotBeReadIsAFaultSaidWithoutValueOrPath(t *testing.T) {
// A directory where the file should be: read fails, and not as "not there" (as root a mode would not stop it).
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write("/var/lib/mesh/mounts/smb-password-games.secret/inside", testPassword)
st := f.applier(time.Now()).Look(context.Background())
if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "could not be read") ||
@@ -697,8 +718,8 @@ func TestAPasswordThatCannotBeReadIsAFaultSaidWithoutValueOrPath(t *testing.T) {
// No passwords= and no state=: the source is not set up, and no relative path is read.
for _, cfg := range []string{
"sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\n",
"sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\npasswords=relative/dir\n",
"sources=games=smb://root@192.0.2.76/Share@/mnt/games\n",
"sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=relative/dir\n",
} {
g := newSystem(t)
g.write(ConfigFile, cfg)
+2 -7
View File
@@ -47,12 +47,7 @@
"sources": {
"kind": "preference",
"default": "none",
"why": "a machine has no occasional source until its assignment names one: space-separated name=smb://<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro (hq ADR 0263 rule 6)"
},
"smb-users": {
"kind": "preference",
"default": "none",
"why": "the username each SMB source logs in with: space-separated name=username; not a secret, so a setting, beside each source's password in its own secret smb-password-<source> (hq ADR 0283)"
"why": "a machine has no occasional source until its assignment names one: space-separated name=smb://[<user>@]<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro (hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password-<name> (hq ADR 0283)"
}
},
"own-secrets": {
@@ -89,7 +84,7 @@
"type": "file",
"path": "/etc/mesh-mounts/mounts.conf",
"mode": "0644",
"content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares, sources and smb-users, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nsmb-users=${setting:smb-users}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n"
"content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n"
},
{
"id": "bus-dir-parent",