Skip to content

Commit f351504

Browse files
kvapsclaude
andauthored
test(harness): release-gate cleanup — cli-matrix/replay/burnin/cli-parity (batch 4) + unmask BUG-048 (#162)
* test(cli-matrix): fix bug-278 to exercise the real SkipDisk auto-clear contract The cell stamped DrbdOptions/SkipDisk=True on a HEALTHY UpToDate replica and read the prop back as a setup precondition. But the product's SkipDiskClearer correctly auto-clears the observer-owned stamp on every reconcile of a healthy slot (HasDisklessVolume==false), so the satellite wiped the stamp between the SSA apply and the read-back — the precondition raced the product's correct behaviour and aborted with "stamp did not land". The stamped state on a healthy disk is transient by design. Restructure into a two-sided contract: A. Persistence (positive control): stamp on a genuinely-Diskless replica, where the auto-clear gate is intentionally closed, and assert the stamp survives — proving the clearer is selective. B. Auto-clear: stamp on a healthy replica, restart its satellite (the Talos reattach shape), and poll for the stamp's ABSENCE (the steady state) plus disk UpToDate, instead of reading back the doomed stamp. No product behaviour changed. Validated on stand "big". Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * test(cli-matrix): fix SIGPIPE (exit 141) in drbd-options-hierarchy wait loop The cell picked a diskful node via `linstor_diskful_nodes | head -1`: head closed the pipe after the first node while the helper's internal `while read` loop was still emitting the rest, so under `set -o pipefail` the pipeline exited 141 and `set -e` aborted the cell before the C1/C2 assertions ran. The replay twin of this scenario passes, confirming the product is correct. Consume the helper via mapfile over a process substitution (no pipe) — the convention every other cell follows — and use the existing wait_diskful_count helper for the convergence wait. Also harden show_max_buffers: capture drbdsetup output into a variable before the awk `... exit`, so the early pipe close can never SIGPIPE the writer. Validated on stand "big". Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * test(replay): fix snap create-multiple CLI form so BUG-046 L7 coverage runs The replay called `snapshot create-multiple` with positional colon args (`{{rd}}:grp bug046-multi-b:grp`), which linstor-client 1.27.1 rejects with argparse exit 2 (`-r`/--resource_names is required). The step never ran, so BUG-046's L7 group-consistency coverage was dead. Use the correct CLI form: the snapshot name is a single positional and the resource names go to `-r`. The snapshot name must come BEFORE `-r`, otherwise argparse's variadic `-r A B grp` greedily consumes `grp` and leaves no positional ("too few arguments"). Mirrors the multi-resource form the cli-matrix cell drives. Validated PASS on stand "big". Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * test(burnin): guard md5 read against transient dd failures to kill false alarms The md5-consistency check compared primary vs peer md5 without checking dd's exit code. Under churn dd can fail to open /dev/drbdN (EAGAIN) and read zero bytes; md5("") is a fixed digest, so the unguarded read raised a FALSE mismatch alarm that would drown out a real future divergence. Guard each read with dd's exit status (PIPESTATUS) and the byte count (must equal the full 1 MiB); the md5 is still piped straight from dd into md5sum so binary NUL bytes are never routed through a shell variable. On a transient read failure the iteration emits a sentinel and its compare is SKIPPED (neither PASS nor FAIL) rather than reported as a mismatch. Validated on stand "big" (12/12 iterations pass, no false alarms). Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * test(cli-parity): normalize rg-l border/DfltRscGrp drift, whitelist sp-l topology The pool-name normalizer cleared n l / r l / v l / vd l false diffs but left rg l / rg l --pastable as WIRE_SHAPE: after the pool token is collapsed to <POOL>, two artefacts survived `diff -w`: - ASCII-table border width tracks the longest cell, so BS's pool name `stand` (5) rendered one dash wider than the oracle's `pool` (4) on every +----+ / |====| rule line (dashes are not whitespace). - The built-in DfltRscGrp row prints `PlaceCount: N` on upstream but a blank cell on BS — a render choice over the same default. Extend normalize_side_output to collapse border-char runs and strip the DfltRscGrp PlaceCount value on both sides. rg l (05) and rg l --pastable (20) now classify PARITY (verified against the live dev-stand raw artefacts). The residual `sp l` (03) divergence is genuine — the BS stand and the upstream oracle have different storage-pool topologies, and BS's FILE_THIN DTO leaves PoolName/SharedName blank where the oracle renders a backing path / shared name. Whitelisted as row 03 with justification rather than masked; sp l -m (79) and sp l --show-props (04) still catch real capacity/driver regressions. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * test(cli-matrix): fix multi-volume-late convergence detection (null vlms) The wait loops parsed `linstor --machine-readable resource list` for `.vlms[].state.disk_state`, but the blockstor apiserver leaves that array null in the golinstor JSON projection, so the jq always read empty and the loop timed out at the vol-0 stage even though `linstor r l` (which reads Resource.Status) showed every replica UpToDate. Switch the convergence checks to status_disk_state / wait_diskful_count (the CRD-backed `.status.volumes[].diskState`, the same reliable helpers the passing cells use), resolve the diskful node set via mapfile over a process substitution (no SIGPIPE), and probe the kernel-truth check via on_node drbdsetup instead of the same null-projection jq. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> --------- Signed-off-by: Andrei Kvapil <kvapss@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2b6dd2d commit f351504

7 files changed

Lines changed: 305 additions & 137 deletions

docs/cli-parity-known-deltas.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Row IDs match the command-catalogue indexes used by `cli-parity-refresh.sh` (see
1515

1616
| # | command | delta_kind | accepted_until | why |
1717
|---|---------|------------|----------------|-----|
18+
| 03 | `sp l` | WIRE_SHAPE | permanent | ENV_TOPOLOGY + envelope-shape. After pool-name + table-border normalization the `sp l` listing still diverges because the BS stand and the upstream oracle are provisioned with DIFFERENT storage-pool topologies (BS carries `lvm-thin` / `zfs-thin` / `zfs-thick` rows the FILE_THIN-only oracle never had) and because of two deliberate wire-shape choices on the FILE_THIN pool: (a) BS leaves the FILE_THIN `PoolName` cell blank where the oracle renders its backing-dir path `/var/lib/linstor-oracle-<pool>` (BS's FILE_THIN provider has no operator-facing PoolName — same family as row 04's StorDriver omission); (b) BS leaves the `SharedName` column blank where the oracle stamps `<node>;<pool>`, and BS provisions a single shared `DfltDisklessStorPool` row rather than one per node. None of these are behavioural deltas — they reflect the stand's pool fixtures and BS's leaner FILE_THIN DTO; linstor-csi / piraeus-operator read pool capacity (`sp l -m`, row 79) not the PoolName/SharedName render columns. Whitelisted so a topology-shaped `sp l` diff does not block the refresh; a genuine capacity/driver regression still surfaces via `sp l -m` (row 79) and `sp l --show-props` (row 04). |
1819
| 04 | `sp l --show-props StorDriver/*` | WIRE_SHAPE | permanent | BLOCKSTOR_SUPERSET: BS surfaces extra `StorDriver/LvmVg`/`ThinPool` keys; client glob still matches → CLI render identical. Operator-visible parity OK. |
1920
| 08 | `r l` (tiebreaker layers DRBD,STORAGE) | WIRE_SHAPE | 2026-12-31 | F10 residual: DRBD layer enrichment (`may_promote`, `promotion_score`, `node_id`, `al_*`) not yet stamped. Audit refresh 2026-05-19 reclassifies F10 as partial — `drbdtop`-style monitoring depends on it but CSI does not. |
2021
| 18 | `controller version` | WIRE_SHAPE | permanent | Intentional version stamping: BS reports `1.33.2+ git=blockstor`. Downstream tooling MUST NOT grep a hex git_hash from BS. |
@@ -71,3 +72,4 @@ These rows are **NOT** whitelisted on purpose — they appear in the audit but b
7172
- 2026-05-14 — original one-shot audit `docs/cli-parity-audit-2026-05-14.md`.
7273
- 2026-05-19 — refresh `docs/cli-parity-audit-2026-05-19-refresh.md`; F1-F20 closed (F10 partial residual remains as accepted delta #08); L7 harness `261d9e32f` lands re-runnable cli-parity-refresh.sh as the going-forward audit driver.
7374
- 2026-06-14 — reclassified two cli-matrix cells from "bug" to intentional BS contract: row 84 (tiebreaker→diskful promotion full-syncs by design, `anyDiskfulPeerHasData` gate avoids the UUID-mint StandAlone wedge) and row 85 (bare `SyncSource`/`SyncTarget` literal is the intended shape when `OutOfSyncKib<=0`). Cells `r-c-over-tiebreaker-skip-sync.sh` and `r-l-conns-shapes.sh` now assert the BS-intended behaviour.
75+
- 2026-06-14 — extended `cli-parity-refresh.sh`'s `normalize_side_output` to collapse ASCII-table border-width drift (pool-name length sets column width) and the upstream-only `DfltRscGrp PlaceCount` render, clearing the `rg l` (05) and `rg l --pastable` (20) false WIRE_SHAPE diffs → both now PARITY. The residual `sp l` (03) divergence is a genuine ENV_TOPOLOGY + FILE_THIN envelope-shape delta and is whitelisted as row 03 (not masked).

tests/burnin-blockstor.sh

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,46 @@ EOF
135135
fi
136136

137137
DEV=$(on_node "$PRIMARY" bash -c "grep -oE '/dev/drbd[0-9]+' /etc/drbd.d/${RD}.res | head -1")
138+
139+
# Write 1 MiB urandom on PRIMARY and capture its md5, then read it
140+
# back on PEER and compare. Each remote read GUARDS dd's exit code
141+
# (via PIPESTATUS) AND the byte count: under churn `dd` can fail to
142+
# open /dev/drbdN (EAGAIN, device transiently busy) and read ZERO
143+
# bytes; md5("") is a fixed digest, so an unguarded read produces a
144+
# FALSE mismatch alarm that would drown out a real future divergence.
145+
# The md5 is computed by piping dd STRAIGHT into md5sum (never via a
146+
# shell variable — command substitution strips NUL bytes, which a
147+
# binary 1 MiB read is full of). On a guarded read failure the snippet
148+
# emits the sentinel "READFAIL"; the iteration's compare is then
149+
# SKIPPED (not counted as a real FAIL) and re-tried next iteration.
150+
# `bs=1M count=1` reads exactly 1048576 bytes on success.
151+
EXPECT_BYTES=1048576
138152
PRIMARY_MD5=$(on_node "$PRIMARY" bash -c "
139153
drbdadm primary ${RD}
140154
dd if=/dev/urandom of=${DEV} bs=1M count=1 status=none oflag=direct
141-
dd if=${DEV} bs=1M count=1 status=none iflag=direct | md5sum | awk '{print \$1}'
155+
md5=\$(dd if=${DEV} bs=1M count=1 iflag=direct 2>/tmp/burnin-dd-primary.err | md5sum | awk '{print \$1}')
156+
rc=\${PIPESTATUS[0]}
157+
n=\$(awk '/bytes/ {print \$1; exit}' /tmp/burnin-dd-primary.err)
142158
drbdadm secondary ${RD}
159+
if [ \"\$rc\" -ne 0 ] || [ \"\$n\" != \"${EXPECT_BYTES}\" ]; then echo 'READFAIL'; else echo \"\$md5\"; fi
143160
" | tail -1)
144161

145162
PEER_MD5=$(on_node "$PEER" bash -c "
146163
drbdadm primary ${RD}
147-
dd if=${DEV} bs=1M count=1 status=none iflag=direct | md5sum | awk '{print \$1}'
164+
md5=\$(dd if=${DEV} bs=1M count=1 iflag=direct 2>/tmp/burnin-dd-peer.err | md5sum | awk '{print \$1}')
165+
rc=\${PIPESTATUS[0]}
166+
n=\$(awk '/bytes/ {print \$1; exit}' /tmp/burnin-dd-peer.err)
148167
drbdadm secondary ${RD}
168+
if [ \"\$rc\" -ne 0 ] || [ \"\$n\" != \"${EXPECT_BYTES}\" ]; then echo 'READFAIL'; else echo \"\$md5\"; fi
149169
" | tail -1)
150170

151-
if [[ "$PRIMARY_MD5" == "$PEER_MD5" ]]; then
171+
if [[ "$PRIMARY_MD5" == "READFAIL" || "$PEER_MD5" == "READFAIL" \
172+
|| -z "$PRIMARY_MD5" || -z "$PEER_MD5" ]]; then
173+
# Transient read failure on at least one side — neither PASS nor
174+
# FAIL. A real mismatch is only credible when BOTH reads succeeded
175+
# and returned the full 1 MiB.
176+
echo "[$(date -u +%FT%TZ)] iter=$ITER SKIP: transient dd read failure (primary='$PRIMARY_MD5' peer='$PEER_MD5'); not comparing"
177+
elif [[ "$PRIMARY_MD5" == "$PEER_MD5" ]]; then
152178
PASS=$((PASS + 1))
153179
else
154180
FAIL=$((FAIL + 1))

tests/e2e/cli-matrix/bug-278-skipdisk-autoclear-after-reattach.sh

Lines changed: 127 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,53 @@
1111
# `drbdadm adjust --skip-disk` and the local volume stayed Diskless
1212
# even though the kernel was healthy.
1313
#
14-
# Bug 278 fix: when the reconciler sees `SkipDisk=True` AND the kernel
15-
# reports the local volume as non-Diskless (UpToDate / Inconsistent /
16-
# Outdated), the satellite releases the observer's SSA claim on the
17-
# SkipDisk key via SkipDiskClearer. The next dispatcher cycle
18-
# re-resolves Spec.Props without SkipDisk, the FSM transitions
19-
# PhaseSkipDisk → PhaseRunning, and the next reconcile dispatches plain
20-
# `drbdadm adjust` to re-attach the lower disk.
14+
# Bug 278 fix (pkg/satellite/reconciler.go runAdjust + SkipDiskClearer):
15+
# on EVERY reconcile, when the reconciler sees the observer-owned
16+
# `SkipDisk=True` stamp AND the kernel probe reports the local volume
17+
# as non-Diskless (HasDisklessVolume==false — UpToDate / Inconsistent /
18+
# Outdated, backing storage attached), the satellite releases the
19+
# observer's SSA claim on the SkipDisk key via SkipDiskClearer. The next
20+
# dispatcher cycle re-resolves Spec.Props without SkipDisk, the FSM
21+
# transitions PhaseSkipDisk → PhaseRunning, and the next reconcile
22+
# dispatches plain `drbdadm adjust` to keep the lower disk attached.
2123
#
22-
# Contract this cell pins:
24+
# Two-sided contract this cell pins (BUG-046 triage corrected the
25+
# previous revision — see below):
2326
#
24-
# 1. Steady-state: 2-replica diskful RD, both UpToDate.
25-
# 2. Stamp SkipDisk=True onto Resource.Spec.Props on $N2 (simulates
26-
# the observer's defensive write on a transient Failed event the
27-
# kernel emits at Talos upgrade time).
28-
# 3. Restart the satellite pod on $N2 (simulates Talos kernel
29-
# restart). The pod reattaches, sees the SkipDisk prop, but
30-
# kernel state on $N2 is healthy (disk:UpToDate, backing_dev
31-
# present — the lower LV survived the OS upgrade).
32-
# 4. Within 60s assert: Spec.Props NO LONGER contains DrbdOptions/SkipDisk
33-
# AND Status.volumes[0].diskState is back to UpToDate.
27+
# A. PERSISTENCE (positive control). On a replica whose kernel volume
28+
# really is Diskless (HasDisklessVolume==true), the auto-clear gate
29+
# is INTENTIONALLY closed: a defensive SkipDisk stamp there MUST
30+
# survive — that is the exact state SkipDisk is meant to guard. This
31+
# proves the clearer is selective (it does not blindly wipe every
32+
# stamp) and that the stamp wiring works at all.
3433
#
35-
# Pre-fix: SkipDisk stays pinned forever; the cell would FAIL at
36-
# step 4 because Spec.Props still carries the key after 60s.
34+
# B. AUTO-CLEAR. On a HEALTHY (UpToDate) replica the satellite auto-
35+
# clears the observer-owned defensive stamp, so the volume stays
36+
# attached after a Talos-upgrade reattach. We restart the satellite
37+
# pod on that node (the documented Talos kernel-restart shape), and
38+
# assert the stamp is gone and the disk is UpToDate.
39+
#
40+
# Why the previous revision could never pass (BUG-046 triage): it stamped
41+
# SkipDisk on a HEALTHY UpToDate replica and then read the prop BACK as a
42+
# setup precondition (`assert stamped == "True"`). But the auto-clear
43+
# fires on EVERY reconcile of a healthy slot, so the satellite cleared
44+
# the stamp between the SSA apply and the read-back — the precondition
45+
# raced the product's correct behaviour and aborted with "stamp did not
46+
# land". The stamped-state on a healthy disk is transient BY DESIGN; the
47+
# cell now asserts the auto-clear directly (poll for ABSENCE, the steady
48+
# state) and uses the genuinely-persistent Diskless replica as the
49+
# positive control instead of a brittle read-back of a doomed stamp.
50+
#
51+
# The SSA stamp is applied under the OBSERVER's field manager
52+
# (`blockstor-satellite-skipdisk`, pkg/satellite/controllers/observer.go)
53+
# so it carries the exact SSA ownership the real defensive write has.
54+
# The auto-clear RELEASES that owner's claim — that is the whole product
55+
# contract: the satellite un-stamps only its own defensive writes, while
56+
# an operator-set SkipDisk (different field manager) would survive. The
57+
# apply doc carries the two +required immutable scalars
58+
# (resourceDefinitionName / nodeName) — same SSA-validation rule the
59+
# observer's writeSkipDiskProp follows; --force-conflicts mirrors the
60+
# observer's ForceOwnership.
3761

3862
set -euo pipefail
3963

@@ -44,7 +68,7 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
4468
# shellcheck source=lib.sh
4569
source "$SCRIPT_DIR/lib.sh"
4670

47-
require_workers 2
71+
require_workers 3
4872

4973
linstor_cli_setup
5074

@@ -59,56 +83,90 @@ trap cleanup EXIT
5983

6084
N1=$WORKER_1
6185
N2=$WORKER_2
62-
63-
echo ">> [Bug 278] 2-replica diskful RD on $N1+$N2"
64-
"${LCTL[@]}" resource-definition create "$RD" >/dev/null
65-
"${LCTL[@]}" volume-definition create "$RD" 128M >/dev/null
66-
"${LCTL[@]}" resource create "$N1" "$RD" --storage-pool=stand >/dev/null
67-
"${LCTL[@]}" resource create "$N2" "$RD" --storage-pool=stand >/dev/null
68-
69-
echo ">> wait for both diskful UpToDate"
70-
RD="$RD" wait_uptodate "$RD" "$N1" "$N2"
71-
72-
echo ">> stamp DrbdOptions/SkipDisk=True onto $N2 (simulates pre-upgrade defensive stamp)"
73-
# Server-side-apply under the OBSERVER's field manager
74-
# (`blockstor-satellite-skipdisk`, pkg/satellite/controllers/observer.go)
75-
# so the stamp carries the exact SSA ownership the real defensive write
76-
# has. The Bug 278 auto-clear RELEASES that owner's claim — that is the
77-
# whole product contract: the satellite un-stamps only its own
78-
# defensive writes, while an operator-set SkipDisk (different field
79-
# manager) survives the reattach. The previous revision of this cell
80-
# stamped via `kubectl patch --type=merge` (field manager
81-
# "kubectl-patch"), which the release path correctly refuses to clear —
82-
# the cell was asserting the opposite of the product contract and could
83-
# never pass (BUG-040 triage).
84-
#
85-
# The apply doc must carry the two +required immutable scalars
86-
# (resourceDefinitionName / nodeName) — same SSA-validation rule the
87-
# observer's writeSkipDiskProp follows.
88-
# --force-conflicts mirrors the observer's ForceOwnership: the apply
89-
# doc must claim the two required scalars even though the controller's
90-
# field manager owns them.
91-
kubectl apply --server-side --force-conflicts --field-manager=blockstor-satellite-skipdisk -f - <<EOF
86+
N3=$WORKER_3
87+
88+
# stamp_skipdisk <node> — SSA-apply DrbdOptions/SkipDisk=True onto
89+
# (RD, NODE).Spec.Props under the observer's field manager, mirroring
90+
# the real defensive stamp's ownership shape.
91+
stamp_skipdisk() {
92+
local node=$1
93+
kubectl apply --server-side --force-conflicts \
94+
--field-manager=blockstor-satellite-skipdisk -f - <<EOF >/dev/null
9295
apiVersion: blockstor.cozystack.io/v1alpha1
9396
kind: Resource
9497
metadata:
95-
name: ${RD}.${N2}
98+
name: ${RD}.${node}
9699
spec:
97100
resourceDefinitionName: ${RD}
98-
nodeName: ${N2}
101+
nodeName: ${node}
99102
props:
100103
DrbdOptions/SkipDisk: "True"
101104
EOF
105+
}
106+
107+
# skipdisk_prop <node> — echo the current SkipDisk prop value on
108+
# (RD, NODE).Spec.Props ("True" or empty).
109+
skipdisk_prop() {
110+
local node=$1
111+
kubectl get "resources.blockstor.cozystack.io/${RD}.${node}" \
112+
-o jsonpath='{.spec.props.DrbdOptions/SkipDisk}' 2>/dev/null || echo ""
113+
}
102114

103-
echo ">> confirm SkipDisk is stamped on $N2 Spec.Props"
104-
stamped=$(kubectl get "resources.blockstor.cozystack.io/${RD}.${N2}" \
105-
-o jsonpath='{.spec.props.DrbdOptions/SkipDisk}' 2>/dev/null || echo "")
106-
if [[ "$stamped" != "True" ]]; then
107-
echo "FAIL (Bug 278 setup): SkipDisk stamp did not land (got '$stamped'); aborting" >&2
115+
echo ">> [Bug 278] 2-replica diskful RD on $N1+$N2, diskless replica on $N3"
116+
"${LCTL[@]}" resource-definition create "$RD" >/dev/null
117+
"${LCTL[@]}" volume-definition create "$RD" 128M >/dev/null
118+
"${LCTL[@]}" resource create "$N1" "$RD" --storage-pool=stand >/dev/null
119+
"${LCTL[@]}" resource create "$N2" "$RD" --storage-pool=stand >/dev/null
120+
"${LCTL[@]}" resource create "$N3" "$RD" --diskless >/dev/null
121+
122+
echo ">> wait for both diskful UpToDate ($N1, $N2)"
123+
wait_uptodate "$RD" "$N1" "$N2"
124+
125+
echo ">> wait for $N3 to settle Diskless"
126+
if ! wait_status_diskless "$RD" "$N3" 60; then
127+
echo "FAIL (Bug 278 setup): $N3 never converged to Diskless; aborting" >&2
128+
exit 1
129+
fi
130+
131+
# ---- Part A: PERSISTENCE on a genuinely-Diskless replica -----------------
132+
#
133+
# HasDisklessVolume==true on $N3, so the auto-clear gate is closed: the
134+
# defensive stamp MUST survive there. This is the positive control that
135+
# proves the clearer is selective (it does not blindly wipe every stamp).
136+
echo ">> [Bug 278/A] stamp SkipDisk=True onto the DISKLESS $N3 (must persist)"
137+
stamp_skipdisk "$N3"
138+
139+
echo ">> [Bug 278/A] confirm SkipDisk persists on Diskless $N3 (15s observation)"
140+
# Poll for a sustained 15s: the stamp must stay pinned, not flicker away.
141+
persist_ok=true
142+
for _ in $(seq 1 8); do
143+
val=$(skipdisk_prop "$N3")
144+
if [[ "$val" != "True" ]]; then
145+
persist_ok=false
146+
break
147+
fi
148+
sleep 2
149+
done
150+
if [[ "$persist_ok" != "true" ]]; then
151+
echo "FAIL (Bug 278/A regression): SkipDisk was cleared on the Diskless $N3 — the clearer is over-eager (must only clear on a HEALTHY slot)" >&2
152+
kubectl get "resources.blockstor.cozystack.io/${RD}.${N3}" \
153+
-o json 2>/dev/null | jq '{props: .spec.props, status: .status}' >&2 || true
108154
exit 1
109155
fi
110156

111-
echo ">> restart satellite pod on $N2 (simulates Talos kernel upgrade reattach)"
157+
# ---- Part B: AUTO-CLEAR on a HEALTHY replica after reattach --------------
158+
#
159+
# Stamp the same defensive prop onto the HEALTHY $N2. Restart its
160+
# satellite pod (the documented Talos kernel-upgrade reattach shape),
161+
# then assert the satellite auto-clears the observer-owned stamp and the
162+
# disk stays UpToDate. We poll for ABSENCE — the steady state the
163+
# product converges to — rather than the transient stamped state (which
164+
# the clearer correctly wipes on every reconcile of a healthy slot, and
165+
# which the previous revision wrongly tried to read back as a precondition).
166+
echo ">> [Bug 278/B] stamp SkipDisk=True onto the HEALTHY $N2 (simulates pre-upgrade defensive stamp)"
167+
stamp_skipdisk "$N2"
168+
169+
echo ">> [Bug 278/B] restart satellite pod on $N2 (simulates Talos kernel upgrade reattach)"
112170
sat_pod=$(kubectl -n "$NS" get pods -l app=blockstor-satellite \
113171
--field-selector "spec.nodeName=$N2" \
114172
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
@@ -119,6 +177,7 @@ fi
119177
kubectl -n "$NS" delete pod "$sat_pod" --wait=true >/dev/null
120178

121179
echo ">> wait for satellite back up on $N2"
180+
new_pod=""
122181
for _ in $(seq 1 60); do
123182
new_pod=$(kubectl -n "$NS" get pods -l app=blockstor-satellite \
124183
--field-selector "spec.nodeName=$N2,status.phase=Running" \
@@ -133,33 +192,31 @@ if [[ -z "$new_pod" || "$new_pod" == "$sat_pod" ]]; then
133192
exit 1
134193
fi
135194

136-
echo ">> wait up to 60s for the satellite to auto-clear SkipDisk on $N2"
195+
echo ">> [Bug 278/B] wait up to 60s for the satellite to auto-clear SkipDisk on $N2"
137196
# Poll Resource.Spec.Props.DrbdOptions/SkipDisk — Bug 278 contract:
138197
# the satellite reconciler probes kernel state, sees healthy
139-
# (disk:UpToDate, backing_dev set), and releases the observer's SSA
140-
# claim on the SkipDisk key. After SSA release the apiserver removes
141-
# the key from Spec.Props (no other owner claims it).
198+
# (HasDisklessVolume==false), and releases the observer's SSA claim on
199+
# the SkipDisk key. After SSA release the apiserver removes the key from
200+
# Spec.Props (no other owner claims it).
142201
cleared=false
143202
for _ in $(seq 1 30); do
144-
val=$(kubectl get "resources.blockstor.cozystack.io/${RD}.${N2}" \
145-
-o jsonpath='{.spec.props.DrbdOptions/SkipDisk}' 2>/dev/null || echo "")
146-
if [[ -z "$val" ]]; then
203+
if [[ -z "$(skipdisk_prop "$N2")" ]]; then
147204
cleared=true
148205
break
149206
fi
150207
sleep 2
151208
done
152209

153210
if [[ "$cleared" != "true" ]]; then
154-
echo "FAIL (Bug 278): SkipDisk did NOT auto-clear on $N2 within 60s after satellite restart" >&2
211+
echo "FAIL (Bug 278): SkipDisk did NOT auto-clear on the HEALTHY $N2 within 60s after satellite restart" >&2
155212
kubectl get "resources.blockstor.cozystack.io/${RD}.${N2}" \
156213
-o json 2>/dev/null | jq '{props: .spec.props, status: .status}' >&2 || true
157214
exit 1
158215
fi
159216

160-
echo ">> confirm $N2 disk state is back to UpToDate (re-attached after clear)"
217+
echo ">> [Bug 278/B] confirm $N2 disk state is UpToDate (stayed attached after clear)"
161218
if ! wait_status_state "$RD" "$N2" "UpToDate" 60 0; then
162-
echo "FAIL (Bug 278 deep): SkipDisk cleared but $N2 did not re-attach to UpToDate within 60s" >&2
219+
echo "FAIL (Bug 278 deep): SkipDisk cleared but $N2 did not hold UpToDate within 60s" >&2
163220
kubectl get "resources.blockstor.cozystack.io/${RD}.${N2}" \
164221
-o json 2>/dev/null | jq '{props: .spec.props, status: .status}' >&2 || true
165222
exit 1
@@ -175,4 +232,4 @@ if [[ "$n1_disk" != "UpToDate" ]]; then
175232
exit 1
176233
fi
177234

178-
echo ">> bug-278-skipdisk-autoclear-after-reattach OK (auto-clear fires on healthy reattach, $N2 back to UpToDate)"
235+
echo ">> bug-278-skipdisk-autoclear-after-reattach OK (A: stamp persists on Diskless $N3; B: auto-clear fires on healthy $N2 reattach, $N2 stays UpToDate)"

0 commit comments

Comments
 (0)