R-310: the golden refusal states the vouched version once; an uninstall with no terminal refuses in words
- The R-297 named-golden refusal no longer repeats "The vouched golden is X." when its reason already says it (kept when the reason does not name the version). - --uninstall checks it can open /dev/tty before the typed vmid confirmation and, if not, refuses with a sentence (deliberate; --force does not skip it) instead of "/dev/tty: No such device". scripts/test_hostinstall.py: test_golden_refusal_* (2), test_require_tty_* (3, with a pty control). The runbook line naming the pty requirement is a documentation edit for the lead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1101,6 +1101,19 @@ _remove_network_storage_units() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# _require_tty WHAT — refuse, in words, when no terminal can be opened (R-310). The uninstall's typed
|
||||
# confirmation reads /dev/tty on purpose and --force does NOT bypass it: an irreversible destroy is not
|
||||
# scriptable without a person. Without this, an unattended run died on
|
||||
# "line N: /dev/tty: No such device or address", which reads as a crash, not as a refusal.
|
||||
# Pinned by scripts/test_hostinstall.py (test_require_tty_*).
|
||||
_require_tty() {
|
||||
if ! { : < /dev/tty; } 2>/dev/null; then
|
||||
die "$1 needs a terminal, and none is attached — nothing was destroyed.
|
||||
This is deliberate: an irreversible teardown is never confirmed unattended (--force does not skip it).
|
||||
Run it from an interactive shell (for example ssh -t root@<host> ...)."
|
||||
fi
|
||||
}
|
||||
|
||||
run_uninstall() {
|
||||
log_step "UNINSTALL — local host teardown"
|
||||
|
||||
@@ -1151,6 +1164,7 @@ run_uninstall() {
|
||||
log_dry "would prompt: Type the vmid ($vmid) to confirm PERMANENT destruction"
|
||||
else
|
||||
local ans
|
||||
_require_tty "the typed vmid confirmation"
|
||||
read -rp "Type the vmid ($vmid) to confirm PERMANENT destruction: " ans < /dev/tty
|
||||
[[ "$ans" == "$vmid" ]] || die "confirmation mismatch (got '$ans', expected '$vmid') — aborting, nothing destroyed"
|
||||
fi
|
||||
@@ -3268,8 +3282,13 @@ step_golden() {
|
||||
fi
|
||||
if $GOLDEN_VOLID_EXPLICIT; then
|
||||
# The operator named this archive. Never silently substitute a different one.
|
||||
# R-310: name the vouched version once — the "it is controller X, but the vouched golden
|
||||
# is Y" reason already says it.
|
||||
local _vouched_line=""
|
||||
[[ -n "$ART_GOLDEN_VER" && "$GOLDEN_CHECK_WHY" == *"$ART_GOLDEN_VER"* ]] \
|
||||
|| _vouched_line="The vouched golden is ${ART_GOLDEN_VER:-<unknown>}. "
|
||||
die "refusing the golden you named ($GOLDEN_VOLID): ${GOLDEN_CHECK_WHY}.
|
||||
The vouched golden is ${ART_GOLDEN_VER:-<unknown>}. Either pass the archive that matches it,
|
||||
${_vouched_line}Either pass the archive that matches it,
|
||||
or re-run with --force-gitea-golden to fetch the vouched one from Gitea."
|
||||
fi
|
||||
log_warn " ignoring the local golden $GOLDEN_VOLID — ${GOLDEN_CHECK_WHY}"
|
||||
|
||||
@@ -11,7 +11,7 @@ functions act on are variables the test points into the temp directory. Nothing
|
||||
Where behaviour cannot be isolated, a STATIC test checks the wiring (the function is CALLED, from the
|
||||
right place) — a helper defined and never called is the seam-built-never-wired shape.
|
||||
|
||||
Rows: R-275, R-276, R-881 (uninstall residue); R-306 (--preflight-only writes no state); R-130 (lvm minimum wording); R-180 (archive storage outside the ACL set); R-179 (NAS units).
|
||||
Rows: R-275, R-276, R-881 (uninstall residue); R-306 (--preflight-only writes no state); R-130 (lvm minimum wording); R-180 (archive storage outside the ACL set); R-179 (NAS units); R-310 (golden refusal wording, the uninstall's terminal).
|
||||
Run: python3 scripts/test_hostinstall.py
|
||||
"""
|
||||
import os
|
||||
@@ -515,6 +515,79 @@ def test_net_unit_marker_matches_the_agent():
|
||||
assert a and a.group(1) == m.group(1), "installer marker %r != agent netUnitMarker %r" % (m.group(1), a and a.group(1))
|
||||
|
||||
|
||||
# ── R-310: the golden refusal names the vouched version once; no terminal is a refusal, in words ──
|
||||
GOLDEN_RUN = """
|
||||
golden_local_matches_manifest() { GOLDEN_CHECK_WHY="%s"; return 1; }
|
||||
resolve_artifacts() { :; }
|
||||
_state_mark() { :; }
|
||||
GOLDEN_VOLID="local:backup/vzdump-lxc-9100-2026_08_03-07_33_00.tar.zst"; GOLDEN_VOLID_EXPLICIT=true
|
||||
FORCE_GITEA_GOLDEN=false; ART_GOLDEN_VER="0.213.0"
|
||||
step_golden
|
||||
echo REACHED
|
||||
"""
|
||||
|
||||
|
||||
def _golden_refusal(why):
|
||||
sb = Sandbox()
|
||||
try:
|
||||
rc, out = sb.run(prelude() + func("step_golden") + GOLDEN_RUN % why)
|
||||
assert rc != 0 and "REACHED" not in out and "refusing the golden you named" in out, out
|
||||
return out
|
||||
finally:
|
||||
sb.close()
|
||||
|
||||
|
||||
def test_golden_refusal_names_the_vouched_version_once():
|
||||
out = _golden_refusal("it is controller 0.192.0, but the vouched golden is 0.213.0")
|
||||
assert out.count("0.213.0") == 1, "the vouched version is stated %d times:\n%s" % (out.count("0.213.0"), out)
|
||||
|
||||
|
||||
def test_golden_refusal_still_names_the_version_when_the_reason_does_not():
|
||||
out = _golden_refusal("the archive could not be resolved to a file on disk")
|
||||
assert "The vouched golden is 0.213.0." in out, out
|
||||
|
||||
|
||||
def _tty_run(with_tty):
|
||||
import fcntl
|
||||
import termios
|
||||
script = prelude() + func("_require_tty") + '_require_tty "the typed vmid confirmation"\necho PASSED\n'
|
||||
if not with_tty:
|
||||
p = subprocess.run(["bash", "-c", script], capture_output=True, text=True, stdin=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
import pty
|
||||
master, slave = pty.openpty()
|
||||
|
||||
def ctty():
|
||||
os.setsid()
|
||||
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
|
||||
try:
|
||||
p = subprocess.run(["bash", "-c", script], capture_output=True, text=True, stdin=slave, preexec_fn=ctty)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
finally:
|
||||
os.close(master); os.close(slave)
|
||||
|
||||
|
||||
def test_require_tty_refuses_in_words_without_a_terminal():
|
||||
rc, out = _tty_run(False)
|
||||
assert rc != 0 and "PASSED" not in out, out
|
||||
assert "needs a terminal" in out and "nothing was destroyed" in out, out
|
||||
assert "No such device" not in out, out
|
||||
|
||||
|
||||
def test_require_tty_passes_with_a_terminal():
|
||||
# the control: with a controlling terminal the guard lets the prompt happen.
|
||||
rc, out = _tty_run(True)
|
||||
assert rc == 0 and "PASSED" in out, out
|
||||
|
||||
|
||||
def test_require_tty_guards_the_uninstall_prompt():
|
||||
body = func("run_uninstall")
|
||||
g = body.find('_require_tty "the typed vmid confirmation"')
|
||||
r = body.find('read -rp "Type the vmid')
|
||||
assert g > 0 and g < r, "the vmid confirmation is not guarded by _require_tty"
|
||||
|
||||
|
||||
def main():
|
||||
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
||||
fails = 0
|
||||
|
||||
Reference in New Issue
Block a user