Guides was shipped without an entry in PLUGINS.md or START-HERE, so a site adopting the product had no way to find out it exists. BACKUP-KINDS.md described a host's badge as coming from the most recently confirmed kind. It has not worked that way since the fleet report was reworked: the freshest revision per (host, kind) is taken, and then the STALEST of those kinds decides, because reporting the freshest would let a healthy NTLARS backup mask a part-marker capture that stopped a month ago. Stalest among kinds that can be JUDGED - a kind never confirmed is unmeasurable, not stale, and treating it as the worst value made a host with a working backup show no badge at all. The backups row also still advertised UDC and CMM kinds that do not exist. api-inventory, openapi.json and PROJECT-MAP regenerated.
7.7 KiB
Fleet config backups: adding a kind, and how health is judged
This is about the backups plugin - the per-PC CONFIG backups the collector
takes off shopfloor machines (NTLARS settings, part-marker configs). It is not
about backing up the ShopDB database itself; that is docs/BACKUP-RESTORE.md.
Two things this covers:
Adding a new backup kind
A kind is one class in plugins/backups/services/registry.py, added to
REGISTRY at the bottom of that file:
REGISTRY = {k.key: k for k in (NtlarsKind(), PartMarkerKind())}
Subclass BackupKind and override what applies. NtlarsKind is the fullest
example (parseable, renderable, has an info panel); PartMarkerKind is the
lean one.
| Member | What it is |
|---|---|
key |
Wire value. This is what lands in backuprevisions.backupkind and what the fleet table shows on the badge. Keep it short and lowercase. |
displayname |
Human label for the UI. |
storagebackend |
'shopdb' (bytes live in the DB, deduped on a semantic hash) or 'share' (file lives on the SMB share, deduped on a raw byte hash). |
assettypes |
Which core asset types this kind can attach to; ['*'] for any. |
emptytext |
Text when an asset has no revisions of this kind. None HIDES the panel, which is the right default - a kind applies to an asset TYPE, but whether a given machine ever has that backup is per-machine. A part-marker panel on all 144 machines is noise. |
parse(raw) |
Opaque kinds return None; parseable kinds return a projection dict. |
formats() / render(...) |
Download formats, for shopdb kinds. |
resolveassetid(payload) |
Map a collector payload to the asset the backup belongs to. |
infopanel() / buildinfo(...) |
Optional at-a-glance card (ADR-010). Declared by the KIND, not hardcoded in the plugin, so a successor technology ships its own card by adding a class. |
sharedir(...) |
Conventional UNC directory for a share kind. Advisory - the authoritative path is whatever the collector reported, because the PC is what actually wrote the file. |
What you do NOT have to touch
The GE-Enforce Enforcement Reports Backup column needs no change. It reads
backuprevisions generically: newest revision per host, whatever the kind, and
shows backupkind on the badge. A new kind inherits the badge, the colour and
the tooltip with no work.
Same for the staleness rule below - it is time-based and kind-agnostic.
How the Backup badge decides good vs stale
The trap this is built around
A revision is only written when the config CHANGES. Dedup compares against
the latest revision for the chain (asset, kind, sourcehostname), so a machine
whose config has been stable for six months has a six-month-old newest revision
and is perfectly healthy. The question worth answering is not "when was the last
backup taken" - it is "is this still being checked".
So the timestamp the fleet table uses is backuprevisions.lastseenat: the last
time the collector CONFIRMED this config, whether or not anything changed. It
moves on every successful collection; the revision does not.
That was shown as a raw date at first, and it read as neglect. At the default
backups_intervalhours of 24, the collector only attempts once a day, so a
day-old confirmation IS the healthy steady state. The date made a working system
look like a stalled one, and made the reader do arithmetic against a setting
they would have to go and look up.
What it does now
GET /api/geenforce/reports returns, per host:
| Field | Meaning |
|---|---|
backupkind |
The kind the verdict is about - the STALEST measurable one, not the freshest. null = no backup at all. |
backuplastseen |
When it was last CONFIRMED still current (ISO). Tooltip only. |
backupcollectedat |
When the config was last CAPTURED (ISO). Only moves on a real change. Tooltip only. |
backupok |
true good, false stale, null nothing to judge. |
backupstaleafterdays |
The threshold in force, so the UI can explain itself. |
Both dates are reported because they answer different questions and one cannot stand for both. The hover reads "ntlars: checked 13 Aug 1:20 PM. Verified the backup taken 12 Aug 5:20 PM is still current." - the first date proves the check is running, the second says what is actually stored. Collapsing them was what made a healthy machine look neglected.
A host with several kinds is judged by its WORST one
A marker bay runs both an NTLARS backup and a part-marker backup. Reporting the
freshest of them would let a healthy kind mask a dead one: the part-marker
capture could stop for a month and the host would stay green on the strength of
its NTLARS row. So the row is reduced twice - the freshest revision per
(host, kind), and then the stalest of those kinds decides the verdict.
Stalest among kinds that can be judged. A kind whose newest revision has
never been confirmed - lastseenat NULL, which means a backfilled row or one
written before the column existed - is not stale, it is UNMEASURABLE, and it
must not win the comparison. Treating NULL as the worst value looked right and
was wrong: a host with a healthy NTLARS backup plus one never-confirmed row
showed NO BADGE at all, because the verdict for an unconfirmed kind is None.
A working backup rendering as nothing is the one outcome the badge must never
produce. When EVERY kind is unmeasurable the host still reports which kind it
has, with a null verdict and no badge.
This is the same rule staleness.py applies per chain: a chain becomes
measurable the first time its PC posts, and says nothing until then.
Known consequence: a device retired from a PC leaves its last revision behind, so that host stays stale until the revision is pruned. That is the honest failure direction - it complains about a backup that genuinely is not running any more, rather than going quiet about one.
backupok is deliberately tri-state. null means there is no revision for
that host, or the check is disabled - and it renders as no badge, never green.
"Never seen" must not read as healthy.
The threshold is the backups plugin's own backups_staledays setting (default
3), read through plugins.backups.services.staleness.staledays() rather
than re-derived, so there is ONE definition of stale. 0 disables the check.
The import is guarded, so a lean site build without the backups plugin returns
null instead of failing.
Why time-based rather than per-kind
Every kind answers the same question the same way: something confirmed this
recently, or it did not. Making the rule per-kind would mean each new kind has
to define health before it can show a badge, for no gain. If a kind ever needs
its own window - a weekly backup that should not be judged on a 3-day rule -
add a threshold override on BackupKind and have _backup_stale_cutoff prefer
it; the tri-state contract stays as it is.
Where the code lives
plugins/geenforce/api/routes.py-_attach_backup_state(newest revision per host),_backup_stale_cutoff(threshold),_backup_ok(verdict)plugins/geenforce/frontend/views/EnforcementReports.vue-backupClass(green / red / none),backupTitle(the hover text)plugins/backups/services/staleness.py- the shared threshold, also behind the dashboard's stale-backups card
Tests: tests/test_plugins/test_geenforce_reporting.py, the backup-verdict
block - recent-is-good, older-than-threshold-is-stale, and none-is-not-green.
See also
docs/BACKUP-RESTORE.md- backing up the ShopDB database itselfdocs/geenforce-api-cutover.md- the fleet reporting path these fields ride on