The keeper process runs under its own account with a bus account of its own, keeps the six states and emits the nine events of ADR 0507 and ADR 0508; its config (upgrade-order, record-days, round-start-minutes) lands in its config file and restarts it. shapes.go fixes every key's value and event body as the interface the keeper and the verbs share. The weekly timer and its service are dropped, which the node-engine removes.
469 lines
18 KiB
Go
469 lines
18 KiB
Go
package main
|
|
|
|
// The keeper's bus shapes (novox/hq ADR 0507, ADR 0508): the value of every key of the module's states and the
|
|
// body of every event its process emits, in one place, so the process that writes them and the verbs that read
|
|
// them agree by type. Each is a struct of named fields only: no map and no free-form value, so nothing but what
|
|
// is named here goes on the bus (ADR 0507 §1: package names, versions, unit and container names, file paths and
|
|
// exit statuses, never a process's command line or environment). A time in a value is the time of the thing it
|
|
// describes (a transaction, a step, an opening), never the time it was read: a key is written only when what it
|
|
// describes changed (ADR 0289 §1), and no field changes on a read alone (ADR 0289 §4). When a key last changed
|
|
// is the bus's to say (its revision), not a field's.
|
|
//
|
|
// A key is `<node>` or `<node>.<id>`: a node name and an id are each one key token ([A-Za-z0-9_-]), so the
|
|
// dot only ever separates them.
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// The module's states (manifest `state` and the keeper's `bus` `state`).
|
|
const (
|
|
// StateInstalled holds each node's installed packages: key `<node>` an InstalledValue, and key
|
|
// `<node>.not-current` a NotCurrent while the node's packages are over the bus's cap (ADR 0507 §2).
|
|
StateInstalled = "installed"
|
|
// StateUpdates holds what a full upgrade would change: key `<node>` an UpdatesValue (ADR 0507 §4).
|
|
StateUpdates = "updates"
|
|
// StateRuns holds every upgrade and rollback: key `<node>.<run>` a Run, kept (ADR 0508 §1).
|
|
StateRuns = "runs"
|
|
// StateRecords holds the package record taken before a run: key `<node>.<run>` a Record, deleted after
|
|
// the config value `record-days` (ADR 0508 §2).
|
|
StateRecords = "records"
|
|
// StateRounds holds the weekly rounds: key `<round>` a Round, created once by the first node of the order,
|
|
// and key `<round>.<node>` that node's RoundNode (ADR 0508 §5, §6).
|
|
StateRounds = "rounds"
|
|
// StateAsks holds the acts asked of a node: key `<node>.<id>` an Ask, taken by that node's keeper alone
|
|
// (ADR 0508 §8).
|
|
StateAsks = "asks"
|
|
)
|
|
|
|
// States are the module's states, in the manifest's order.
|
|
var States = []string{StateInstalled, StateUpdates, StateRuns, StateRecords, StateRounds, StateAsks}
|
|
|
|
// The events the keeper emits (the keeper's `bus` `emits`, and the manifest's `emits`).
|
|
const (
|
|
EventPackageChanged = "package.changed" // PackageChanged (ADR 0507 §3)
|
|
EventRecorded = "upgrade.recorded" // UpgradeRecorded (ADR 0508 §1, §2)
|
|
EventKernelHeld = "upgrade.kernel-held" // KernelHeld (ADR 0508 §3)
|
|
EventStarted = "upgrade.started" // UpgradeStarted
|
|
EventFinished = "upgrade.finished" // UpgradeFinished
|
|
EventFailed = "upgrade.failed" // UpgradeFailed
|
|
EventChecked = "upgrade.checked" // UpgradeChecked (ADR 0508 §4)
|
|
EventRollbackStarted = "rollback.started" // RollbackStarted (ADR 0508 §7)
|
|
EventRollbackFinished = "rollback.finished" // RollbackFinished
|
|
)
|
|
|
|
// Events are the events the keeper emits, each with the type of its body.
|
|
var Events = []struct {
|
|
Name string
|
|
Body any
|
|
}{
|
|
{EventPackageChanged, PackageChanged{}},
|
|
{EventRecorded, UpgradeRecorded{}},
|
|
{EventKernelHeld, KernelHeld{}},
|
|
{EventStarted, UpgradeStarted{}},
|
|
{EventFinished, UpgradeFinished{}},
|
|
{EventFailed, UpgradeFailed{}},
|
|
{EventChecked, UpgradeChecked{}},
|
|
{EventRollbackStarted, RollbackStarted{}},
|
|
{EventRollbackFinished, RollbackFinished{}},
|
|
}
|
|
|
|
// Key names.
|
|
|
|
// NotCurrentKey is the key in `installed` that marks a node's packages as not current.
|
|
func NotCurrentKey(node string) string { return node + ".not-current" }
|
|
|
|
// NodeKey is a key `<node>.<id>`: a run in `runs` or `records`, an ask in `asks`.
|
|
func NodeKey(node, id string) string { return node + "." + id }
|
|
|
|
// RoundNodeKey is a node's key in a round: `<round>.<node>` in `rounds`.
|
|
func RoundNodeKey(round, node string) string { return round + "." + node }
|
|
|
|
// RunID names a run by the moment it began, in UTC: `20261024T040012Z`. A run read from the package manager's
|
|
// log is named by the log's time of its start the same way.
|
|
func RunID(began time.Time) string { return began.UTC().Format("20060102T150405Z") }
|
|
|
|
// RoundID names the round of the ISO week that holds the given moment, in its own time zone: `2026-W43`. The
|
|
// round opens on Saturday 04:00 local time, so the local date decides the week.
|
|
func RoundID(at time.Time) string {
|
|
year, week := at.ISOWeek()
|
|
return fmt.Sprintf("%04d-W%02d", year, week)
|
|
}
|
|
|
|
// installed
|
|
|
|
// InstalledValue is `installed.<node>`: every installed package, sorted by name, and the time of the transaction
|
|
// that left the database so (ADR 0507 §2). Written compactly: one short entry per package.
|
|
type InstalledValue struct {
|
|
At time.Time `json:"at"`
|
|
Packages []InstalledPackage `json:"packages"`
|
|
}
|
|
|
|
// InstalledPackage is one installed package: its version, whether it was installed explicitly (else as a
|
|
// dependency), and its repository, or RepoForeign when no repository holds it (ADR 0507 §7).
|
|
type InstalledPackage struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Explicit bool `json:"explicit"`
|
|
Repo string `json:"repo"`
|
|
}
|
|
|
|
// RepoForeign marks a package from outside the repositories.
|
|
const RepoForeign = "foreign"
|
|
|
|
// NotCurrent is `installed.<node>.not-current`: the node's packages came to more than the bus's cap on a value,
|
|
// so `installed.<node>` keeps the last value written. Written once, when it first happens, and deleted at the
|
|
// next successful write of `installed.<node>`.
|
|
type NotCurrent struct {
|
|
Bytes int `json:"bytes"`
|
|
Since time.Time `json:"since"`
|
|
}
|
|
|
|
// updates
|
|
|
|
// UpdatesValue is `updates.<node>`: every package a full upgrade would change, sorted by name, and whether the
|
|
// node reached its mirrors (ADR 0507 §4). A failed read writes Reachable false and keeps the last list, never an
|
|
// empty list in its place.
|
|
type UpdatesValue struct {
|
|
Reachable bool `json:"reachable"`
|
|
Packages []VersionChange `json:"packages"`
|
|
}
|
|
|
|
// VersionChange is one package going from one version to another.
|
|
type VersionChange struct {
|
|
Name string `json:"name"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// PackageVersion is one package at one version.
|
|
type PackageVersion struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// runs
|
|
|
|
// The kinds of run.
|
|
const (
|
|
RunUpgrade = "upgrade"
|
|
RunRollback = "rollback"
|
|
)
|
|
|
|
// What started a run.
|
|
const (
|
|
FromRound = "round" // its turn in the week's round
|
|
FromAsk = "ask" // an ask: pacman_upgrade, or pacman_rollback with apply
|
|
FromLog = "log" // read from the package manager's log at the keeper's first start (ADR 0508 §1)
|
|
)
|
|
|
|
// The steps of a run, in the order they are written. A run that does not start ends at StepRefused; a
|
|
// transaction ends at StepFinished or StepFailed, and the after-upgrade check follows either.
|
|
const (
|
|
StepRecorded = "recorded" // the record is written and read back (upgrade only)
|
|
StepCheckedBefore = "checked-before" // the DKMS gate ran; Held names what it held
|
|
StepRefused = "refused" // the run did not start; Why says why
|
|
StepStarted = "started" // the transaction's unit started
|
|
StepFinished = "finished" // the transaction exited 0
|
|
StepFailed = "failed" // the transaction failed
|
|
StepCheckedAfter = "checked-after" // the after-upgrade check
|
|
)
|
|
|
|
// Run is `runs.<node>.<run>`: one upgrade or rollback of a node, written at each step and at no other time
|
|
// (ADR 0508 §1). Round names the round it ran in; Ask the ask that started it (the keeper deletes the ask once
|
|
// the run names it); Of, for a rollback, the run whose record it returns to.
|
|
type Run struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Kind string `json:"kind"`
|
|
From string `json:"from"`
|
|
Round string `json:"round,omitempty"`
|
|
Ask string `json:"ask,omitempty"`
|
|
Of string `json:"of,omitempty"`
|
|
Steps []Step `json:"steps"`
|
|
}
|
|
|
|
// Step is one step of a run with its time and outcome; only the fields of its kind are set.
|
|
type Step struct {
|
|
Step string `json:"step"`
|
|
At time.Time `json:"at"`
|
|
// StepRefused: why the run did not start.
|
|
Why string `json:"why,omitempty"`
|
|
// StepCheckedBefore: the kernels held from this run.
|
|
Held []HeldKernel `json:"held,omitempty"`
|
|
// StepStarted: the transaction's unit, the packages left out of it (held kernels and their headers), and
|
|
// for a rollback its plan.
|
|
Unit string `json:"unit,omitempty"`
|
|
Ignored []string `json:"ignored,omitempty"`
|
|
Plan []RollbackItem `json:"plan,omitempty"`
|
|
// StepFinished and StepFailed: the exit status, the last error line, and what the transaction changed,
|
|
// read from its lines in the package manager's log.
|
|
Exit *int `json:"exit,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Changes *Changes `json:"changes,omitempty"`
|
|
// StepCheckedAfter: the after-upgrade check.
|
|
Check *Check `json:"check,omitempty"`
|
|
}
|
|
|
|
// HeldKernel is a kernel package left out of a run because an installed DKMS module's own bound excludes its
|
|
// new version (ADR 0508 §3): `Linux-Maximum` in the module's META, or `BUILD_EXCLUSIVE_KERNEL` in its dkms.conf.
|
|
type HeldKernel struct {
|
|
Package string `json:"package"`
|
|
Version string `json:"version"`
|
|
Module string `json:"module"`
|
|
Bound string `json:"bound"`
|
|
}
|
|
|
|
// Changes is what a transaction changed, each list sorted by name.
|
|
type Changes struct {
|
|
Added []PackageVersion `json:"added"`
|
|
Removed []PackageVersion `json:"removed"`
|
|
Changed []VersionChange `json:"changed"`
|
|
}
|
|
|
|
// Check is the after-upgrade check (ADR 0508 §4). Passed when the transaction succeeded, no unit newly failed
|
|
// and every DKMS module is built for every installed kernel; the rest is said and does not stop the round.
|
|
type Check struct {
|
|
Passed bool `json:"passed"`
|
|
// The .pacnew and .pacsave files the transaction left, by path.
|
|
Pacnew []string `json:"pacnew"`
|
|
Pacsave []string `json:"pacsave"`
|
|
// The distribution's news since the node's previous upgrade.
|
|
News []NewsTitle `json:"news"`
|
|
// The units and containers whose processes still map a file the upgrade replaced.
|
|
Mapped []Mapped `json:"mapped"`
|
|
// The units failed now that were not failed in the record.
|
|
NewlyFailed []string `json:"newly_failed"`
|
|
// Whether a restart is needed: the running kernel is not the installed version of its package.
|
|
RestartNeeded bool `json:"restart_needed"`
|
|
RunningKernel string `json:"running_kernel"`
|
|
InstalledKernel string `json:"installed_kernel"`
|
|
// The DKMS modules not built for an installed kernel.
|
|
Unbuilt []DKMSBuild `json:"unbuilt"`
|
|
}
|
|
|
|
// NewsTitle is one item of the distribution's news, without its text.
|
|
type NewsTitle struct {
|
|
Title string `json:"title"`
|
|
Link string `json:"link"`
|
|
Published string `json:"published"`
|
|
}
|
|
|
|
// Mapped is a process still mapping a replaced file, named by its unit or its container and its executable's
|
|
// path; never its command line or environment.
|
|
type Mapped struct {
|
|
Unit string `json:"unit,omitempty"`
|
|
Container string `json:"container,omitempty"`
|
|
Executable string `json:"executable"`
|
|
}
|
|
|
|
// DKMSBuild is one DKMS module at one version for one kernel.
|
|
type DKMSBuild struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Kernel string `json:"kernel"`
|
|
}
|
|
|
|
// RollbackItem is one package a rollback brings back (ADR 0508 §7): from its version now to the record's, and
|
|
// where that version comes from.
|
|
type RollbackItem struct {
|
|
Name string `json:"name"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Source string `json:"source"`
|
|
Location string `json:"location"`
|
|
}
|
|
|
|
// Where a rollback takes a package's version from, in the order it is looked for.
|
|
const (
|
|
SourceCache = "cache" // the node's package cache: a file path
|
|
SourceArchive = "archive" // the Arch Linux Archive's copy of that exact version: a URL
|
|
SourceRepository = "repository" // the archive's repository as it stood on the record's date: a URL
|
|
)
|
|
|
|
// records
|
|
|
|
// Record is `records.<node>.<run>`: the node's packages taken before a run and read back (ADR 0508 §2): a copy of
|
|
// `installed.<node>`, the running kernel (/proc/sys/kernel/osrelease), the units failed at that moment, and the
|
|
// installed DKMS modules.
|
|
type Record struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
At time.Time `json:"at"`
|
|
Installed InstalledValue `json:"installed"`
|
|
Kernel string `json:"kernel"`
|
|
FailedUnits []string `json:"failed_units"`
|
|
DKMS []DKMSModule `json:"dkms"`
|
|
}
|
|
|
|
// DKMSModule is one installed DKMS module and the kernels it is built for.
|
|
type DKMSModule struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Kernels []string `json:"kernels"`
|
|
}
|
|
|
|
// rounds
|
|
|
|
// Round is `rounds.<round>`: the week's round, created once (a create, so one round per week) by the first node
|
|
// of the order when it opens it, with the order as it stood then (ADR 0508 §5).
|
|
type Round struct {
|
|
Round string `json:"round"`
|
|
Opened time.Time `json:"opened"`
|
|
OpenedBy string `json:"opened_by"`
|
|
Order []string `json:"order"`
|
|
}
|
|
|
|
// RoundNode is `rounds.<round>.<node>`: the node's place in the round. Run is the run it started in its turn,
|
|
// written by the node itself; Pass, the operator's pass of a node that stopped the round, written by the keeper
|
|
// of the node waiting on it (ADR 0508 §6). Both write through a compare-and-set.
|
|
type RoundNode struct {
|
|
Round string `json:"round"`
|
|
Node string `json:"node"`
|
|
Run string `json:"run,omitempty"`
|
|
Pass *Pass `json:"pass,omitempty"`
|
|
}
|
|
|
|
// Pass is a pass of a node asked of the operator through the operator's channel (ADR 0259): the reason, the
|
|
// channel's ask, and its answer once given (Answer empty while it waits).
|
|
type Pass struct {
|
|
Reason string `json:"reason"`
|
|
Ask string `json:"ask"`
|
|
Asked time.Time `json:"asked"`
|
|
Answer string `json:"answer,omitempty"`
|
|
Answered *time.Time `json:"answered,omitempty"`
|
|
}
|
|
|
|
// The operator's answers to a pass.
|
|
const (
|
|
PassApproved = "approved"
|
|
PassDeclined = "declined"
|
|
)
|
|
|
|
// asks
|
|
|
|
// The acts an ask asks.
|
|
const (
|
|
ActUpgrade = "upgrade" // pacman_upgrade
|
|
ActRollback = "rollback" // pacman_rollback with apply
|
|
ActPass = "pass" // pacman_round with pass
|
|
)
|
|
|
|
// Ask is `asks.<node>.<id>`: an act asked of a node, taken by that node's keeper alone, which answers in `runs`
|
|
// (an upgrade, a rollback) or in `rounds` (a pass) and then deletes the ask (ADR 0508 §8). A rollback names the
|
|
// run whose record it returns to and whether the packages the upgrade added are removed; a pass names the
|
|
// round, the node passed and the reason, and is asked of the node waiting on it.
|
|
type Ask struct {
|
|
ID string `json:"id"`
|
|
Node string `json:"node"`
|
|
Act string `json:"act"`
|
|
At time.Time `json:"at"`
|
|
Of string `json:"of,omitempty"`
|
|
RemoveAdded bool `json:"remove_added,omitempty"`
|
|
Round string `json:"round,omitempty"`
|
|
Pass string `json:"pass,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// events
|
|
|
|
// PackageChanged is `package.changed`: a write of `installed.<node>` that changed it (ADR 0507 §3).
|
|
type PackageChanged struct {
|
|
Node string `json:"node"`
|
|
At time.Time `json:"at"`
|
|
Added []PackageVersion `json:"added"`
|
|
Removed []PackageVersion `json:"removed"`
|
|
Changed []VersionChange `json:"changed"`
|
|
}
|
|
|
|
// UpgradeRecorded is `upgrade.recorded`: the run's record is written and read back.
|
|
type UpgradeRecorded struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Packages int `json:"packages"`
|
|
Kernel string `json:"kernel"`
|
|
}
|
|
|
|
// KernelHeld is `upgrade.kernel-held`: kernels left out of a run, each with the module and bound that hold it.
|
|
type KernelHeld struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Held []HeldKernel `json:"held"`
|
|
}
|
|
|
|
// UpgradeStarted is `upgrade.started`: a run's transaction started.
|
|
type UpgradeStarted struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
From string `json:"from"`
|
|
Round string `json:"round,omitempty"`
|
|
Unit string `json:"unit"`
|
|
Ignored []string `json:"ignored"`
|
|
}
|
|
|
|
// UpgradeFinished is `upgrade.finished`: a run's transaction exited 0, with what it changed.
|
|
type UpgradeFinished struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Changes Changes `json:"changes"`
|
|
}
|
|
|
|
// UpgradeFailed is `upgrade.failed`: a run that did not start (Step StepRefused, with why in Error) or whose
|
|
// transaction failed (Step StepFailed, with its exit status and last error line).
|
|
type UpgradeFailed struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Step string `json:"step"`
|
|
Exit *int `json:"exit,omitempty"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// UpgradeChecked is `upgrade.checked`: the after-upgrade check of a run, an upgrade's or a rollback's; the
|
|
// check whole is in the run.
|
|
type UpgradeChecked struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Kind string `json:"kind"`
|
|
Passed bool `json:"passed"`
|
|
}
|
|
|
|
// RollbackStarted is `rollback.started`: a rollback's transaction started, returning to the record of run Of.
|
|
type RollbackStarted struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Of string `json:"of"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// RollbackFinished is `rollback.finished`: a rollback's transaction ended, with its exit status and, when it
|
|
// failed, its last error line.
|
|
type RollbackFinished struct {
|
|
Node string `json:"node"`
|
|
Run string `json:"run"`
|
|
Of string `json:"of"`
|
|
Exit int `json:"exit"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// The keeper's config file
|
|
|
|
// The keeper's environment, named by the manifest's process resource.
|
|
const (
|
|
EnvBroker = "PACMAN_KEEPER_BROKER" // the keeper's bus account file
|
|
EnvConfig = "PACMAN_KEEPER_CONFIG" // the keeper's config file, a KeeperConfig
|
|
)
|
|
|
|
// KeeperConfig is the keeper's config file: the module's config values the mesh merges into it (ADR 0348 §5).
|
|
// The process reads it at its start; the mesh restarts the process when it changes (the manifest's
|
|
// `restart-on`).
|
|
type KeeperConfig struct {
|
|
// UpgradeOrder is the nodes in the order they upgrade; the first opens each week's round. Empty: no round.
|
|
UpgradeOrder []string `json:"upgrade-order"`
|
|
// RecordDays is how long a record is kept, in days.
|
|
RecordDays int `json:"record-days"`
|
|
// RoundStartMinutes is how long a node whose turn came may wait before starting, in minutes, before the
|
|
// round is said to stand still.
|
|
RoundStartMinutes int `json:"round-start-minutes"`
|
|
}
|