Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54d3a1057b | |||
| fbbb168d1f | |||
| 3911cc6f52 | |||
| ad80ee749a | |||
| 0683cc2dab | |||
| b1bc63dd32 | |||
| e66a48f2b7 | |||
| b890b57540 |
+43
-3
@@ -1,4 +1,44 @@
|
|||||||
## 2026-10-25 (night, unattended) — a BYO host loses our SSH drop-in in ONE update (R-1285) — committed, NOT released
|
## Unreleased (next: v0.167.0) — the BYO SSH clean-up in the SAME update (R-1285); a reinstalled box's whole-guest tier is due at once (R-1232); Go 1.27.3 (R-972) — committed, not released
|
||||||
|
|
||||||
|
- **R-972: Go 1.27.3** (`go.mod`; released 2026-10-19 with a security fix to `crypto/x509`). `govulncheck` v1.8.0 before:
|
||||||
|
1 called vulnerability (GO-2026-6809, `crypto/x509@go1.27.2`); after: 0. `golang.org/x/crypto` v0.58.0 is already the
|
||||||
|
newest. Full `go build/vet/test` green. The controller and hub move the same night.
|
||||||
|
- **R-1232 (P2):** `backup.BackupRunner.NewestArchiveTime` — the tier due-check — counts only archives written with THIS
|
||||||
|
storage's key on an encrypted storage (the R-727 discriminator; the archive carries no host id). Before: a reinstalled
|
||||||
|
box shares the customer's PBS namespace and guest number with the old box, the old box's archive read as „the last
|
||||||
|
backup", and the whole-guest tier waited up to its 168-h cadence (the 2026-10-23 test night). A key lookup failure is
|
||||||
|
an error (the due-check reads UNKNOWN), never a foreign age. Tests `TestR1232_*` (foreign only → none; own → its time;
|
||||||
|
an unencrypted dir → unfiltered), red-proved (case 1 returned the old box's time).
|
||||||
|
|
||||||
|
- **The cause (measured 2026-10-26 on demo-hp, felhom.eu R-1285):** a box gets `agent_update` and `agent_config_update` in
|
||||||
|
one pass; the bundle job runs in the OLD process (no policy pass in it) and the restart into the new agent comes a
|
||||||
|
second later — so 0.166.0's pass never ran on the bundle that delivered it.
|
||||||
|
- **The fix:** `selfupdate.Manager.SetOnCommitted` — a hook run once right after „update committed"; `main.go`
|
||||||
|
`wireBundlePolicyAfterCommit` hooks `osupdate.ConfigUpdateExecutor.PolicyPassAfterUpdate` (the wrapper's
|
||||||
|
`bundle-policy` mode, host layer, no envelope) onto it. By the commit the wrapper on disk is the bundle's own, so a BYO
|
||||||
|
host loses our drop-in in the same update. Idempotent; an appliance removes nothing. The smaller of the two fixes:
|
||||||
|
agent-only — holding the bundle job until the commit would need new hub queue logic.
|
||||||
|
- **Test in the real order:** `cmd/felhom-agent/r1285_policy_after_commit_test.go` — a pending marker for the running
|
||||||
|
version → commit, THEN one `bundle-policy` plan; no marker / a mismatched marker → nothing. Red-proved (the hook call
|
||||||
|
removed → commit only, no pass — the demo-hp symptom).
|
||||||
|
- **Which box can show it live:** only one coming FROM 0.164.0 — 0.165.0's wrapper already applies D43 at plan time, so a
|
||||||
|
0.165.0 box passes even without this fix.
|
||||||
|
|
||||||
|
## v0.166.0 (2026-10-26) — a signed job's window is at most 24 h (R-1280); the policy pass after each bundle (R-1285); the published gate follows D50 (R-1290) — released in the attended release run; **VOUCHED by the operator 2026-10-26 10:04:55 CET** (binary `3f22c65b…`, bundle `093772c1…`)
|
||||||
|
|
||||||
|
Released from a clean clone at `aa69114` (tag `v0.166.0`; binary sha256 `3f22c65b78914632fa476292f6b58e5af3ed9e9900f3a5a7a347a9c0bfea9c5c`,
|
||||||
|
bundle `093772c1bda8b974e86ed45472f8fadc8992f247563f5739876102a5156003a3`); delivered to Tester 1, demo-hp and demo-felhom
|
||||||
|
by signed `agent_update` + `agent_config_update` (each: COMPLETED, bundle INSTALLED, self-check 76/76, update committed).
|
||||||
|
Live: a 25 h signed job REJECTED on demo-hp („op validity window too long"), a 30 min one ran; `felhom-opsign -ttl 25h`
|
||||||
|
refuses. D50 pin `day0_agent` → 0.166.0 (`scripts/retention-policy.json`). Evidence: felhom.eu
|
||||||
|
`documentation/audits/release-monday-2026-10-26/agent-0166/`.
|
||||||
|
|
||||||
|
**Found at the release (felhom.eu R-1285, back to OPEN):** in the normal two-job delivery the bundle job runs in the OLD
|
||||||
|
process (0.165.0) before the restart into 0.166.0, so the policy pass does not run on that bundle — a BYO host coming
|
||||||
|
from 0.164.0 still loses our drop-in one release late. The pass itself is wired: under 0.166.0 a later bundle logged
|
||||||
|
„BUNDLE POLICY DONE install_mode=appliance removed=0".
|
||||||
|
|
||||||
|
### 2026-10-25 (night, unattended) — a BYO host loses our SSH drop-in in ONE update (R-1285) — committed, NOT released
|
||||||
|
|
||||||
- **The cause, measured 2026-10-25 on a BYO test host:** the bundle that brings a new `felhom-os-apply` is planned by
|
- **The cause, measured 2026-10-25 on a BYO test host:** the bundle that brings a new `felhom-os-apply` is planned by
|
||||||
the OLD one on disk, which did not know D43's appliance-only policy — so a BYO host upgraded from 0.164.0 kept our
|
the OLD one on disk, which did not know D43's appliance-only policy — so a BYO host upgraded from 0.164.0 kept our
|
||||||
@@ -13,7 +53,7 @@
|
|||||||
`ByoKeepsItsSSH.test_the_policy_pass_*` (three). Red-proofs: felhom.eu
|
`ByoKeepsItsSSH.test_the_policy_pass_*` (three). Red-proofs: felhom.eu
|
||||||
`documentation/audits/night-2026-10-25/redproof-r1285.txt`.
|
`documentation/audits/night-2026-10-25/redproof-r1285.txt`.
|
||||||
|
|
||||||
## 2026-10-25 (night, unattended) — a signed job's window is at most 24 h (R-1280, D44 #22) — committed, NOT released (rides the next agent release; the vouch is the operator's)
|
### 2026-10-25 (night, unattended) — a signed job's window is at most 24 h (R-1280, D44 #22) — committed, NOT released (rides the next agent release; the vouch is the operator's)
|
||||||
|
|
||||||
- `internal/authz/verifier.go`: `MaxWindow = 24h`; a blob whose `expires_at - issued_at` is longer is refused
|
- `internal/authz/verifier.go`: `MaxWindow = 24h`; a blob whose `expires_at - issued_at` is longer is refused
|
||||||
(`ErrWindowTooLong`) before the expiry and nonce checks — a job signed with a long window by mistake no longer stays
|
(`ErrWindowTooLong`) before the expiry and nonce checks — a job signed with a long window by mistake no longer stays
|
||||||
@@ -25,7 +65,7 @@
|
|||||||
`test_felhom_os_apply.DockerLane.test_a_window_longer_than_24h_is_refused` / `…_of_24h_is_accepted`. Both refusals
|
`test_felhom_os_apply.DockerLane.test_a_window_longer_than_24h_is_refused` / `…_of_24h_is_accepted`. Both refusals
|
||||||
seen failing with their check switched off: felhom.eu `documentation/audits/night-2026-10-25/redproof-r1280.txt`.
|
seen failing with their check switched off: felhom.eu `documentation/audits/night-2026-10-25/redproof-r1280.txt`.
|
||||||
|
|
||||||
## 2026-10-25 (evening, attended) — the `published` gate follows D50 = A: the newest 5 plus the pinned MinAgent and day-0 agent (R-1290; no binary change)
|
### 2026-10-25 (evening, attended) — the `published` gate follows D50 = A: the newest 5 plus the pinned MinAgent and day-0 agent (R-1290; no binary change)
|
||||||
|
|
||||||
- **D50 = A** (operator, 2026-10-25): the registry keeps the newest 5 released agent versions, the hub's MinAgent floor and
|
- **D50 = A** (operator, 2026-10-25): the registry keeps the newest 5 released agent versions, the hub's MinAgent floor and
|
||||||
the day-0 manifest's agent. `scripts/retention-policy.json`: `generic_versions_kept` 10 → 5, new `pinned`
|
the day-0 manifest's agent. `scripts/retention-policy.json`: `generic_versions_kept` 10 → 5, new `pinned`
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# CONTEXT — felhom-agent working state
|
# CONTEXT — felhom-agent working state
|
||||||
|
|
||||||
|
> **2026-10-26 — R-1285 on `main` (`b1bc63d`), NOT released (operator: „not today").** The new agent runs the bundle
|
||||||
|
> policy pass once after its self-update commits (`selfupdate.Manager.SetOnCommitted` → `PolicyPassAfterUpdate`). Live
|
||||||
|
> proof needs a BYO box coming from 0.164.0. Tuesday 2026-10-27: the weekly line test's first run against a seeding box
|
||||||
|
> on demo-hp (felhom.eu R-1250, read, not forced).
|
||||||
|
|
||||||
> **2026-10-24 night (unattended) — D43 = A on `main` (`c8d3c17`), NOT released (next v0.165.0, with R-1250).** The sshd
|
> **2026-10-24 night (unattended) — D43 = A on `main` (`c8d3c17`), NOT released (next v0.165.0, with R-1250).** The sshd
|
||||||
> drop-in's bundle policy is `appliance` (install record `/var/lib/felhom-install/state.json` mode); BYO: nothing written,
|
> drop-in's bundle policy is `appliance` (install record `/var/lib/felhom-install/state.json` mode); BYO: nothing written,
|
||||||
> our copy removed; no proof: untouched. X3: `lan_addresses()` for `sshd -T`. X4: `undo_sshd_step` — only the SSH file is
|
> our copy removed; no proof: untouched. X3: `lan_addresses()` for `sshd -T`. X4: `undo_sshd_step` — only the SSH file is
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
# REPORT — D50 retention gate, R-1280, R-1285: committed, not released (2026-10-25 night)
|
# REPORT — R-1285: the BYO SSH clean-up in the same update — committed, NOT released (2026-10-26 afternoon)
|
||||||
|
|
||||||
**Plain words first.** Three agent changes are on `main`. None is released. The release (v0.166.0) is Monday's and
|
**Plain words first.** A BYO box coming from 0.164.0 kept our SSH file one release too long: in the normal update the
|
||||||
waits for the operator's vouch (felhom.eu `STATUS.md`, the Monday list).
|
OLD agent installs the config bundle, so the new agent's clean-up pass never ran on it. Now the new agent runs that pass
|
||||||
|
once, right after its self-update commits. Not released today (operator: „not today"); its live proof needs a BYO box on
|
||||||
|
0.164.0, which rides the next agent release.
|
||||||
|
|
||||||
## Each change, before → after
|
## The fix (`b1bc63d`, CI 2363 ✓)
|
||||||
|
|
||||||
| Row | Commit | Before | After | Proof |
|
- `internal/selfupdate/commit.go`: `Manager.SetOnCommitted` — a hook run once after „update committed".
|
||||||
|---|---|---|---|---|
|
- `internal/osupdate/bundle.go`: `ConfigUpdateExecutor.PolicyPassAfterUpdate` (the wrapper's `bundle-policy` mode, host
|
||||||
| R-1290 (D50 = A) | `feaaa0e` | the published-versions gate knew no keep rule | it asserts the newest 5 released versions plus the pinned MinAgent floor (0.158.1) and the day-0 agent (0.165.0) in `scripts/retention-policy.json`; a pin with no tag fails | decoys seen failing; CI 2339 ✓ |
|
layer, no envelope); `cmd/felhom-agent/main.go` `wireBundlePolicyAfterCommit` hooks it on.
|
||||||
| R-1280 | `d912307` | a signed job's window could be any length | at most 24 h — the verifier (`ErrWindowTooLong`), the root wrapper (`MAX_WINDOW_S`) and `felhom-opsign -ttl`; exactly 24 h is accepted | red-proof; unit tests; not live |
|
- The smaller of the two fixes: agent-only — holding the bundle job until the commit would need new hub queue logic.
|
||||||
| R-1285 | `abcc1e6` | a BYO host upgraded from an old wrapper kept our SSH drop-in until its NEXT bundle (the old wrapper planned the new bundle) | after every bundle install the agent runs the new wrapper's `bundle-policy` pass; the file is removed in one update; a failing `sshd -t` puts it back | red-proof; live on a BYO test VM: one signed run gave `BUNDLE POLICY REMOVED`; the test package was deleted after (204, then 404) |
|
|
||||||
|
|
||||||
## CI (by commit hash, `jobs` endpoint)
|
## Proof
|
||||||
|
|
||||||
`feaaa0e` 2339 ✓ · `d912307` 2344 ✓ · `abcc1e6` 2345 ✓ — all `run_attempt` 1.
|
- `cmd/felhom-agent/r1285_policy_after_commit_test.go`, in the real order: a pending marker for the running version →
|
||||||
|
`commit`, then exactly one `bundle-policy` plan; no marker / a mismatched marker → nothing. Red-proved (the hook call
|
||||||
## Teardown
|
removed → commit only, no pass — the demo-hp symptom of 2026-10-26).
|
||||||
|
- **Not live:** only a box coming FROM 0.164.0 can show it (0.165.0's wrapper already applies D43 at plan time), which
|
||||||
The BYO test VM (demo-hp 9372) was uninstalled through the product and destroyed. Its hub host record needs the
|
needs a signed downgrade chain on a BYO box.
|
||||||
operator's delete (felhom.eu `STATUS.md`).
|
|
||||||
|
|
||||||
## Evidence
|
|
||||||
|
|
||||||
felhom.eu `documentation/audits/night-2026-10-25/` (`redproof-d50.txt`, `redproof-r1280.txt`, `redproof-r1285.txt`,
|
|
||||||
`rig/r1285-live.txt`).
|
|
||||||
|
|||||||
@@ -1216,6 +1216,9 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
|||||||
}
|
}
|
||||||
logger.Warn("osupdate: capability probe after the config bundle", "ok", ok, "total", total, "degraded", strings.Join(names, ","))
|
logger.Warn("osupdate: capability probe after the config bundle", "ok", ok, "total", total, "degraded", strings.Join(names, ","))
|
||||||
}}
|
}}
|
||||||
|
// R-1285: the NEW agent runs the bundle policy pass once after its self-update commits — in the normal two-job
|
||||||
|
// delivery the bundle was installed by the OLD process, which has no such pass (measured 2026-10-26).
|
||||||
|
wireBundlePolicyAfterCommit(selfUpdateMgr, bundleExec, logger, version)
|
||||||
// v0.158.0 (felhom.eu `16` §9): the internal data place on an old box — the root helper, then the mount (late-bound
|
// v0.158.0 (felhom.eu `16` §9): the internal data place on an old box — the root helper, then the mount (late-bound
|
||||||
// to the local API server built below).
|
// to the local API server built below).
|
||||||
internalPlaceExec := signedjobs.NewInternalPlaceExecutor(suRunner,
|
internalPlaceExec := signedjobs.NewInternalPlaceExecutor(suRunner,
|
||||||
@@ -4040,3 +4043,9 @@ func agentStateDir(cfg config.Config) string {
|
|||||||
}
|
}
|
||||||
return "/var/lib/felhom-agent"
|
return "/var/lib/felhom-agent"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wireBundlePolicyAfterCommit hooks the bundle policy pass onto the self-update commit (R-1285). Pinned by
|
||||||
|
// TestR1285_TheNewAgentRunsThePolicyPassAfterItsCommit.
|
||||||
|
func wireBundlePolicyAfterCommit(m *selfupdate.Manager, e osupdate.ConfigUpdateExecutor, lg *slog.Logger, ver string) {
|
||||||
|
m.SetOnCommitted(func(ctx context.Context) { e.PolicyPassAfterUpdate(ctx, lg, ver) })
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-agent/internal/osupdate"
|
||||||
|
"gitea.dooplex.hu/admin/felhom-agent/internal/selfupdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-1285, in the REAL order (measured 2026-10-26 on demo-hp): the box gets agent_update and agent_config_update in one
|
||||||
|
// pass; the OLD process installs the bundle (no policy pass in it), then restarts into the new agent, which dwells and
|
||||||
|
// commits. The consequence asserted: the NEW agent runs the bundle policy pass exactly once, AFTER its commit — so a
|
||||||
|
// BYO host loses our drop-in in the same update. No pass without a commit (no marker, a mismatched marker).
|
||||||
|
// RED-PROOF: drop the wireBundlePolicyAfterCommit call in main → no bundle-policy plan after the commit.
|
||||||
|
|
||||||
|
// orderRunner records, in order, the self-update wrapper's verbs and the os-apply plans' modes.
|
||||||
|
type orderRunner struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
order []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *orderRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if name == osupdate.WrapperPath && len(args) == 2 && args[0] == "--plan" {
|
||||||
|
var p map[string]any
|
||||||
|
b, _ := os.ReadFile(args[1])
|
||||||
|
_ = json.Unmarshal(b, &p)
|
||||||
|
mode, _ := p["mode"].(string)
|
||||||
|
r.order = append(r.order, "os-apply:"+mode)
|
||||||
|
return []byte(`OSAPPLY-REPORT {"mode":"bundle-policy","bundle_policy":{"install_mode":"byo","removed":["/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf"]}}` + "\n"), nil, nil
|
||||||
|
}
|
||||||
|
if len(args) >= 1 {
|
||||||
|
r.order = append(r.order, filepath.Base(name)+":"+args[0])
|
||||||
|
}
|
||||||
|
return []byte("ok"), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *orderRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
||||||
|
return r.Run(ctx, name, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func r1285Start(t *testing.T, marker *selfupdate.PendingMarker, running string) []string {
|
||||||
|
t.Helper()
|
||||||
|
stateDir := t.TempDir()
|
||||||
|
if marker != nil {
|
||||||
|
dir := filepath.Join(stateDir, "selfupdate")
|
||||||
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(marker)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "pending.json"), b, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := &orderRunner{}
|
||||||
|
lg := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
mgr := selfupdate.NewManager(selfupdate.ManagerConfig{StateDir: stateDir, RunningVersion: running, Dwell: time.Millisecond, Runner: r, Logger: lg})
|
||||||
|
leg := &osupdate.Leg{Runner: r, Logger: lg, PlanDir: t.TempDir(), StatePath: filepath.Join(t.TempDir(), "last")}
|
||||||
|
exec := osupdate.ConfigUpdateExecutor{Leg: leg}
|
||||||
|
wireBundlePolicyAfterCommit(mgr, exec, lg, running)
|
||||||
|
mgr.MaybeCommit(context.Background())
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return append([]string(nil), r.order...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestR1285_TheNewAgentRunsThePolicyPassAfterItsCommit(t *testing.T) {
|
||||||
|
got := r1285Start(t, &selfupdate.PendingMarker{OldVersion: "0.166.0", NewVersion: "0.167.0", SHA256: "ab", AppliedAt: "t"}, "0.167.0")
|
||||||
|
if len(got) != 2 || got[0] != "felhom-selfupdate-guarded:commit" || got[1] != "os-apply:bundle-policy" {
|
||||||
|
t.Fatalf("the new agent must commit, then run ONE bundle policy pass: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestR1285_NoCommitNoPolicyPass(t *testing.T) {
|
||||||
|
if got := r1285Start(t, nil, "0.167.0"); len(got) != 0 {
|
||||||
|
t.Fatalf("an ordinary start (no pending update) must run nothing: %v", got)
|
||||||
|
}
|
||||||
|
got := r1285Start(t, &selfupdate.PendingMarker{OldVersion: "0.166.0", NewVersion: "0.168.0", SHA256: "ab", AppliedAt: "t"}, "0.167.0")
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Fatalf("a mismatched marker is not committed, so no pass: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module gitea.dooplex.hu/admin/felhom-agent
|
module gitea.dooplex.hu/admin/felhom-agent
|
||||||
|
|
||||||
go 1.27.2
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/crypto v0.58.0
|
golang.org/x/crypto v0.58.0
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-1232 — the 2026-10-23 test night: a reinstalled box (new install, new PBS key, same customer namespace, same guest
|
||||||
|
// number 9201) found the OLD box's archive (2.6 days old) when the due-check asked „when did this tier last land?", so
|
||||||
|
// the whole-guest tier read „not due" for up to its 168-h cadence and the ep0 copy stayed the OLD box's. The due-check
|
||||||
|
// now counts only archives written with THIS storage's key (the R-727 discriminator; the archive carries no host id).
|
||||||
|
// The CONSEQUENCE asserted: with only a foreign archive the answer is „none found" (the tier is due); with the box's
|
||||||
|
// own archive, that archive's time. COMPANION RED-PROOF: drop the key filter in NewestArchiveTime → case 1 returns the
|
||||||
|
// old box's time.
|
||||||
|
func TestR1232_TheDueCheckCountsOnlyThisBoxsArchives(t *testing.T) {
|
||||||
|
oldBox := proxmox.StorageContent{VolID: "felhom-pbsdr:backup/ct/9201/2026-10-20T21:10:00Z", Content: "backup", VMID: 9201,
|
||||||
|
Size: 3490689830, CTime: time.Date(2026, 10, 20, 21, 10, 0, 0, time.UTC).Unix(), Encrypted: earlierBox1}
|
||||||
|
api := &fakeBackupAPI{
|
||||||
|
storages: []proxmox.Storage{{Storage: "felhom-pbsdr", Type: "pbs", EncryptionKey: thisBoxKey}},
|
||||||
|
content: []proxmox.StorageContent{oldBox},
|
||||||
|
}
|
||||||
|
r := NewBackupRunner(api, "felhom-pbsdr", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
||||||
|
|
||||||
|
// 1. The fresh box has written nothing yet: the old box's archive must not count.
|
||||||
|
when, found, err := r.NewestArchiveTime(context.Background(), 9201)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
t.Fatalf("the due-check counted ANOTHER box's archive (%s) as this tier's last backup — the tier would wait a week", when)
|
||||||
|
}
|
||||||
|
// 2. After the box's own first copy, that copy is the answer.
|
||||||
|
own := proxmox.StorageContent{VolID: "felhom-pbsdr:backup/ct/9201/2026-10-24T02:30:00Z", Content: "backup", VMID: 9201,
|
||||||
|
Size: 3600000000, CTime: time.Date(2026, 10, 24, 2, 30, 0, 0, time.UTC).Unix(), Encrypted: thisBoxKey}
|
||||||
|
api.content = append(api.content, own)
|
||||||
|
when, found, err = r.NewestArchiveTime(context.Background(), 9201)
|
||||||
|
if err != nil || !found || when.Unix() != own.CTime {
|
||||||
|
t.Fatalf("want this box's own archive time %d, got %v found=%v err=%v", own.CTime, when, found, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unencrypted storage (a local dir) holds only this box's vzdumps: no key filter, the newest counts.
|
||||||
|
func TestR1232_UnencryptedStorageCountsEveryArchive(t *testing.T) {
|
||||||
|
api := &fakeBackupAPI{
|
||||||
|
storages: []proxmox.Storage{{Storage: "local", Type: "dir"}},
|
||||||
|
content: []proxmox.StorageContent{{VolID: "local:backup/vzdump-lxc-9201-2026_10_25-21_27_05.tar.zst", Content: "backup", VMID: 9201, Size: 955425507, CTime: 1793050025}},
|
||||||
|
}
|
||||||
|
r := NewBackupRunner(api, "local", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
||||||
|
if _, found, err := r.NewestArchiveTime(context.Background(), 9201); err != nil || !found {
|
||||||
|
t.Fatalf("an unencrypted local archive was not counted: found=%v err=%v", found, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -609,11 +609,23 @@ func (r *BackupRunner) NewestArchiveTime(ctx context.Context, vmid int) (time.Ti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, false, err
|
return time.Time{}, false, err
|
||||||
}
|
}
|
||||||
|
// R-1232: on an ENCRYPTED storage only archives written with THIS storage's key are this box's (the R-727
|
||||||
|
// discriminator — the archive carries no host id). A reinstalled box shares the customer's namespace and guest
|
||||||
|
// number with the old box, and the old box's archive made the tier read „not due" for a whole cadence (168 h).
|
||||||
|
// A key lookup failure is returned: the due-check reads UNKNOWN, never a foreign archive's age.
|
||||||
|
ownKey, err := r.storageKeyFingerprint(ctx, r.target)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false, fmt.Errorf("reading the key fingerprint of storage %s: %w", r.target, err)
|
||||||
|
}
|
||||||
var best int64 = -1
|
var best int64 = -1
|
||||||
for _, e := range contents {
|
for _, e := range contents {
|
||||||
if e.Content != "backup" || e.VMID != vmid {
|
if e.Content != "backup" || e.VMID != vmid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if ownKey != "" && !strings.EqualFold(e.Encrypted, ownKey) {
|
||||||
|
r.noteNotAGuestBackupOnce(e, fmt.Sprintf("written by another box (key %s, this box's key %s) — not this tier's last backup (R-1232)", shortFP(e.Encrypted), shortFP(ownKey)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
if ok, why := archivePlausiblyComplete(e); !ok {
|
if ok, why := archivePlausiblyComplete(e); !ok {
|
||||||
r.warnRejectedArchiveOnce(e, why)
|
r.warnRejectedArchiveOnce(e, why)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -126,6 +126,19 @@ func (e ConfigUpdateExecutor) Execute(ctx context.Context, op string, params jso
|
|||||||
// appliance-only files' policy (mode "bundle-policy"): on a BYO host it removes the SSH drop-in we wrote. The bundle run
|
// appliance-only files' policy (mode "bundle-policy"): on a BYO host it removes the SSH drop-in we wrote. The bundle run
|
||||||
// itself was planned by the wrapper that was on disk BEFORE it, which may predate the policy (0.164.0). Never fatal:
|
// itself was planned by the wrapper that was on disk BEFORE it, which may predate the policy (0.164.0). Never fatal:
|
||||||
// the bundle is installed either way; an older wrapper refuses the unknown mode and the next bundle does it.
|
// the bundle is installed either way; an older wrapper refuses the unknown mode and the next bundle does it.
|
||||||
|
// PolicyPassAfterUpdate runs the wrapper's bundle-policy pass once after a self-update commits (R-1285). In the normal
|
||||||
|
// delivery the box gets agent_update and agent_config_update in ONE pass: the bundle job runs in the OLD process, which
|
||||||
|
// has no policy pass, and the restart into the new agent comes a second later (measured 2026-10-26 on demo-hp). By the
|
||||||
|
// commit the wrapper on disk is the bundle's own, so the pass removes our appliance-only files from a BYO host in the
|
||||||
|
// SAME update. Idempotent; on an appliance it removes nothing.
|
||||||
|
func (e ConfigUpdateExecutor) PolicyPassAfterUpdate(ctx context.Context, lg *slog.Logger, ver string) {
|
||||||
|
if lg == nil {
|
||||||
|
lg = slog.Default()
|
||||||
|
}
|
||||||
|
lg.Info("osupdate: running the bundle policy pass after the self-update commit (R-1285)", "agent_version", ver)
|
||||||
|
e.policyPass(ctx, lg, ver)
|
||||||
|
}
|
||||||
|
|
||||||
func (e ConfigUpdateExecutor) policyPass(ctx context.Context, lg *slog.Logger, ver string) {
|
func (e ConfigUpdateExecutor) policyPass(ctx context.Context, lg *slog.Logger, ver string) {
|
||||||
runID := "bundle-policy-" + e.Leg.now().UTC().Format("20060102T150405Z")
|
runID := "bundle-policy-" + e.Leg.now().UTC().Format("20060102T150405Z")
|
||||||
wr, err := e.Leg.call(ctx, runID, map[string]any{"release_id": "bundle-policy-" + ver, "layer": LayerHost, "mode": "bundle-policy"})
|
wr, err := e.Leg.call(ctx, runID, map[string]any{"release_id": "bundle-policy-" + ver, "layer": LayerHost, "mode": "bundle-policy"})
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ type Manager struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
now func() time.Time // injectable for tests
|
now func() time.Time // injectable for tests
|
||||||
sleep func(context.Context, time.Duration)
|
sleep func(context.Context, time.Duration)
|
||||||
|
// onCommitted runs once, right after a successful commit (R-1285: the bundle policy pass of the NEW agent — in the
|
||||||
|
// normal two-job delivery the bundle was installed by the OLD process, which has no such pass). nil = none.
|
||||||
|
onCommitted func(context.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOnCommitted wires the after-commit hook (R-1285). INIT-ONLY: call it before MaybeCommit's goroutine starts.
|
||||||
|
func (m *Manager) SetOnCommitted(fn func(context.Context)) { m.onCommitted = fn }
|
||||||
|
|
||||||
// ManagerConfig wires the commit manager.
|
// ManagerConfig wires the commit manager.
|
||||||
type ManagerConfig struct {
|
type ManagerConfig struct {
|
||||||
StateDir string
|
StateDir string
|
||||||
@@ -119,4 +125,7 @@ func (m *Manager) MaybeCommit(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.logger.Warn("selfupdate: update committed", "version", m.runningVersion, "wrapper", trim(stdout))
|
m.logger.Warn("selfupdate: update committed", "version", m.runningVersion, "wrapper", trim(stdout))
|
||||||
|
if m.onCommitted != nil {
|
||||||
|
m.onCommitted(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
"readers": [
|
"readers": [
|
||||||
"scripts/check-published-versions.py — asserts the newest N versions plus every pinned version"
|
"scripts/check-published-versions.py — asserts the newest N versions plus every pinned version"
|
||||||
],
|
],
|
||||||
"recorded": "2026-10-25",
|
"recorded": "2026-10-26",
|
||||||
"recorded_by": "CC 2026-10-25, D50 = A (operator); earlier history: R-287 (2026-08-09), R-291 (2026-10-05)",
|
"recorded_by": "CC 2026-10-26: day0_agent 0.166.0 (operator vouch 2026-10-26 10:04:55 CET); D50 = A (operator 2026-10-25); earlier history: R-287 (2026-08-09), R-291 (2026-10-05)",
|
||||||
"pinned": {
|
"pinned": {
|
||||||
"hub_min_agent": "0.158.1",
|
"hub_min_agent": "0.158.1",
|
||||||
"day0_agent": "0.165.0"
|
"day0_agent": "0.166.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user