package main import ( "encoding/json" "errors" "fmt" "time" ) // What the module says and keeps of what it changed (design 42 zfs-owns-zfs §25, §26). // // Each kind of event has a subject of its own on the module's events stream (the tool runner's or the // process's `Emit`, by the kind's name). The state `applied` holds one key per config value, `{before, after, // at, outcome}`, written by the apply and deleted by it when the value goes back to not managed, after its // last `config.applied`. // The module's event kinds (§25), as the subject's last words. const ( EventApplied = "config.applied" EventRefused = "config.refused" EventWaiting = "config.waiting" EventTakenPerson = "snapshot.taken.person" EventTakenSchedule = "snapshot.taken.schedule" EventRemovedPerson = "snapshot.removed.person" EventRemovedSched = "snapshot.removed.schedule" EventSkippedPerson = "snapshot.skipped.person" EventSkippedSched = "snapshot.skipped.schedule" EventRunSkipped = "run.skipped" EventScrubRequested = ScrubEvent ) // The module's states (§26): the state's name, as `requires` declares it. const ( StateApplied = "applied" StateSchedule = "schedule" StateFindings = "findings" ) // The outcomes a key of the state `applied` holds. const ( OutcomeApplied = "applied" OutcomeRefused = "refused" OutcomeWaiting = "waiting" OutcomeFailed = "failed" // OutcomeAwaitingLoad is a kernel module parameter set in the options file whose live value differs: it // takes effect at the next load of the kernel module (§18, §29). OutcomeAwaitingLoad = "awaiting-load" ) // StateStore is the module's states on the bus, as the process or the tools bundle reaches them. type StateStore interface { Put(state, key string, value any) error Delete(state, key string) error // Get answers false when the key is not there. Get(state, key string) (json.RawMessage, bool, error) // Keys are the keys of a state. Keys(state string) ([]string, error) } // AppliedEntry is one key of the state `applied`. type AppliedEntry struct { Before string `json:"before"` After string `json:"after"` At time.Time `json:"at"` Outcome string `json:"outcome"` // Why is the reason of a refusal or a failure, WaitsOn what a wait waits on, Since when it began. Why string `json:"why,omitempty"` WaitsOn string `json:"waits-on,omitempty"` Since time.Time `json:"since,omitzero"` } // notSaid is a change, refusal or wait the bus did not take: the machine is as the apply left it, but the // event or the key is missing. It is a warning, never a failed command (§18: with the bus down the apply // converges from the config file it last got). type notSaid struct{ err error } func (e notSaid) Error() string { return e.err.Error() } func (e notSaid) Unwrap() error { return e.err } // say publishes one event of the module. func (m *Machine) say(kind string, body map[string]any) error { if m.Emit == nil { return notSaid{fmt.Errorf("this process publishes no events: %s was not said", kind)} } if err := m.Emit(kind, body); err != nil { return notSaid{err} } return nil } // failure records one failed step of the apply: a failed command is kept under its key and answered as the // apply's failure; a change the bus did not take is only a warning. func (m *Machine) failure(failed *[]error, key string, err error) { var ns notSaid if errors.As(err, &ns) { m.Warnings = append(m.Warnings, fmt.Sprintf("%s: %v", key, err)) return } *failed = append(*failed, fmt.Errorf("%s: %v", key, err)) _ = m.CommandFailed(key, err) } // keep writes a key of the state `applied`. func (m *Machine) keep(key string, e AppliedEntry) error { if m.States == nil { return notSaid{fmt.Errorf("this process keeps no state: %s was not kept", key)} } e.At = m.Now().UTC() if err := m.States.Put(StateApplied, key, e); err != nil { return notSaid{err} } return nil } // Changed says a change as `config.applied` {target, key, before, after, read-back} and keeps it. The event // goes first: the state is deleted only after the last event of its key. func (m *Machine) Changed(target, key, before, after, readBack string) error { if err := m.say(EventApplied, map[string]any{"target": target, "key": key, "before": before, "after": after, "read-back": readBack}); err != nil { return err } return m.keep(key, AppliedEntry{Before: before, After: after, Outcome: OutcomeApplied}) } // Refused says a refusal as `config.refused` {key, value, reason} and keeps it, so zfs_health is unhealthy // naming the key (§29). func (m *Machine) Refused(r Refusal) error { if err := m.say(EventRefused, map[string]any{"key": r.Key, "value": r.Value, "reason": r.Reason}); err != nil { return err } return m.keep(r.Key, AppliedEntry{After: r.Value, Outcome: OutcomeRefused, Why: r.Reason}) } // Waiting says a wait as `config.waiting` {key, waits-on, since} and keeps it; a wait already kept keeps // its own since. func (m *Machine) Waiting(key, waitsOn string) error { since := m.Now().UTC() if m.States != nil { if raw, ok, err := m.States.Get(StateApplied, key); err == nil && ok { var old AppliedEntry if json.Unmarshal(raw, &old) == nil && old.Outcome == OutcomeWaiting && old.WaitsOn == waitsOn && !old.Since.IsZero() { since = old.Since } } } if err := m.say(EventWaiting, map[string]any{"key": key, "waits-on": waitsOn, "since": since.Format(time.RFC3339)}); err != nil { return err } return m.keep(key, AppliedEntry{Outcome: OutcomeWaiting, WaitsOn: waitsOn, Since: since}) } // CommandFailed keeps a failed command under its key; the apply then exits non-zero (A-8). func (m *Machine) CommandFailed(key string, cause error) error { return m.keep(key, AppliedEntry{Outcome: OutcomeFailed, Why: cause.Error()}) } // NotManaged is a value gone back to not managed: its key leaves the state, after its last `config.applied`. func (m *Machine) NotManaged(key string) error { if m.States == nil { return nil } return m.States.Delete(StateApplied, key) }