backup-manager¶
Owns the backup hierarchy: the Site → Environment → Module backup-policy cascade. It is a manager — it owns configuration state (backup policy resolved from site.json, environments/*.json, and per-module JSON) and orchestrates the pbs-controller for live PBS operations. It does not talk to PBS directly.
What it owns¶
The effective backup policy for any module, derived by merging three layers (most specific wins):
| Field | Source (in precedence order) |
|---|---|
retention | module .backup.retention → environment .backup.retention → site .backup.defaultRetention → 7y |
residency | environment .backup.residency → environment .dataResidency → eu-only |
enabled | module .backup.enabled (default true) |
exclude | module .backup.exclude (default []) |
schedule | environment .backup.schedule → null (inherit the site PBS job) |
target | site .backup.target |
offsite | site .backup.offsite |
The shared PBS backup job itself is driven by dependsOn: ["backup:vm"]. The manager owns the .backup writes on the deployed module JSON (modify / add / delete) and re-wires the config; install-module still calls resolve to record the resolved policy after a deploy (that path is unchanged — the manager does not duplicate it). The live PBS job membership converges on reconcile.
Verb / controller split (ADR-007 verb-alignment)¶
The manager resolves the cascade; the controller mutates PBS. backup-manager reconcile resolves the Site→Environment→Module policy for every deployed module and calls the controller's mutation verbs (add-to-job <vmid>, apply-schedule <spec>) to make PBS match. The manager never talks to PBS directly.
Implementation¶
The manager lives under src/ (built by default.nix; shared CLI/help/exec/config-io helpers come from lib/ts/src/ — mirrors site-manager). install.sh builds + links the backup-manager bin; the legacy bash entry scripts (backup-manager.sh, backup-status.sh, backup-restore.sh, validate-backup.sh, lib-cascade.sh) were retired in the ADR-007 post-implementation refactor, Phase 7.4. The backup-manager bin shells out to pbs-controller via CliClient (src/client.ts, parsing --json output) — no PBS API is reimplemented.
Commands (standardized verbs)¶
backup-manager validate [--config-dir DIR]
Backup hierarchy is well-formed + internally consistent (was validate-backup.sh).
backup-manager list [--disabled-only] [--json] [--config-dir DIR]
Effective policy for every deployed module (was backup-status).
OPTED-IN is the module's backup:vm declaration; IN-PBS-JOB is read
from the managed bucket jobs and names the bucket holding it. They
diverge — an archived module declares but has no guest, and a module
that declared before the backup server existed is not a member yet.
When PBS is unreachable the column falls back to the declaration and
is marked `?` (--json: membershipSource "declaration").
backup-manager show <module> [--json] [--config-dir DIR]
One module's effective policy (was backup-status <module>).
backup-manager resolve <module> [--environment <env>] [--config-dir DIR]
Cascade-resolve + print one module's policy (JSON).
backup-manager modify <module> [--enabled true|false] [--retention SPEC] [--exclude a,b]
Write the module's .backup {enabled,retention,exclude} onto
config/<module>.json (ATOMIC). Only the flags given are changed.
backup-manager add <module> | delete <module>
Wire / un-wire the module into the shared PBS job (adds/removes
"backup:vm" in .dependsOn). modify-driven .json writes; live membership
converges on reconcile.
backup-manager purge <module|vmid> [--yes]
Erase a removed guest's restore points (#789): delete the backup group
vm/<vmid> in this site's datastore (root namespace; buddy copies under
pull/<peer> are never touched) and start garbage collection, which is
what frees the space. Shows the plan first, then asks you to type the
VMID (or --yes). Refused for a deployed module (archived ones may be
purged), a VMID a backup job still lists, or when the cluster cannot be
asked. After `module-manager delete --remove` the config is gone: pass
the VMID. See RESTORE.md §11.
backup-manager backup <module>... [--config-dir DIR]
Take each module's backup NOW (#759): through the TAPPaaS job that
covers it — its storage, mode and compression — on the node the guest
runs on, VM or container; waits and names the snapshot (noted "on
demand"). Refuses a name with no vmid before anything runs, and a module
no TAPPaaS job covers (add it first). Non-zero if any backup was not
taken — an unreachable cluster included. Calls pbs-controller backup.
backup-manager key list
backup-manager key status [--json]
backup-manager key export --usb [--node N] [--format] | --file DIR
backup-manager key import --usb [--node N] | --file DIR
The backup encryption keys (ADR-012 §2.5.1). --usb: a USB stick in a
node (default tappaas1) — export erases the stick you confirm (a key stick
written before is not erased: only its keys, restore script and README are
replaced, everything else on it stays; --format erases it anyway), writes the
keys encrypted with a password you choose, reads them back, ejects; import
decrypts and imports, then offers the backup update that puts the storage
key back on the cluster. Runs the backup module's scripts/keys-to-usb.sh /
keys-from-usb.sh, interactively (ssh -t). --file: a directory, the keys
UNENCRYPTED (a bare directory still means --file). Keep two sticks.
Writing a stick (and reading one back with import --usb) records the
fingerprint of the keys it holds; `key status` says whether that is the
escrow as it is now (--json: escrowed, fingerprint, stick, current). The
sweep summary and health-manager's backup-keys gate repeat it until a
stick holds every key. `key stamp --dir DIR [--label TEXT]` records a
stick; the USB scripts call it. --file records nothing.
backup-manager reconcile [--apply] [--config-dir DIR]
Converge resolved policies → PBS (whole-cluster; PREVIEW by default,
--apply commits). Idempotent. Calls pbs-controller add-to-job /
apply-schedule.
backup-manager restore list <module> | restore <module> [opts] | list-all
SPECIAL recovery verb — delegates to the foundation backup/restore.sh and
pbs-controller (snapshot listing). restore <module> overwrites the
module's own guest after asking; --yes confirms it up front (a script or
an agent has no terminal). --from <module|vmid> restores another guest's
backup into it instead. See RESTORE.md §1–§2.
backup-manager restore list-all --json
Per guest and per target (this site's guests, each host's file backups,
another site's copy kept here): the last backup, whether it is within
its job's interval (ok) or not (late), the newest snapshot's
verification and the last verify with its outcome; with the backup,
verify and sync jobs and their last runs. Reads PBS through
pbs-controller runs; {"reachable": false} when it does not answer.
backup-manager coverage <module> [--json]
Every cluster backup job that covers the module's VM — TAPPaaS's own
bucket jobs and any other — and how (by vmid, --all, or pool). Two
enabled jobs is a warning: the VM is backed up twice (#554).
backup-manager placement [--json]
Where this site's PBS lives (placementState, pbsUrl, storage name).
backup-manager placement reset [--peer NAME] [--yes] [--no-update]
Leave an external PBS for a local one (ADR-012 §2.3, #607). Refused unless
placementState is external and a tankc pool exists. Runs, in order:
backup-manage.sh reset-external (the old storage entry → <name>_former,
still restorable; placementState → shim; formerExternal recorded), writes
the old PBS as pull peer former-<host>, update-module.sh backup (the shim
becomes a local PBS; the nodes push there), then onboards the pull
(prompts for a read login on the old PBS). src/placement-reset.ts.
backup-manager placement use-external <url> [--datastore D] [--namespace NS] [--fingerprint FP]
Back up to a PBS this site already runs — on the LAN, at a satellite, a
third party's — provisioning nothing (#456). Prompts for the credential
the remote issues, registers it as Proxmox storage, checks its existing
backups are listable, records placementState external + pbsUrl. Runs
the backup module's backup-manage.sh use-external. Every later update
fails, named, if that storage is gone or unusable.
backup-manager placement finish-reset [--yes]
Remove the <name>_former storage entry — after the pull and a test restore
from pull/<peer>. The old PBS is never touched.
The single entry point is the backup-manager bin (linked onto PATH by install.sh). Legacy-name → verb mapping (Phase 7.4 retirements):
| Retired script | Replacement |
|---|---|
backup-manager.sh | backup-manager resolve / list / restore |
backup-status.sh | backup-manager list [--json] [--disabled-only] |
backup-restore.sh | backup-manager restore list\|restore\|list-all |
validate-backup.sh | backup-manager validate (also via validate.sh, P10) |
lib-cascade.sh | src/config.ts resolvePolicy (the cascade source of truth) |
Controllers it calls¶
pbs-controller— PBS job status, snapshot listing, verify, namespaces (--jsonfor machine output), thereconcilemutationsadd-to-job/apply-schedule, andbackup(an on-demand backup through the module's job) — reusing the foundation PBS libs.- The foundation
backup/restore.shfor the actual VM restore.
Validation (validate verb)¶
backup-manager validate (src/validate.ts) checks the hierarchy is consistent and exits non-zero on any inconsistency: retention strings parse (^[0-9]+[dwmy]$), residency is a valid enum, an eu-only environment is not targeted at a non-EU offsite (site.backup.offsiteResidency), module backup.enabled:false is honoured, and there is no dangling target (enabled in-job modules require site.backup.target).
It also warns — never fails — about an off-site target not shown to be off-site (src/offsite.ts): a remote- or pull- peer that names no location, names one site.json does not have, names the primary location itself, or names one equal to the primary location at every level both record (country, city, address); and a satellite holding a copy whose location is missing or names a place in the same way. Both places are read through site-manager's locations (lib/ts/src/locations.ts); a peer or satellite the migrations have not reached still carries its own physicalLocation, which is read until then.
peers lists each peer with the location it names and that location's place (--json: location and place). peer add takes --location <name>, or --country [--city] [--building], which use the site's location at that place or add one (kind: remote) to site.json; a --force re-add keeps the peer's location unless another is given.
Testing¶
test.sh is fast + offline (fixtures, never the live config or PBS). It compiles the sources + unit tests, runs the unit suite under test/unit/ (a FakeClient for the controller boundary, fixtures under test/fixtures/config/): cascade resolution, validate, list/show, the reconcile plan (idempotent ensure-job-member + apply-schedule, and the controller-mutation apply path), the restore delegation, and the modify/add/delete atomic .backup writes. It then exercises the compiled CLI against temp fixtures: cascade resolution at each layer, the 7y → 5y → 1y override demo, enabled:false disabling, list output, and the validator's accept/reject cases.