Files
mesh-catalog/modules/zfs/cmd/zfs-tools/applied.go
T

164 lines
5.9 KiB
Go

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)
}