Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b0678fa05 | |||
| 6da3a7d866 |
@@ -0,0 +1,3 @@
|
|||||||
|
07:47:02 anonymous round trip
|
||||||
|
HTTP 200 648132539 bytes
|
||||||
|
34bd8f5c37ec8423d4c23d66f4069835837485b867119326455248952827f6f7
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Golden 0.295.0 — bake + publish + vouch, 2026-10-05 (afternoon)
|
||||||
|
|
||||||
|
Procedure: `documentation/runbooks/RUNBOOK-manual-build.md` §4.0 and §4.1 steps 1–5, in the drill VM on DooPlex.
|
||||||
|
|
||||||
|
| | Previous (`../golden-0.294.0-2026-10-05/`) | This bake |
|
||||||
|
|---|---|---|
|
||||||
|
| `build-golden.sh` | v3.2.0 (sha256 `645b3b659cba…`) | same file, unchanged (sha256 `645b3b659cba…`; VM copy matched) |
|
||||||
|
| Controller | `felhom-controller:0.294.0` | **`felhom-controller:0.295.0`** (MinAgent 0.131.0, unchanged) |
|
||||||
|
| Docker engine | the operator-approved set `os-docker-20261004-142842` | same pinned set (still in force) |
|
||||||
|
| Guest packages | template | template — `GOLDEN_GUEST_PKGS` EMPTY: no guest release is in force (the earliest real approval is after ~11:07 UTC today, `audits/night-2026-10-04/PREDICTION.md`) |
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
- Drill VM reverted to `virgin`, cold-booted per §4.0 (07:28:27 UTC); `pveversion` = `pve-manager/9.2.2`.
|
||||||
|
- `pveam update` → `update successful`; template `debian-13-standard_13.6-1_amd64.tar.zst` (the only `_amd64`
|
||||||
|
debian-13 entry), downloaded, `checksum verified`.
|
||||||
|
- `/root/bake-run.sh` in the VM reads the token from the file; launched as transient unit `golden-bake`.
|
||||||
|
- Token copied file → file (`scp`). `systemctl show golden-bake -p Environment -p ExecStart | grep -c -F <token>` =
|
||||||
|
**0** (control with the token appended = **1**).
|
||||||
|
|
||||||
|
## Pass markers (from `bake.log`)
|
||||||
|
|
||||||
|
```
|
||||||
|
[golden] Docker engine set PINNED to the approved release: containerd.io=2.3.6-1~debian.13~trixie … docker-ce=5:29.8.2-1~debian.13~trixie …
|
||||||
|
[golden] no approved guest release given - the template versions stay; first-night count vs an approved release: n/a
|
||||||
|
[golden] pending Debian upgrades in the baked guest (what a FUTURE approval may bring): 49
|
||||||
|
docker OK (overlay2; data-root /var/lib/docker)
|
||||||
|
live-restore: on
|
||||||
|
INFO: including mount point rootfs ('/') in backup
|
||||||
|
INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||||
|
[golden] pre-delete existing: HTTP 404 (404/204 expected)
|
||||||
|
[golden] upload OK (HTTP 201)
|
||||||
|
GOLDEN_VERSION=0.295.0
|
||||||
|
GOLDEN_SHA256=34bd8f5c37ec8423d4c23d66f4069835837485b867119326455248952827f6f7
|
||||||
|
```
|
||||||
|
|
||||||
|
No `excluding` and no `FATAL` in the log.
|
||||||
|
|
||||||
|
## Round trip
|
||||||
|
|
||||||
|
Anonymous GET of `…/generic/felhom-golden/0.295.0/golden.tar.zst`: HTTP 200, **648132539 bytes**, sha256
|
||||||
|
`34bd8f5c37ec8423d4c23d66f4069835837485b867119326455248952827f6f7` = the printed sha (`02-round-trip.txt`).
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Saved-log leak grep for the literal token: **0**; positive control (a throwaway copy with the token appended): **1**,
|
||||||
|
copy shredded.
|
||||||
|
|
||||||
|
## Vouch (step 5)
|
||||||
|
|
||||||
|
`POST /configuration/artifacts` (operator Basic auth): agent **0.145.0** (sha256 `894da35c…`), golden **0.295.0**
|
||||||
|
(sha256 `34bd8f5c…`), `min_agent` **0.131.0**, wrapper sha empty. Result and hub log line: `../../audits/catchup-2026-10-05/
|
||||||
|
partE/vouch.txt`. The controller floor stays per customer (demo-hp, demo-felhom, tester-1 at 0.295.0); the global floor
|
||||||
|
is unchanged (Tester 2 not moved).
|
||||||
|
|
||||||
|
## Teardown
|
||||||
|
|
||||||
|
`pct destroy 9100 --purge`; `shred -u` of the token, the runner script and the log in the VM (log copied off first);
|
||||||
|
`poweroff`; qemu gone (`ps -eo comm | grep -c qemu-system-x86` = 0); `qemu-img snapshot -a virgin`. Host: nothing
|
||||||
|
provisioned.
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
[golden] build-golden.sh v3.2.0 — baking controller gitea.dooplex.hu/admin/felhom-controller:0.295.0
|
||||||
|
[golden] creating build LXC 9100 (nesting=1,keyctl=1, unprivileged; rootfs 32G + ONE data volume 24G @ /var/lib/felhom, backup=1) …
|
||||||
|
Logical volume "vm-9100-disk-0" created.
|
||||||
|
Logical volume pve/vm-9100-disk-0 changed.
|
||||||
|
Creating filesystem with 8388608 4k blocks and 2097152 inodes
|
||||||
|
Filesystem UUID: 77b84d7b-4822-4462-998f-0e82e1eafa88
|
||||||
|
Superblock backups stored on blocks:
|
||||||
|
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
|
||||||
|
4096000, 7962624
|
||||||
|
Logical volume "vm-9100-disk-1" created.
|
||||||
|
Logical volume pve/vm-9100-disk-1 changed.
|
||||||
|
Creating filesystem with 6291456 4k blocks and 1572864 inodes
|
||||||
|
Filesystem UUID: c8d23fa2-f92c-4d6c-a235-34396bc69999
|
||||||
|
Superblock backups stored on blocks:
|
||||||
|
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
|
||||||
|
extracting archive '/var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst'
|
||||||
|
Total bytes read: 553512960 (528MiB, 77MiB/s)
|
||||||
|
Detected container architecture: amd64
|
||||||
|
Creating SSH host key 'ssh_host_ed25519_key' - this may take some time ...
|
||||||
|
done: SHA256:N/1fFvLeCS9JgSJrtXpUAqOPFmmuzXk40NRGFTnjbqU root@felhom-golden
|
||||||
|
Creating SSH host key 'ssh_host_rsa_key' - this may take some time ...
|
||||||
|
done: SHA256:tD+MhaLIvIpDvzR/Q2JHv7v3LAcuk/RMTzBZ54sDY1s root@felhom-golden
|
||||||
|
Creating SSH host key 'ssh_host_ecdsa_key' - this may take some time ...
|
||||||
|
done: SHA256:YY8fM8Gh9pl6FtLg/BoA5tXILyryRGPNHK/9Xl53MWg root@felhom-golden
|
||||||
|
[golden] starting + installing Docker (official repo, trixie channel) …
|
||||||
|
[golden] Docker engine set PINNED to the approved release: containerd.io=2.3.6-1~debian.13~trixie docker-buildx-plugin=0.37.1-1~debian.13~trixie docker-ce=5:29.8.2-1~debian.13~trixie docker-ce-cli=5:29.8.2-1~debian.13~trixie docker-ce-rootless-extras=5:29.8.2-1~debian.13~trixie docker-compose-plugin=5.6.0-1~debian.13~trixie
|
||||||
|
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
|
||||||
|
perl: warning: Setting locale failed.
|
||||||
|
perl: warning: Please check that your locale settings:
|
||||||
|
LANGUAGE = (unset),
|
||||||
|
LC_ALL = (unset),
|
||||||
|
LC_CTYPE = (unset),
|
||||||
|
LC_NUMERIC = (unset),
|
||||||
|
LC_COLLATE = (unset),
|
||||||
|
LC_TIME = (unset),
|
||||||
|
LC_MESSAGES = (unset),
|
||||||
|
LC_MONETARY = (unset),
|
||||||
|
LC_ADDRESS = (unset),
|
||||||
|
LC_IDENTIFICATION = (unset),
|
||||||
|
LC_MEASUREMENT = (unset),
|
||||||
|
LC_PAPER = (unset),
|
||||||
|
LC_TELEPHONE = (unset),
|
||||||
|
LC_NAME = (unset),
|
||||||
|
LANG = "en_US.UTF-8"
|
||||||
|
are supported and installed on your system.
|
||||||
|
perl: warning: Falling back to the standard locale ("C").
|
||||||
|
locale: Cannot set LC_CTYPE to default locale: No such file or directory
|
||||||
|
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
|
||||||
|
locale: Cannot set LC_ALL to default locale: No such file or directory
|
||||||
|
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
|
||||||
|
perl: warning: Setting locale failed.
|
||||||
|
perl: warning: Please check that your locale settings:
|
||||||
|
LANGUAGE = (unset),
|
||||||
|
LC_ALL = (unset),
|
||||||
|
LC_CTYPE = (unset),
|
||||||
|
LC_NUMERIC = (unset),
|
||||||
|
LC_COLLATE = (unset),
|
||||||
|
LC_TIME = (unset),
|
||||||
|
LC_MESSAGES = (unset),
|
||||||
|
LC_MONETARY = (unset),
|
||||||
|
LC_ADDRESS = (unset),
|
||||||
|
LC_IDENTIFICATION = (unset),
|
||||||
|
LC_MEASUREMENT = (unset),
|
||||||
|
LC_PAPER = (unset),
|
||||||
|
LC_TELEPHONE = (unset),
|
||||||
|
LC_NAME = (unset),
|
||||||
|
LANG = "en_US.UTF-8"
|
||||||
|
are supported and installed on your system.
|
||||||
|
perl: warning: Falling back to the standard locale ("C").
|
||||||
|
locale: Cannot set LC_CTYPE to default locale: No such file or directory
|
||||||
|
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
|
||||||
|
locale: Cannot set LC_ALL to default locale: No such file or directory
|
||||||
|
installed: containerd.io 2.3.6-1~debian.13~trixie
|
||||||
|
installed: docker-buildx-plugin 0.37.1-1~debian.13~trixie
|
||||||
|
installed: docker-ce 5:29.8.2-1~debian.13~trixie
|
||||||
|
installed: docker-ce-cli 5:29.8.2-1~debian.13~trixie
|
||||||
|
installed: docker-ce-rootless-extras 5:29.8.2-1~debian.13~trixie
|
||||||
|
installed: docker-compose-plugin 5.6.0-1~debian.13~trixie
|
||||||
|
[golden] no approved guest release given - the template versions stay; first-night count vs an approved release: n/a
|
||||||
|
[golden] pending Debian upgrades in the baked guest (what a FUTURE approval may bring): 49
|
||||||
|
[golden] baking daemon.json: classic overlay2 driver (containerd-snapshotter OFF) + log rotation …
|
||||||
|
[golden] wiring the single data volume (R-165 variant V-c): /var/lib/felhom/{docker,sys_drive} -> binds …
|
||||||
|
[golden] verifying Docker works in the build guest (storage driver should be overlay2 on the ext4 data volume) …
|
||||||
|
Unable to find image 'hello-world:latest' locally
|
||||||
|
latest: Pulling from library/hello-world
|
||||||
|
4f55086f7dd0: Pulling fs layer
|
||||||
|
4f55086f7dd0: Download complete
|
||||||
|
4f55086f7dd0: Pull complete
|
||||||
|
Digest: sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8
|
||||||
|
Status: Downloaded newer image for hello-world:latest
|
||||||
|
docker OK (overlay2; data-root /var/lib/docker)
|
||||||
|
live-restore: on
|
||||||
|
/var/lib/docker is a real mount: /dev/mapper/pve-vm--9100--disk--1[/docker] ext4
|
||||||
|
/mnt/sys_drive is a real mount: /dev/mapper/pve-vm--9100--disk--1[/sys_drive] ext4
|
||||||
|
both paths are ONE filesystem: /dev/mapper/pve-vm--9100--disk--1 23317576
|
||||||
|
[golden] baking the in-guest controller image gitea.dooplex.hu/admin/felhom-controller:0.295.0 (no registry cred at deploy) …
|
||||||
|
|
||||||
|
WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
|
||||||
|
Configure a credential helper to remove this warning. See
|
||||||
|
https://docs.docker.com/go/credential-store/
|
||||||
|
|
||||||
|
0.295.0: Pulling from admin/felhom-controller
|
||||||
|
774043ccc8cc: Pulling fs layer
|
||||||
|
ab6b448d4be9: Pulling fs layer
|
||||||
|
23a5bfa58353: Pulling fs layer
|
||||||
|
862a57157567: Pulling fs layer
|
||||||
|
ec5d49fd2de1: Pulling fs layer
|
||||||
|
049d42a206a8: Pulling fs layer
|
||||||
|
862a57157567: Waiting
|
||||||
|
ec5d49fd2de1: Waiting
|
||||||
|
049d42a206a8: Waiting
|
||||||
|
23a5bfa58353: Verifying Checksum
|
||||||
|
23a5bfa58353: Download complete
|
||||||
|
862a57157567: Verifying Checksum
|
||||||
|
862a57157567: Download complete
|
||||||
|
774043ccc8cc: Verifying Checksum
|
||||||
|
774043ccc8cc: Download complete
|
||||||
|
ec5d49fd2de1: Verifying Checksum
|
||||||
|
ec5d49fd2de1: Download complete
|
||||||
|
049d42a206a8: Verifying Checksum
|
||||||
|
049d42a206a8: Download complete
|
||||||
|
ab6b448d4be9: Verifying Checksum
|
||||||
|
ab6b448d4be9: Download complete
|
||||||
|
774043ccc8cc: Pull complete
|
||||||
|
ab6b448d4be9: Pull complete
|
||||||
|
23a5bfa58353: Pull complete
|
||||||
|
862a57157567: Pull complete
|
||||||
|
ec5d49fd2de1: Pull complete
|
||||||
|
049d42a206a8: Pull complete
|
||||||
|
Digest: sha256:43b75dcb8f618affb19dab5c1e2419ca824c050447ec3f9ebfa44a434354ade9
|
||||||
|
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-controller:0.295.0
|
||||||
|
gitea.dooplex.hu/admin/felhom-controller:0.295.0
|
||||||
|
[golden] asking the controller which infra images it manages …
|
||||||
|
[golden] baking infra images (4): traefik:v3.7.13 cloudflare/cloudflared:2026.9.3 gtstef/filebrowser:1.5.6-stable gitea.dooplex.hu/admin/felhom-samba:1.1.0 …
|
||||||
|
v3.7.13: Pulling from library/traefik
|
||||||
|
e2de96513ba9: Pulling fs layer
|
||||||
|
b686a4f73445: Pulling fs layer
|
||||||
|
78cb21c375ca: Pulling fs layer
|
||||||
|
acb2f33459b1: Pulling fs layer
|
||||||
|
acb2f33459b1: Waiting
|
||||||
|
e2de96513ba9: Verifying Checksum
|
||||||
|
e2de96513ba9: Download complete
|
||||||
|
b686a4f73445: Verifying Checksum
|
||||||
|
b686a4f73445: Download complete
|
||||||
|
acb2f33459b1: Verifying Checksum
|
||||||
|
acb2f33459b1: Download complete
|
||||||
|
e2de96513ba9: Pull complete
|
||||||
|
78cb21c375ca: Verifying Checksum
|
||||||
|
78cb21c375ca: Download complete
|
||||||
|
b686a4f73445: Pull complete
|
||||||
|
78cb21c375ca: Pull complete
|
||||||
|
acb2f33459b1: Pull complete
|
||||||
|
Digest: sha256:24841fe2de7304c149343d877d2923b4c8800a38ba015dea9174c23b20e344a0
|
||||||
|
Status: Downloaded newer image for traefik:v3.7.13
|
||||||
|
docker.io/library/traefik:v3.7.13
|
||||||
|
2026.9.3: Pulling from cloudflare/cloudflared
|
||||||
|
2cc7ee286bf3: Pulling fs layer
|
||||||
|
c172f21841df: Pulling fs layer
|
||||||
|
218cf840d0d9: Pulling fs layer
|
||||||
|
f6069939f718: Pulling fs layer
|
||||||
|
d6b1b89eccac: Pulling fs layer
|
||||||
|
2780920e5dbf: Pulling fs layer
|
||||||
|
7c12895b777b: Pulling fs layer
|
||||||
|
3214acf345c0: Pulling fs layer
|
||||||
|
52630fc75a18: Pulling fs layer
|
||||||
|
dd64bf2dd177: Pulling fs layer
|
||||||
|
b839dfae01f6: Pulling fs layer
|
||||||
|
ebddc55facdc: Pulling fs layer
|
||||||
|
c4bc6f35ff5e: Pulling fs layer
|
||||||
|
b96fe2995f90: Pulling fs layer
|
||||||
|
58c0c263dc73: Pulling fs layer
|
||||||
|
bd8962e29291: Pulling fs layer
|
||||||
|
cac2ae0193cb: Pulling fs layer
|
||||||
|
f0383d5ebc47: Pulling fs layer
|
||||||
|
d6b1b89eccac: Waiting
|
||||||
|
2780920e5dbf: Waiting
|
||||||
|
7c12895b777b: Waiting
|
||||||
|
3214acf345c0: Waiting
|
||||||
|
52630fc75a18: Waiting
|
||||||
|
dd64bf2dd177: Waiting
|
||||||
|
b839dfae01f6: Waiting
|
||||||
|
ebddc55facdc: Waiting
|
||||||
|
c4bc6f35ff5e: Waiting
|
||||||
|
b96fe2995f90: Waiting
|
||||||
|
58c0c263dc73: Waiting
|
||||||
|
bd8962e29291: Waiting
|
||||||
|
cac2ae0193cb: Waiting
|
||||||
|
f0383d5ebc47: Waiting
|
||||||
|
f6069939f718: Waiting
|
||||||
|
2cc7ee286bf3: Download complete
|
||||||
|
218cf840d0d9: Verifying Checksum
|
||||||
|
218cf840d0d9: Download complete
|
||||||
|
c172f21841df: Verifying Checksum
|
||||||
|
c172f21841df: Download complete
|
||||||
|
f6069939f718: Verifying Checksum
|
||||||
|
f6069939f718: Download complete
|
||||||
|
d6b1b89eccac: Verifying Checksum
|
||||||
|
d6b1b89eccac: Download complete
|
||||||
|
2780920e5dbf: Verifying Checksum
|
||||||
|
2780920e5dbf: Download complete
|
||||||
|
2cc7ee286bf3: Pull complete
|
||||||
|
7c12895b777b: Verifying Checksum
|
||||||
|
7c12895b777b: Download complete
|
||||||
|
3214acf345c0: Verifying Checksum
|
||||||
|
3214acf345c0: Download complete
|
||||||
|
52630fc75a18: Verifying Checksum
|
||||||
|
52630fc75a18: Download complete
|
||||||
|
dd64bf2dd177: Verifying Checksum
|
||||||
|
dd64bf2dd177: Download complete
|
||||||
|
b839dfae01f6: Verifying Checksum
|
||||||
|
b839dfae01f6: Download complete
|
||||||
|
c172f21841df: Pull complete
|
||||||
|
ebddc55facdc: Verifying Checksum
|
||||||
|
ebddc55facdc: Download complete
|
||||||
|
c4bc6f35ff5e: Verifying Checksum
|
||||||
|
c4bc6f35ff5e: Download complete
|
||||||
|
b96fe2995f90: Verifying Checksum
|
||||||
|
b96fe2995f90: Download complete
|
||||||
|
bd8962e29291: Verifying Checksum
|
||||||
|
bd8962e29291: Download complete
|
||||||
|
58c0c263dc73: Verifying Checksum
|
||||||
|
58c0c263dc73: Download complete
|
||||||
|
cac2ae0193cb: Verifying Checksum
|
||||||
|
cac2ae0193cb: Download complete
|
||||||
|
218cf840d0d9: Pull complete
|
||||||
|
f0383d5ebc47: Verifying Checksum
|
||||||
|
f0383d5ebc47: Download complete
|
||||||
|
f6069939f718: Pull complete
|
||||||
|
d6b1b89eccac: Pull complete
|
||||||
|
2780920e5dbf: Pull complete
|
||||||
|
7c12895b777b: Pull complete
|
||||||
|
3214acf345c0: Pull complete
|
||||||
|
52630fc75a18: Pull complete
|
||||||
|
dd64bf2dd177: Pull complete
|
||||||
|
b839dfae01f6: Pull complete
|
||||||
|
ebddc55facdc: Pull complete
|
||||||
|
c4bc6f35ff5e: Pull complete
|
||||||
|
b96fe2995f90: Pull complete
|
||||||
|
58c0c263dc73: Pull complete
|
||||||
|
bd8962e29291: Pull complete
|
||||||
|
cac2ae0193cb: Pull complete
|
||||||
|
f0383d5ebc47: Pull complete
|
||||||
|
Digest: sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c
|
||||||
|
Status: Downloaded newer image for cloudflare/cloudflared:2026.9.3
|
||||||
|
docker.io/cloudflare/cloudflared:2026.9.3
|
||||||
|
1.5.6-stable: Pulling from gtstef/filebrowser
|
||||||
|
55afa1ecc21d: Pulling fs layer
|
||||||
|
8ed8f35f8d4f: Pulling fs layer
|
||||||
|
989b226a579c: Pulling fs layer
|
||||||
|
660aeead31d5: Pulling fs layer
|
||||||
|
4f4fb700ef54: Pulling fs layer
|
||||||
|
adce24567e4c: Pulling fs layer
|
||||||
|
f17ea56b313b: Pulling fs layer
|
||||||
|
6b6f3b3efe88: Pulling fs layer
|
||||||
|
4ed1ca4f3fce: Pulling fs layer
|
||||||
|
e6fc9c6a5757: Pulling fs layer
|
||||||
|
d47782d1182a: Pulling fs layer
|
||||||
|
660aeead31d5: Waiting
|
||||||
|
4ed1ca4f3fce: Waiting
|
||||||
|
e6fc9c6a5757: Waiting
|
||||||
|
d47782d1182a: Waiting
|
||||||
|
4f4fb700ef54: Waiting
|
||||||
|
adce24567e4c: Waiting
|
||||||
|
f17ea56b313b: Waiting
|
||||||
|
6b6f3b3efe88: Waiting
|
||||||
|
55afa1ecc21d: Verifying Checksum
|
||||||
|
55afa1ecc21d: Download complete
|
||||||
|
660aeead31d5: Verifying Checksum
|
||||||
|
660aeead31d5: Download complete
|
||||||
|
989b226a579c: Verifying Checksum
|
||||||
|
989b226a579c: Download complete
|
||||||
|
4f4fb700ef54: Verifying Checksum
|
||||||
|
4f4fb700ef54: Download complete
|
||||||
|
55afa1ecc21d: Pull complete
|
||||||
|
adce24567e4c: Verifying Checksum
|
||||||
|
adce24567e4c: Download complete
|
||||||
|
f17ea56b313b: Verifying Checksum
|
||||||
|
f17ea56b313b: Download complete
|
||||||
|
4ed1ca4f3fce: Verifying Checksum
|
||||||
|
4ed1ca4f3fce: Download complete
|
||||||
|
6b6f3b3efe88: Verifying Checksum
|
||||||
|
6b6f3b3efe88: Download complete
|
||||||
|
8ed8f35f8d4f: Verifying Checksum
|
||||||
|
8ed8f35f8d4f: Download complete
|
||||||
|
d47782d1182a: Verifying Checksum
|
||||||
|
d47782d1182a: Download complete
|
||||||
|
e6fc9c6a5757: Verifying Checksum
|
||||||
|
e6fc9c6a5757: Download complete
|
||||||
|
8ed8f35f8d4f: Pull complete
|
||||||
|
989b226a579c: Pull complete
|
||||||
|
660aeead31d5: Pull complete
|
||||||
|
4f4fb700ef54: Pull complete
|
||||||
|
adce24567e4c: Pull complete
|
||||||
|
f17ea56b313b: Pull complete
|
||||||
|
6b6f3b3efe88: Pull complete
|
||||||
|
4ed1ca4f3fce: Pull complete
|
||||||
|
e6fc9c6a5757: Pull complete
|
||||||
|
d47782d1182a: Pull complete
|
||||||
|
Digest: sha256:7c5d7ac8ffda31294d278063cf9d2e04303b39e6dce1f4c691342240ca7703b8
|
||||||
|
Status: Downloaded newer image for gtstef/filebrowser:1.5.6-stable
|
||||||
|
docker.io/gtstef/filebrowser:1.5.6-stable
|
||||||
|
1.1.0: Pulling from admin/felhom-samba
|
||||||
|
897d797d2723: Pulling fs layer
|
||||||
|
3051591aa250: Pulling fs layer
|
||||||
|
ce57a3f93416: Pulling fs layer
|
||||||
|
fb94eeec2fe1: Pulling fs layer
|
||||||
|
fb94eeec2fe1: Waiting
|
||||||
|
ce57a3f93416: Verifying Checksum
|
||||||
|
ce57a3f93416: Download complete
|
||||||
|
fb94eeec2fe1: Verifying Checksum
|
||||||
|
fb94eeec2fe1: Download complete
|
||||||
|
897d797d2723: Verifying Checksum
|
||||||
|
897d797d2723: Download complete
|
||||||
|
3051591aa250: Verifying Checksum
|
||||||
|
3051591aa250: Download complete
|
||||||
|
897d797d2723: Pull complete
|
||||||
|
3051591aa250: Pull complete
|
||||||
|
ce57a3f93416: Pull complete
|
||||||
|
fb94eeec2fe1: Pull complete
|
||||||
|
Digest: sha256:1c17c09422bec0366d7cf0e0fcfc1486ba6c90334a0a5d5c851073a9342f8f10
|
||||||
|
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-samba:1.1.0
|
||||||
|
gitea.dooplex.hu/admin/felhom-samba:1.1.0
|
||||||
|
[golden] baking the controller-bootstrap unit (deploys the BAKED controller from the config mount) …
|
||||||
|
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.service' → '/etc/systemd/system/felhom-controller-bootstrap.service'.
|
||||||
|
[golden] baking the controller-bootstrap PATH unit (starts the service on bootstrap-mount hot-plug — B1) …
|
||||||
|
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.path' → '/etc/systemd/system/felhom-controller-bootstrap.path'.
|
||||||
|
[golden] baking the first-boot SSH host-key regeneration unit (F3) …
|
||||||
|
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-regen-hostkeys.service' → '/etc/systemd/system/felhom-regen-hostkeys.service'.
|
||||||
|
[golden] identity-clean + minimize …
|
||||||
|
[golden] stop + archive …
|
||||||
|
INFO: including mount point rootfs ('/') in backup
|
||||||
|
INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||||
|
INFO: archive file size: 618MB
|
||||||
|
INFO: Finished Backup of VM 9100 (00:00:33)
|
||||||
|
[golden] DONE. golden archive volid: local:backup/vzdump-lxc-9100-2026_10_05-09_33_33.tar.zst (rootfs 32G + ONE data volume 24G @ /var/lib/felhom, all in the archive)
|
||||||
|
[golden] publishing golden (648132539 bytes, sha256 34bd8f5c37ec8423…) → https://gitea.dooplex.hu/api/packages/admin/generic/felhom-golden/0.295.0/golden.tar.zst
|
||||||
|
[golden] pre-delete existing: HTTP 404 (404/204 expected)
|
||||||
|
[golden] upload OK (HTTP 201)
|
||||||
|
GOLDEN_VERSION=0.295.0
|
||||||
|
GOLDEN_SHA256=34bd8f5c37ec8423d4c23d66f4069835837485b867119326455248952827f6f7
|
||||||
|
[golden] Record in the hub operator UI (Configs → Day-0 artifacts): golden 0.295.0 / 34bd8f5c37ec8423d4c23d66f4069835837485b867119326455248952827f6f7
|
||||||
|
[golden] (the build guest 9100 is stopped; destroy it with: pct destroy 9100 --purge)
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
## v0.134.0 — a box that is off at night: the missed-backup alarm judges a down box; the household hears an outage at most weekly; the catch-up line is allowed (R-872, R-873, R-871) (2026-10-05)
|
||||||
|
|
||||||
|
**Controller v0.295.0** sends `backup_catchup_done`; an older controller never does.
|
||||||
|
|
||||||
|
- **R-872 (`08` §6.4).** The 05:00 deadline check no longer skips a customer whose node is `down`. A box down at
|
||||||
|
EVERY deadline (a laptop switched off at night — measured 2026-10-05: `1 skipped (down)`) was never judged. A down
|
||||||
|
box is now judged on longer lines: `expected_dbdump_missed` after 48 h without `db_dump_completed`,
|
||||||
|
`expected_backup_missed` after 72 h without a whole-guest backup in any retained host report, and nothing for a box
|
||||||
|
first seen less than 48 h ago. A box that died last night still raises only its staleness alarm. `disabled` boxes are
|
||||||
|
still skipped (R-321). `monitor/r872_down_box_test.go`, 3 red-proofs.
|
||||||
|
- **R-873 (`08` §6.4).** "Your server cannot be reached" (`node_stale`, `node_down`, `host_stale`, `host_down`) reaches the
|
||||||
|
HOUSEHOLD at most once per 7 days, read from the persisted notification log; the operator still gets every edge; the
|
||||||
|
recovery mail stays paired (a held-back down mail also holds back its recovery). `notify/r873_liveness_weekly_test.go`.
|
||||||
|
- **R-871.** `backup_catchup_done` is an allowed event type (info: recorded, never mailed).
|
||||||
|
- CC decisions 115–116 (`09` §3), operator may reverse.
|
||||||
|
|
||||||
## v0.133.0 — test approvals end with the test (R-859); the root-file bundle on the System page, in the install manifest, and its alarm (R-840) (2026-10-04)
|
## v0.133.0 — test approvals end with the test (R-859); the root-file bundle on the System page, in the install manifest, and its alarm (R-840) (2026-10-04)
|
||||||
|
|
||||||
**Agent v0.143.0** reports the bundle; an older agent shows `unknown` in the new column (never a guess).
|
**Agent v0.143.0** reports the bundle; an older agent shows `unknown` in the new column (never a guess).
|
||||||
|
|||||||
@@ -2050,6 +2050,10 @@ var allowedEventTypes = map[string]bool{
|
|||||||
// of memory. Household + operator; hu AND en `mail.event.*`; per-app cooldown on both legs.
|
// of memory. Household + operator; hu AND en `mail.event.*`; per-app cooldown on both legs.
|
||||||
"app_stopped_unhealthy": true,
|
"app_stopped_unhealthy": true,
|
||||||
|
|
||||||
|
// R-871 (v0.134.0, controller v0.295.0, `09` decision 109): the box was off at its backup time and made the
|
||||||
|
// missed backups when it came back. Info — recorded on the box's timeline, never mailed.
|
||||||
|
"backup_catchup_done": true,
|
||||||
|
|
||||||
// Controller-pushed events
|
// Controller-pushed events
|
||||||
"controller_started": true,
|
"controller_started": true,
|
||||||
"claim_lockout": true, // v0.50.0 — claim/reset code brute-force lockout tripped
|
"claim_lockout": true, // v0.50.0 — claim/reset code brute-force lockout tripped
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// R-871 (v0.134.0): the box's "missed backup made now" line (controller v0.295.0, event backup_catchup_done) must be
|
||||||
|
// an allowed type, or POST /event answers 400 and the household's timeline line vanishes (hub-event-allowlist trap).
|
||||||
|
// COMPANION RED-PROOF: remove "backup_catchup_done" from allowedEventTypes → this fails.
|
||||||
|
func TestR871_CatchUpEventIsAllowed(t *testing.T) {
|
||||||
|
if !allowedEventTypes["backup_catchup_done"] {
|
||||||
|
t.Fatal("backup_catchup_done is not an allowed event type — the box's catch-up line would be dropped with a 400")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -334,6 +334,68 @@ func stalenessState(staleness *StalenessChecker, customerID string) string {
|
|||||||
return staleness.GetState(customerID)
|
return staleness.GetState(customerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downDumpMissedAfter / downBackupMissedAfter (R-872): the lines a DOWN box is judged on. 48 h = two nights without
|
||||||
|
// a database dump (a box that died last night is the staleness alarm's alone). 72 h for the whole-guest backup = the
|
||||||
|
// agent's own catch-up valve (cadence 24 h + 24 h) plus a day. Decided by CC — operator may reverse.
|
||||||
|
const (
|
||||||
|
downDumpMissedAfter = 48 * time.Hour
|
||||||
|
downBackupMissedAfter = 72 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// judgeDownCustomer is R-872's judgement of a box that is down at the deadline. It raises at most one
|
||||||
|
// expected_dbdump_missed and one expected_backup_missed, each only past its longer line, and never for a box that
|
||||||
|
// was bound or first reported less than downDumpMissedAfter ago (nothing has been expected yet).
|
||||||
|
func judgeDownCustomer(s *store.Store, id string, now time.Time, onEvent EventNotifyFunc, logger *log.Logger) (backupMissed, dbdumpMissed int) {
|
||||||
|
if bound, err := s.HasEverBoundHost(id); err == nil && !bound {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
first, ferr := s.GetFirstHostReportAt(id)
|
||||||
|
if ferr != nil || first.IsZero() || now.Sub(first) < downDumpMissedAfter {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
raise := func(typ, msg string) {
|
||||||
|
if _, err := s.SaveEvent(id, typ, "error", msg, "{}", "hub"); err != nil {
|
||||||
|
logger.Printf("[WARN] Failed to save %s for %s: %v", typ, id, err)
|
||||||
|
} else if onEvent != nil {
|
||||||
|
onEvent(id, typ, "error", msg, "{}", "hub")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Database dumps: the newest db_dump_completed in the last 7 days.
|
||||||
|
if dumps, err := s.GetEventsByType(id, "db_dump_completed", now.Add(-backupEvidenceLookback)); err == nil {
|
||||||
|
var newest time.Time
|
||||||
|
for _, e := range dumps {
|
||||||
|
if e.CreatedAt.After(newest) {
|
||||||
|
newest = e.CreatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newest.IsZero() || now.Sub(newest) > downDumpMissedAfter {
|
||||||
|
last := "none in the last 7 days"
|
||||||
|
if !newest.IsZero() {
|
||||||
|
last = newest.UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
raise("expected_dbdump_missed", fmt.Sprintf("No DB dump for over %s while the box is down at its deadline (last: %s) — it may be switched off at its backup time (R-872)",
|
||||||
|
downDumpMissedAfter, last))
|
||||||
|
dbdumpMissed = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Whole-guest backup: the newest backup any retained host report shows.
|
||||||
|
if rows, err := s.GetHostReportsSince(id, now.Add(-backupEvidenceLookback)); err == nil {
|
||||||
|
newest, have := newestBackupEvidence(rows, now)
|
||||||
|
if !have || now.Sub(newest) > downBackupMissedAfter {
|
||||||
|
last := "none in the last 7 days"
|
||||||
|
if have {
|
||||||
|
last = newest.UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
raise("expected_backup_missed", fmt.Sprintf("No fresh verified backup for over %s while the box is down at its deadline (last: %s) (R-872)",
|
||||||
|
downBackupMissedAfter, last))
|
||||||
|
backupMissed = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Printf("[INFO] Deadline check: %s is DOWN — judged on the longer lines (dump %s, whole-guest %s): dump missed=%d backup missed=%d",
|
||||||
|
id, downDumpMissedAfter, downBackupMissedAfter, dbdumpMissed, backupMissed)
|
||||||
|
return backupMissed, dbdumpMissed
|
||||||
|
}
|
||||||
|
|
||||||
func CheckBackupDeadlines(s *store.Store, staleness *StalenessChecker, onEvent EventNotifyFunc, logger *log.Logger) {
|
func CheckBackupDeadlines(s *store.Store, staleness *StalenessChecker, onEvent EventNotifyFunc, logger *log.Logger) {
|
||||||
customerIDs, err := s.GetActiveCustomerIDs()
|
customerIDs, err := s.GetActiveCustomerIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -357,7 +419,24 @@ func CheckBackupDeadlines(s *store.Store, staleness *StalenessChecker, onEvent E
|
|||||||
// `expected_backup_missed` / `expected_dbdump_missed` every morning about a machine we asked
|
// `expected_backup_missed` / `expected_dbdump_missed` every morning about a machine we asked
|
||||||
// to be quiet. That is R-195's shape exactly — a skip keyed off the wrong fact missing the
|
// to be quiet. That is R-195's shape exactly — a skip keyed off the wrong fact missing the
|
||||||
// customer it would most obviously cover — which is why both doors are closed together.
|
// customer it would most obviously cover — which is why both doors are closed together.
|
||||||
if st := stalenessState(staleness, id); st == "down" || st == StateDisabled {
|
st := stalenessState(staleness, id)
|
||||||
|
if st == StateDisabled {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// R-872 (v0.134.0): a box that is DOWN at 05:00 is no longer skipped outright. It was, so that a box that
|
||||||
|
// just died raises its staleness alarm and not a backup alarm too — but a box that is down at EVERY
|
||||||
|
// deadline (a laptop switched off at night, Tester 2, measured 2026-10-05: "1 skipped (down)") was then
|
||||||
|
// never judged at all, and its missing backups stayed silent for ever. A down box is now judged on a
|
||||||
|
// LONGER line (downDumpMissedAfter / downBackupMissedAfter): a box that died last night still raises only
|
||||||
|
// its staleness alarm; a box that has gone two nights without a dump raises the backup alarm, down or not.
|
||||||
|
// Pinned by TestR872_*.
|
||||||
|
if st == "down" {
|
||||||
|
if !s.IsCustomerBlocked(id) {
|
||||||
|
b, d := judgeDownCustomer(s, id, time.Now().UTC(), onEvent, logger)
|
||||||
|
backupMissed += b
|
||||||
|
dbdumpMissed += d
|
||||||
|
}
|
||||||
skipped++
|
skipped++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-872 (v0.134.0) — a box DOWN at the 05:00 deadline is judged on longer lines instead of being skipped.
|
||||||
|
// THE MEASURED SHAPE (Tester 2, 2026-10-05 05:00 Budapest): "Deadline check: … 0 backup missed … 1 skipped (down)" —
|
||||||
|
// a laptop off at every deadline, no dump ever, no alarm ever.
|
||||||
|
// COMPANION RED-PROOF: restore `if st == "down" || st == StateDisabled { skipped++; continue }` → the Tester 2
|
||||||
|
// shape raises nothing.
|
||||||
|
|
||||||
|
func downBox(t *testing.T, firstReportAge time.Duration) (*store.Store, string, *StalenessChecker) {
|
||||||
|
t.Helper()
|
||||||
|
st, path := seedStalenessCustomer(t, "ok", 3*time.Hour) // the controller report is 3 h old → down
|
||||||
|
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := st.SaveHostReport("h1", "c1", []byte(`{"pbs_snapshots":[],"backups":[]}`), store.HostReportDenorm{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r872Backdate(t, path, "UPDATE host_reports SET received_at = ?", time.Now().UTC().Add(-firstReportAge))
|
||||||
|
sc, _ := newChecker(t, st)
|
||||||
|
sc.Check()
|
||||||
|
if sc.GetState("c1") != "down" {
|
||||||
|
t.Fatalf("setup: state %q, want down", sc.GetState("c1"))
|
||||||
|
}
|
||||||
|
return st, path, sc
|
||||||
|
}
|
||||||
|
|
||||||
|
func r872Backdate(t *testing.T, path, q string, at time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if _, err := db.Exec(q, at.Format("2006-01-02 15:04:05")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deadlineEvents(st *store.Store, sc *StalenessChecker) map[string]int {
|
||||||
|
got := map[string]int{}
|
||||||
|
CheckBackupDeadlines(st, sc, func(cid, et, sev, msg, det, src string) { got[et]++ }, log.New(io.Discard, "", 0))
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tester 2's shape: bound 5 days ago, down at every deadline, no dump, no backup → both alarms.
|
||||||
|
func TestR872_DownEveryNightRaisesTheMissedAlarms(t *testing.T) {
|
||||||
|
st, _, sc := downBox(t, 5*24*time.Hour)
|
||||||
|
got := deadlineEvents(st, sc)
|
||||||
|
if got["expected_dbdump_missed"] != 1 || got["expected_backup_missed"] != 1 {
|
||||||
|
t.Fatalf("events %v — a box off at every deadline must raise the missed-backup alarms, down or not", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A box that went down last night but made its dump yesterday evening (the catch-up) → no alarm (staleness owns it).
|
||||||
|
func TestR872_DownWithARecentDumpIsQuiet(t *testing.T) {
|
||||||
|
st, path, sc := downBox(t, 5*24*time.Hour)
|
||||||
|
if _, err := st.SaveEvent("c1", "db_dump_completed", "info", "ok", "{}", "controller"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r872Backdate(t, path, "UPDATE events SET created_at = ? WHERE event_type = 'db_dump_completed'", time.Now().UTC().Add(-20*time.Hour))
|
||||||
|
ts := time.Now().UTC().Add(-30 * time.Hour).Format(time.RFC3339)
|
||||||
|
if err := st.SaveHostReport("h1", "c1", []byte(`{"pbs_snapshots":[{"backup_time":"`+ts+`","verify_state":"ok"}],"backups":[]}`), store.HostReportDenorm{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := deadlineEvents(st, sc); got["expected_dbdump_missed"] != 0 || got["expected_backup_missed"] != 0 {
|
||||||
|
t.Fatalf("events %v — a dump 20 h ago and a backup 30 h ago are inside the down box's lines", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new box (first report a day ago) that is down → nothing expected yet.
|
||||||
|
func TestR872_NewDownBoxIsQuiet(t *testing.T) {
|
||||||
|
st, _, sc := downBox(t, 24*time.Hour)
|
||||||
|
if got := deadlineEvents(st, sc); len(got) != 0 {
|
||||||
|
t.Fatalf("events %v for a box bound a day ago", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -758,6 +758,15 @@ var operatorOnlyEvents = map[string]bool{
|
|||||||
// Read-only: the register itself stays unexported so nothing can widen it at runtime.
|
// Read-only: the register itself stays unexported so nothing can widen it at runtime.
|
||||||
func IsOperatorOnly(eventType string) bool { return operatorOnlyEvents[eventType] }
|
func IsOperatorOnly(eventType string) bool { return operatorOnlyEvents[eventType] }
|
||||||
|
|
||||||
|
// householdLivenessTypes are the "your server cannot be reached" family; householdLivenessQuiet is how long after
|
||||||
|
// one reached the household the next is held back (R-873).
|
||||||
|
var (
|
||||||
|
householdLivenessTypes = []string{"node_stale", "node_down", "host_stale", "host_down"}
|
||||||
|
householdLivenessWeekly = map[string]bool{"node_stale": true, "node_down": true, "host_stale": true, "host_down": true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const householdLivenessQuiet = 7 * 24 * time.Hour
|
||||||
|
|
||||||
func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, messageCustomer, detailsJSON, source string) {
|
func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, messageCustomer, detailsJSON, source string) {
|
||||||
// R-97c: operator-tier events stop here, BEFORE prefs are consulted — the point is that no
|
// R-97c: operator-tier events stop here, BEFORE prefs are consulted — the point is that no
|
||||||
// customer configuration can opt in. Logged rather than dropped, so the skip is visible in
|
// customer configuration can opt in. Logged rather than dropped, so the skip is visible in
|
||||||
@@ -785,6 +794,22 @@ func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, m
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// R-873 (v0.134.0): "your server cannot be reached" reaches the HOUSEHOLD at most once per 7 days. A box that
|
||||||
|
// is switched off every night (Tester 2, a laptop — measured 2026-10-05) mailed the household every night and
|
||||||
|
// "reachable again" every morning. The operator still gets every edge (processOperator, above), and the
|
||||||
|
// recovery mail stays paired with a down mail the household actually received (processRecovery), so a skipped
|
||||||
|
// down mail also skips its recovery. Persisted (notification_log), so a hub restart does not reset it.
|
||||||
|
// Decided by CC — operator may reverse (`08` §3.1). Pinned by TestR873_*.
|
||||||
|
if householdLivenessWeekly[eventType] {
|
||||||
|
if last, ok, err := d.store.LastCustomerSentAt(customerID, householdLivenessTypes); err == nil && ok && time.Since(last) < householdLivenessQuiet {
|
||||||
|
d.store.LogNotification(customerID, eventType, severity, message, "skipped",
|
||||||
|
"liveness: the household hears this at most once per 7 days (R-873)", "customer")
|
||||||
|
d.logger.Printf("[INFO] Customer mail skipped for %s/%s — the household was told about an outage %s ago (R-873: at most once per 7 days)",
|
||||||
|
customerID, eventType, time.Since(last).Round(time.Minute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Customer cooldown (from prefs, default 6h)
|
// Customer cooldown (from prefs, default 6h)
|
||||||
cooldownHours := prefs.CooldownHours
|
cooldownHours := prefs.CooldownHours
|
||||||
if cooldownHours <= 0 {
|
if cooldownHours <= 0 {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-873 (v0.134.0) — a household whose box is OFF EVERY NIGHT (Tester 2, a laptop) is told "your server cannot be
|
||||||
|
// reached" at most once per 7 days; the operator still hears every edge; the recovery mail stays paired.
|
||||||
|
// COMPANION RED-PROOF: drop the householdLivenessWeekly block in processCustomer → night 2 mails the household.
|
||||||
|
func TestR873_HouseholdHearsAnOutageAtMostWeekly(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "d.db")
|
||||||
|
st, err := store.New(path, log.New(io.Discard, "", 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer st.Close()
|
||||||
|
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "k", RetrievalPassword: "p"})
|
||||||
|
if err := st.SaveNotificationPrefs("c1", "cust@example.com", []string{"node_down"}, 6); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
night := func() (cust, op int) {
|
||||||
|
// a fresh dispatcher each night: the in-memory 6 h cooldown is gone by the next evening anyway
|
||||||
|
d := NewDispatcher(st, "test-key", "from@felhom.eu", "op@felhom.eu", true, log.New(io.Discard, "", 0))
|
||||||
|
sent := captureSeam(d)
|
||||||
|
d.ProcessEvent("c1", "node_down", "error", "No report received for 90m", "{}", "hub")
|
||||||
|
d.ProcessEvent("c1", "node_recovered", "info", "Reports resumed", "{}", "hub")
|
||||||
|
return len(mailsFor(*sent, "cust@example.com")), len(mailsFor(*sent, "op@felhom.eu"))
|
||||||
|
}
|
||||||
|
if c, o := night(); c != 2 || o != 2 {
|
||||||
|
t.Fatalf("night 1: household %d mails, operator %d — want down+recovered to both", c, o)
|
||||||
|
}
|
||||||
|
if c, o := night(); c != 0 || o != 2 {
|
||||||
|
t.Fatalf("night 2: household %d mails (want 0 — told once this week), operator %d (want every edge)", c, o)
|
||||||
|
}
|
||||||
|
// Eight days later the household is told again.
|
||||||
|
db, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
old := time.Now().UTC().Add(-8 * 24 * time.Hour).Format("2006-01-02 15:04:05")
|
||||||
|
if _, err := db.Exec(`UPDATE notification_log SET created_at = ?`, old); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c, _ := night(); c != 2 {
|
||||||
|
t.Fatalf("after 8 days the household got %d mails, want down+recovered again", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user