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.
152 lines
7.7 KiB
Markdown
152 lines
7.7 KiB
Markdown
# 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:
|
|
|
|
1. [Adding a new backup kind](#adding-a-new-backup-kind)
|
|
2. [How the Backup badge decides good vs stale](#how-the-backup-badge-decides-good-vs-stale)
|
|
|
|
---
|
|
|
|
## 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:
|
|
|
|
```python
|
|
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 itself
|
|
- `docs/geenforce-api-cutover.md` - the fleet reporting path these fields ride on
|