mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 09:14:33 +00:00
fix(release): boxapply validate-first + rollback-only-on-success, no leaked temp files
Rewrite apply_4r() to call sources_line(ring) first (fail-closed on invalid ring, no stray .shadow file). Only create .rollback copy after successful validation, leaving no temp files on failure path. Removes unused shutil import. Adds 3 regression tests: invalid_ring_no_shadow, failure_no_rollback, and fn_raise_failclosed. 6 boxapply tests + 2 repo tests (8 total) PASSING. Ref release-rings Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
50367801f0
commit
0c195c8701
|
|
@ -12,7 +12,6 @@ on failure, so a bad ring never bricks the box.
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from annuaire.model import RINGS # ring names
|
||||
|
||||
|
|
@ -31,21 +30,22 @@ def sources_line(ring: str) -> str:
|
|||
|
||||
|
||||
def apply_4r(ring, target_path, apt_update_fn) -> dict:
|
||||
line = sources_line(ring) # validate ring BEFORE touching any file
|
||||
shadow = target_path + ".shadow"
|
||||
rollback = target_path + ".rollback"
|
||||
prior = None
|
||||
if os.path.exists(target_path):
|
||||
prior = open(target_path).read()
|
||||
shutil.copy(target_path, rollback)
|
||||
with open(target_path) as fh:
|
||||
prior = fh.read()
|
||||
with open(shadow, "w") as fh:
|
||||
fh.write(sources_line(ring) + "\n")
|
||||
fh.write(line + "\n")
|
||||
ok = False
|
||||
try:
|
||||
ok = bool(apt_update_fn(shadow))
|
||||
except Exception:
|
||||
ok = False
|
||||
if not ok:
|
||||
# restore prior ring; drop the shadow
|
||||
# restore the prior ring; drop the shadow — leave no stray temp files
|
||||
if prior is not None:
|
||||
with open(target_path, "w") as fh:
|
||||
fh.write(prior)
|
||||
|
|
@ -54,5 +54,9 @@ def apply_4r(ring, target_path, apt_update_fn) -> dict:
|
|||
except OSError:
|
||||
pass
|
||||
raise ApplyError(f"apt validation failed for ring {ring!r}; restored prior")
|
||||
# success: keep a rollback copy of the prior, then atomic swap
|
||||
if prior is not None:
|
||||
with open(rollback, "w") as fh:
|
||||
fh.write(prior)
|
||||
os.replace(shadow, target_path) # atomic
|
||||
return {"ring": ring, "applied": True}
|
||||
|
|
|
|||
|
|
@ -27,3 +27,32 @@ def test_apply_4r_rolls_back_on_apt_failure(tmp_path):
|
|||
bx.apply_4r("internal", str(target), apt_update_fn=lambda p: False)
|
||||
# prior 'published' ring preserved — box not bricked
|
||||
assert "published" in target.read_text()
|
||||
|
||||
|
||||
def test_apply_4r_invalid_ring_leaves_no_shadow(tmp_path):
|
||||
target = tmp_path / "secubox-ring.list"
|
||||
target.write_text("deb https://apt.secubox.in published main contrib\n")
|
||||
with pytest.raises(ValueError):
|
||||
bx.apply_4r("prod", str(target), apt_update_fn=lambda p: True)
|
||||
assert not (tmp_path / "secubox-ring.list.shadow").exists()
|
||||
assert "published" in target.read_text() # live ring untouched
|
||||
|
||||
|
||||
def test_apply_4r_failure_leaves_no_rollback(tmp_path):
|
||||
target = tmp_path / "secubox-ring.list"
|
||||
target.write_text("deb https://apt.secubox.in published main contrib\n")
|
||||
with pytest.raises(bx.ApplyError):
|
||||
bx.apply_4r("internal", str(target), apt_update_fn=lambda p: False)
|
||||
assert not (tmp_path / "secubox-ring.list.rollback").exists()
|
||||
assert not (tmp_path / "secubox-ring.list.shadow").exists()
|
||||
assert "published" in target.read_text()
|
||||
|
||||
|
||||
def test_apply_4r_fn_raising_is_failclosed(tmp_path):
|
||||
def boom(p):
|
||||
raise RuntimeError("apt exploded")
|
||||
target = tmp_path / "secubox-ring.list"
|
||||
target.write_text("deb https://apt.secubox.in published main contrib\n")
|
||||
with pytest.raises(bx.ApplyError):
|
||||
bx.apply_4r("internal", str(target), apt_update_fn=boom)
|
||||
assert "published" in target.read_text() # fail-closed, prior preserved
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user