164 lines
5.9 KiB
Go
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)
|
|
}
|