Files
mesh-catalog/modules/mounts/cmd/mounts/apply.go
T

703 lines
26 KiB
Go

package main
// The module's two long-running processes, run by the node-engine as root (novox/hq ADR 0263, to-be 52 §2),
// over one pass of the same code every 30 seconds:
//
// - `mounts apply` acts: it writes or removes the module's own unit pairs and the SMB logins, makes a
// missing mount point and arms the automounts, and leaves what it could not do in AppliedFile. The
// service manager judges it by its unit;
// - `mounts watch` judges: it tries each source without touching a mount point and leaves what it found
// in StatusFile, which the health check `mounts_health`, the tools and the bundle read.
//
// Neither reaches the bus: the bundle reads what the sharing machine exports and leaves it in ExportsFile,
// and puts StatusFile in the module's state.
//
// What it never does: write, stop or remove a unit file without the module's marker; chown or chmod a
// mount point (it makes one that is missing, and nothing else); remove a pair because the sharing machine
// did not answer — a pair goes only when the settings stop naming it.
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"os/user"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// LookEvery is how often the process looks.
const LookEvery = 30 * time.Second
// ClearEvery is how often the acting process clears a failed mount of its own whose source does not
// answer (clearFailed). Shorter than the node-engine's two looks of 15 s, so a mount that failed because
// its source is away is never read as a failed unit of the machine's own.
const ClearEvery = 5 * time.Second
// ProbeTimeout bounds one try of a source.
const ProbeTimeout = 2 * time.Second
// The states of a share (to-be 52 §2).
const (
StateArmed = "armed" // the automount waits for the first use
StateMounted = "mounted" // mounted now
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
)
// ExportsAnswer is what the bundle left from the sharing machine's `node-nfs-server.exports`.
type ExportsAnswer struct {
Server string `json:"server"`
At string `json:"at,omitempty"`
Asked string `json:"asked"`
Shares []ServerShare `json:"shares"`
// Error is why the last ask got no answer; the shares are then the last answer's.
Error string `json:"error,omitempty"`
Answered string `json:"answered,omitempty"`
}
// ShareStatus is one share or source as the process found it.
type ShareStatus struct {
Name string `json:"name"`
Kind string `json:"kind"`
Occasional bool `json:"occasional"`
Mountpoint string `json:"mountpoint"`
What string `json:"what,omitempty"`
Probe string `json:"probe,omitempty"`
State string `json:"state"`
Since string `json:"since"`
Why string `json:"why,omitempty"`
MountUnit string `json:"mount_unit,omitempty"`
AutoUnit string `json:"automount_unit,omitempty"`
}
// Status is what one look found.
type Status struct {
Looked string `json:"looked"`
Server string `json:"server,omitempty"`
Shares []ShareStatus `json:"shares"`
Problems []string `json:"problems,omitempty"`
Changed []string `json:"changed,omitempty"`
}
// Applier is the process, over one machine. Act writes and arms; Judge tries and says. A test does both.
type Applier struct {
M *Machine
Act bool
Judge bool
// Dial tries a host:port; Now is the clock; Lookup finds an account's uid and gid.
Dial func(addr string, timeout time.Duration) error
Now func() time.Time
Lookup func(account string) (int, int, error)
// written is every pair the last pass wrote and armed, by its mount unit: what clearFailed looks at
// between passes. The acting process's own memory; nothing else reads it.
written map[string]Pair
// LookInterval and ClearInterval are LookEvery and ClearEvery, or a test's own when set.
LookInterval, ClearInterval time.Duration
}
// NewApplier is the process on this machine.
func NewApplier() *Applier {
return &Applier{
M: Here(),
Dial: func(addr string, timeout time.Duration) error {
c, err := net.DialTimeout("tcp", addr, timeout)
if err == nil {
c.Close()
}
return err
},
Now: time.Now,
Lookup: func(account string) (int, int, error) {
u, err := user.Lookup(account)
if err != nil {
return -1, -1, err
}
uid, _ := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid)
return uid, gid, nil
},
}
}
func (a *Applier) path(p string) string { return filepath.Join(a.M.Root, p) }
// writeIfChanged writes a file through a temporary beside it, only when its content differs.
func (a *Applier) writeIfChanged(p, content string, mode os.FileMode) (bool, error) {
full := a.path(p)
if old, err := os.ReadFile(full); err == nil && string(old) == content {
if info, err := os.Stat(full); err == nil && info.Mode().Perm() == mode {
return false, nil
}
}
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return false, err
}
tmp := full + ".mesh-new"
if err := os.WriteFile(tmp, []byte(content), mode); err != nil {
return false, err
}
if err := os.Chmod(tmp, mode); err != nil {
os.Remove(tmp)
return false, err
}
return true, os.Rename(tmp, full)
}
// ownUnits are the unit files in UnitDir that carry the module's marker, by name.
func (a *Applier) ownUnits() map[string]bool {
out := map[string]bool{}
for _, pattern := range []string{"*.mount", "*.automount"} {
for _, p := range a.M.glob(filepath.Join(UnitDir, pattern)) {
if IsOwnUnit(a.M, p) {
out[filepath.Base(p)] = true
}
}
}
return out
}
// IsOwnUnit says whether a unit file opens with the module's marker.
func IsOwnUnit(m *Machine, p string) bool {
text, err := m.read(p)
return err == nil && strings.HasPrefix(text, Marker)
}
// unitStates are the service manager's states for some units: ActiveState, SubState, Result and
// UnitFileState, by unit.
func (a *Applier) unitStates(ctx context.Context, units []string) map[string]map[string]string {
out := map[string]map[string]string{}
if len(units) == 0 {
return out
}
args := append([]string{"show", "-p", "Id,ActiveState,SubState,Result,UnitFileState", "--no-pager"}, units...)
shown, _ := a.M.Run(ctx, "systemctl", args...)
for _, block := range strings.Split(shown, "\n\n") {
kv := map[string]string{}
for _, line := range strings.Split(block, "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
kv[k] = v
}
}
if kv["Id"] != "" {
out[kv["Id"]] = kv
}
}
return out
}
func readJSON[T any](m *Machine, p string) (T, error) {
var v T
text, err := m.read(p)
if err != nil {
return v, err
}
return v, json.Unmarshal([]byte(text), &v)
}
// Look is one pass: the units written as asked, the automounts armed, and each share found.
func (a *Applier) Look(ctx context.Context) Status {
now := a.Now()
st := Status{Looked: now.Format(time.RFC3339), Shares: []ShareStatus{}}
previous, _ := readJSON[Status](a.M, StatusFile)
since := map[string]ShareStatus{}
for _, s := range previous.Shares {
since[s.Name] = s
}
text, err := a.M.read(ConfigFile)
if err != nil {
st.Problems = append(st.Problems, unreadable(ConfigFile, err)+"; nothing is written or removed until the mesh places it")
a.finish(&st, nil)
return st
}
cfg := ParseConfig(text)
st.Server = cfg.ServerNode()
st.Problems = append(st.Problems, cfg.Problems...)
// Where the mesh keeps data on this machine (finding 4d). Without it nothing new is set up: a pair the
// module already has stays, and one it does not have waits until the list is read.
data, derr := readJSON[DataPaths](a.M, DataFile)
haveData := derr == nil && data.Paths != nil
if !haveData {
why := "it has not been left by the module's tools yet"
if derr == nil && data.Error != "" {
why = data.Error
}
st.Problems = append(st.Problems, "where the mesh keeps data on this machine is not known ("+why+"); nothing new is set up until it is")
}
exports, eerr := readJSON[ExportsAnswer](a.M, ExportsFile)
byName := map[string]ServerShare{}
for _, e := range exports.Shares {
byName[e.Name] = e
}
var pairs []Pair
notSet := map[string]ShareStatus{} // a share named but not set up, with why
for _, w := range cfg.Shares {
e, ok := byName[w.Name]
switch {
case cfg.At == "":
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: "no machine shares nfs-share for this one: the binding gives no address"}
case cfg.Node != "" && st.Server == cfg.Node:
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: "this machine shares it itself (" + cfg.Node + "): a share is never mounted onto the machine that serves it"}
case !ok && eerr != nil:
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateUnreachable,
Why: "what the sharing machine " + st.Server + " exports has not been read from the bus yet"}
case !ok:
why := "the sharing machine " + exports.Server + " does not export a share named " + w.Name
if exports.Error != "" {
why = "what the sharing machine " + st.Server + " exports could not be read from the bus: " + exports.Error
}
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed, Why: why}
case !strings.HasPrefix(e.Path, "/") || strings.ContainsAny(e.Path, " \t\n\\\"'"):
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: fmt.Sprintf("the sharing machine gave the path %q, which the module does not mount", e.Path)}
default:
mine, exported := e.ForNode(cfg.Node)
if !exported {
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: "the sharing machine " + st.Server + " does not export " + w.Name + " to this machine: its grants do not name " + cfg.Node}
continue
}
pairs = append(pairs, NFSPair(w, cfg.At, mine))
}
}
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 {
uid, gid = u, g
}
}
for _, s := range cfg.Sources {
switch s.Kind {
case "disk":
pairs = append(pairs, DiskPair(s))
case "smb":
c, ok := creds[s.Name]
if !ok || 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: put a [%s] section in the module's secret with `secret accept <node> mounts smb-credentials --from <file>`", s.Name)}
continue
}
file := filepath.Join(cfg.State, "smb-"+s.Name+".credentials")
changed, err := false, error(nil)
if a.Act {
changed, err = a.writeIfChanged(file, c.CredentialsFile(), 0o600)
}
if err != nil {
notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateFailed,
Why: "its credentials file could not be written: " + err.Error()}
continue
}
if changed {
st.Changed = append(st.Changed, "the login of "+s.Name)
}
pairs = append(pairs, SMBPair(s, file, uid, gid))
}
}
// Every mount point the settings still name, set up or not: a pair is removed only when its mount
// point is no longer named, never because an answer is missing.
named := map[string]bool{}
for _, w := range cfg.Shares {
named[EscapePath(w.Mountpoint)] = true
}
for _, s := range cfg.Sources {
named[EscapePath(s.Mountpoint)] = true
}
own := a.ownUnits()
mounted := ParseMountinfo(func() string { t, _ := a.M.read("/proc/self/mountinfo"); return t }())
changed := false
// The mount units this pass wrote where no unit file of the module's was before: a unit of the same
// name that was there — the one fstab's generator makes until the line is adopted — is taken over.
takenOver := map[string]bool{}
var written []Pair
for _, p := range pairs {
ours := own[p.MountUnit] && own[p.AutoUnit]
refuse := func(why string) {
notSet[p.Name] = ShareStatus{Name: p.Name, Kind: p.Kind, Occasional: p.Occasional, Mountpoint: p.Mountpoint, What: p.What,
State: StateFailed, Why: why}
}
if haveData {
if err := nearData(p.Mountpoint, data.Paths); err != nil {
refuse(err.Error())
continue
}
} else if !ours {
refuse("where the mesh keeps data on this machine is not known yet (the controller's data verb), so a new mount point is not set up")
continue
}
if err := a.M.mountpointFree(p, own, mounted); err != nil {
refuse(err.Error())
continue
}
if err := a.foreign(p, own); err != nil {
notSet[p.Name] = ShareStatus{Name: p.Name, Kind: p.Kind, Occasional: p.Occasional, Mountpoint: p.Mountpoint, What: p.What,
State: StateFailed, Why: err.Error()}
continue
}
if !a.Act {
written = append(written, p)
continue
}
c1, err1 := a.writeIfChanged(filepath.Join(UnitDir, p.MountUnit), p.Mount, 0o644)
c2, err2 := a.writeIfChanged(filepath.Join(UnitDir, p.AutoUnit), p.Automount, 0o644)
if err1 != nil || err2 != nil {
notSet[p.Name] = ShareStatus{Name: p.Name, Kind: p.Kind, Occasional: p.Occasional, Mountpoint: p.Mountpoint, What: p.What,
State: StateFailed, Why: fmt.Sprintf("its units could not be written: %v %v", err1, err2)}
continue
}
if c1 && !own[p.MountUnit] {
takenOver[p.MountUnit] = true
}
if c1 || c2 {
changed = true
st.Changed = append(st.Changed, "the units of "+p.Name)
}
// A missing mount point is made; one that is there is left exactly as it is. An automount point
// is in mountinfo and is never looked at, so a server that is away cannot hold this look.
if _, there := mounted[p.Mountpoint]; !there {
if _, err := os.Lstat(a.path(p.Mountpoint)); os.IsNotExist(err) {
if err := os.MkdirAll(a.path(p.Mountpoint), 0o755); err == nil {
st.Changed = append(st.Changed, "made the mount point "+p.Mountpoint)
}
}
}
written = append(written, p)
}
// The module's own pairs whose mount point nothing names any more: stopped, disabled and removed —
// unless an entry of the settings could not be read, when nobody can tell which mount point it named
// (finding 8), or what is mounted there is not the pair's own source (finding 4c).
var gone []string
if len(cfg.Unreadable) > 0 {
if a.Act {
st.Problems = append(st.Problems, "nothing is removed while an entry of shares or sources cannot be read: "+strings.Join(cfg.Unreadable, "; "))
}
} else {
kept := map[string]bool{}
for unit := range own {
base := strings.TrimSuffix(strings.TrimSuffix(unit, ".automount"), ".mount")
if named[base] || !a.Act || kept[base] {
continue
}
text, _ := a.M.read(filepath.Join(UnitDir, base+".mount"))
what, where := unitField(text, "What"), unitField(text, "Where")
if where != "" {
if err := a.M.notOwnSource(kindOf(what, unitField(text, "Type")), what, where, mounted); err != nil {
kept[base] = true
st.Problems = append(st.Problems, fmt.Sprintf("the pair %s is no longer named and is kept, and nothing is stopped: %v", base, err))
continue
}
}
gone = append(gone, unit)
}
var still []string
for _, unit := range gone {
if !kept[strings.TrimSuffix(strings.TrimSuffix(unit, ".automount"), ".mount")] {
still = append(still, unit)
}
}
gone = still
}
sort.Strings(gone)
for _, unit := range gone {
if strings.HasSuffix(unit, ".automount") {
a.M.Run(ctx, "systemctl", "disable", "--now", unit)
} else {
a.M.Run(ctx, "systemctl", "stop", unit)
}
}
for _, unit := range gone {
if err := os.Remove(a.path(filepath.Join(UnitDir, unit))); err == nil {
changed = true
st.Changed = append(st.Changed, "removed "+unit+", which nothing names any more")
}
}
if changed {
if _, err := a.M.Run(ctx, "systemctl", "daemon-reload"); err != nil {
st.Problems = append(st.Problems, "the service manager did not reload: "+err.Error())
}
}
// Arm every automount that is not.
var units []string
for _, p := range written {
units = append(units, p.MountUnit, p.AutoUnit)
}
states := a.unitStates(ctx, units)
for _, p := range written {
auto := states[p.AutoUnit]
if a.Act && (auto["UnitFileState"] != "enabled" || auto["ActiveState"] != "active") {
if _, err := a.M.Run(ctx, "systemctl", "enable", "--now", p.AutoUnit); err != nil {
st.Problems = append(st.Problems, fmt.Sprintf("%s was not armed: %v", p.AutoUnit, err))
} else {
st.Changed = append(st.Changed, "armed "+p.AutoUnit)
}
}
}
if a.Act {
// A mount unit the module wrote for the first time, over a unit of the same name that had already
// failed — the one fstab's generator makes until the line is adopted — carries a failure that is not
// the module's pair's. It is cleared, so the pair starts armed and is judged on its own mounts (ADR
// 0263 rule 4). A pair the module already had and rewrote keeps its failure: that one is its own.
for _, p := range written {
if takenOver[p.MountUnit] && states[p.MountUnit]["ActiveState"] == "failed" {
if _, err := a.M.Run(ctx, "systemctl", "reset-failed", p.MountUnit); err == nil {
st.Changed = append(st.Changed, "cleared "+p.MountUnit+", which had failed before the module wrote it")
}
}
}
a.written = map[string]Pair{}
for _, p := range written {
a.written[p.MountUnit] = p
}
st.Changed = append(st.Changed, a.clearFailed(ctx)...)
}
if !a.Judge {
a.finish(&st, notSet)
return st
}
if len(written) > 0 {
states = a.unitStates(ctx, units)
}
// What the acting process could not do, which only it knows: a unit or a login it could not write.
if applied, err := readJSON[Applied](a.M, AppliedFile); err == nil && !a.Act {
st.Problems = append(st.Problems, applied.Problems...)
for _, s := range applied.NotSet {
if _, said := notSet[s.Name]; !said {
notSet[s.Name] = s
}
}
}
for _, p := range written {
if _, failed := notSet[p.Name]; failed {
continue
}
s := ShareStatus{Name: p.Name, Kind: p.Kind, Occasional: p.Occasional, Mountpoint: p.Mountpoint, What: p.What,
Probe: p.Probe, MountUnit: p.MountUnit, AutoUnit: p.AutoUnit}
s.State, s.Why = a.judge(p, states, mounted)
st.Shares = append(st.Shares, s)
}
for _, s := range notSet {
st.Shares = append(st.Shares, s)
}
sort.Slice(st.Shares, func(i, j int) bool { return st.Shares[i].Name < st.Shares[j].Name })
for i := range st.Shares {
s := &st.Shares[i]
if prev, ok := since[s.Name]; ok && prev.State == s.State && prev.Since != "" {
s.Since = prev.Since
} else {
s.Since = st.Looked
}
}
a.finish(&st, notSet)
return st
}
// AppliedFile is what the acting process could not do, for the judging one.
const AppliedFile = RunDir + "/applied.json"
// Applied is the acting process's last pass.
type Applied struct {
Looked string `json:"looked"`
Problems []string `json:"problems,omitempty"`
NotSet []ShareStatus `json:"not_set,omitempty"`
Changed []string `json:"changed,omitempty"`
}
// finish leaves what this pass found: the acting process's account, the judging one's status.
func (a *Applier) finish(st *Status, notSet map[string]ShareStatus) {
if a.Act && !a.Judge {
ap := Applied{Looked: st.Looked, Problems: st.Problems, Changed: st.Changed}
for _, s := range notSet {
ap.NotSet = append(ap.NotSet, s)
}
sort.Slice(ap.NotSet, func(i, j int) bool { return ap.NotSet[i].Name < ap.NotSet[j].Name })
raw, _ := json.MarshalIndent(ap, "", " ")
if _, err := a.writeIfChanged(AppliedFile, string(raw)+"\n", 0o644); err != nil {
fmt.Fprintln(os.Stderr, "mounts: what was applied was not written:", err)
}
}
if a.Judge {
a.writeStatus(st)
}
}
// foreign refuses a pair whose unit file exists and is not the module's.
func (a *Applier) foreign(p Pair, own map[string]bool) error {
for _, unit := range []string{p.MountUnit, p.AutoUnit} {
// A transient unit of the same name — made by `systemd-mount` or `mount` on the command line — is
// someone's mount now, whatever the module's own file says.
transient := filepath.Join(TransientDir, unit)
if _, err := os.Lstat(a.path(transient)); err == nil {
return fmt.Errorf("%s has a transient unit that is not the module's (%s); it is left as it is, and this share is not mounted here", p.Mountpoint, transient)
}
if own[unit] {
continue
}
if _, err := os.Lstat(a.path(filepath.Join(UnitDir, unit))); err == nil {
return fmt.Errorf("%s already has a unit file that is not the module's (%s); it is left as it is, and this share is not mounted here", p.Mountpoint, filepath.Join(UnitDir, unit))
}
}
return nil
}
// TransientDir is where the service manager keeps the units made at run time rather than from a file.
const TransientDir = "/run/systemd/transient"
// judge is one pair's state. The source is tried first, without touching the mount point.
func (a *Applier) judge(p Pair, states map[string]map[string]string, mounted map[string]Mounted) (string, string) {
there := true
var tried error
if p.Kind == "disk" {
if !a.M.present(p.Probe) {
there, tried = false, fmt.Errorf("the disk %s is not plugged in", p.Probe)
}
} else if err := a.Dial(p.Probe, ProbeTimeout); err != nil {
there, tried = false, err
}
mnt, auto := states[p.MountUnit], states[p.AutoUnit]
cur, isMounted := mounted[p.Mountpoint]
isMounted = isMounted && cur.Type != "autofs"
switch {
case !there && p.Occasional:
return StateAbsent, "not present: " + tried.Error()
case !there:
return StateUnreachable, fmt.Sprintf("%s does not answer: %v", p.Probe, tried)
case isMounted:
return StateMounted, ""
case mnt["ActiveState"] == "failed":
return StateFailed, fmt.Sprintf("its last mount failed (%s); see the journal of %s", orDefault(mnt["Result"], "failed"), p.MountUnit)
case auto["ActiveState"] == "active":
return StateArmed, ""
default:
return StateFailed, fmt.Sprintf("its automount %s is not armed (%s)", p.AutoUnit, orDefault(auto["ActiveState"], "unknown"))
}
}
func (a *Applier) writeStatus(st *Status) {
raw, _ := json.MarshalIndent(st, "", " ")
if _, err := a.writeIfChanged(StatusFile, string(raw)+"\n", 0o644); err != nil {
fmt.Fprintln(os.Stderr, "mounts: the status was not written:", err)
}
}
// clearFailed clears every failed mount of the module's own pairs whose source does not answer now: an
// occasional source that is away, or a node's share whose server is. Such a mount failed because its
// source was not there, which the module already says as the share's state (absent, or unreachable, and
// for a share that should always be there its health after five minutes, ADR 0263 rule 8); left failed,
// the unit would be said a second time as a failed unit of the machine's own, with a notification each
// time it is touched — what rules 4 and 6 promise never happens. A mount that failed while its source
// answers is left failed: that is a fault, and the share is said failed until a person looks.
func (a *Applier) clearFailed(ctx context.Context) []string {
if len(a.written) == 0 {
return nil
}
var mounts, units []string
for unit, p := range a.written {
mounts = append(mounts, unit)
units = append(units, unit, p.AutoUnit)
}
sort.Strings(mounts)
states := a.unitStates(ctx, units)
var cleared []string
for _, unit := range mounts {
p := a.written[unit]
mountFailed := states[unit]["ActiveState"] == "failed"
// After repeated failed mounts the automount gives up too (mount-start-limit-hit) and stops
// answering its point: cleared and armed again, or the share stays dead until the next pass.
autoFailed := states[p.AutoUnit]["ActiveState"] == "failed"
if !mountFailed && !autoFailed {
continue
}
if a.sourceAnswers(p) {
continue
}
if mountFailed {
if _, err := a.M.Run(ctx, "systemctl", "reset-failed", unit); err == nil {
cleared = append(cleared, "cleared "+unit+": "+p.Name+" was not there to mount")
}
}
if autoFailed {
if _, err := a.M.Run(ctx, "systemctl", "reset-failed", p.AutoUnit); err == nil {
if _, err := a.M.Run(ctx, "systemctl", "start", p.AutoUnit); err == nil {
cleared = append(cleared, "cleared and armed "+p.AutoUnit+" again ("+orDefault(states[p.AutoUnit]["Result"], "failed")+"): "+p.Name+" was not there to mount")
}
}
}
}
return cleared
}
// sourceAnswers tries a pair's source the way the judging process does, without touching its mount point.
func (a *Applier) sourceAnswers(p Pair) bool {
if p.Kind == "disk" {
return a.M.present(p.Probe)
}
return a.Dial != nil && a.Dial(p.Probe, ProbeTimeout) == nil
}
// RunApply looks every LookEvery until the context ends, saying on stderr what it changed. Between looks
// the acting process clears, every ClearEvery, a mount of its own that failed because its source was away.
func RunApply(ctx context.Context, a *Applier) {
for {
st := a.Look(ctx)
for _, c := range st.Changed {
fmt.Fprintln(os.Stderr, "mounts:", c)
}
next := time.After(orInterval(a.LookInterval, LookEvery))
between:
for {
select {
case <-ctx.Done():
return
case <-next:
break between
case <-time.After(orInterval(a.ClearInterval, ClearEvery)):
if a.Act {
for _, c := range a.clearFailed(ctx) {
fmt.Fprintln(os.Stderr, "mounts:", c)
}
}
}
}
}
}
func orInterval(d, otherwise time.Duration) time.Duration {
if d > 0 {
return d
}
return otherwise
}