475 lines
19 KiB
Go
475 lines
19 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"`
|
|
// Why the news could not be read, when it could not: no news is then not said to be none.
|
|
NewsError string `json:"news_error,omitempty"`
|
|
// The units and containers whose processes still map a file the upgrade replaced.
|
|
Mapped []Mapped `json:"mapped"`
|
|
// Why the mapped files could not be read, when they could not.
|
|
MappedError string `json:"mapped_error,omitempty"`
|
|
// 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"`
|
|
// Why the DKMS builds could not be read, when they could not; the check then does not pass.
|
|
DKMSError string `json:"dkms_error,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|