mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
fix(profiles): a condition-gated START is not an apply failure (no spurious rollback)
Some checks failed
License Headers / check (push) Has been cancelled
Some checks failed
License Headers / check (push) Has been cancelled
A unit whose systemd start-condition fails (e.g. hexo) can never go active, so enable --now succeeds but wait_state timed out → apply of any profile listing such a module rolled back. condition_failed() detects ConditionResult=no and _do_change/ _rollback treat that START as reached (the module is deliberately off here). Found applying the full profile from the panel. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
e2819f3d31
commit
ffff1b70f5
|
|
@ -190,3 +190,19 @@ def wait_state(m: Manifest, want_on: bool, *, observe=_observe,
|
||||||
if now() - start >= timeout:
|
if now() - start >= timeout:
|
||||||
return False
|
return False
|
||||||
sleep(poll)
|
sleep(poll)
|
||||||
|
|
||||||
|
|
||||||
|
def condition_failed(m: Manifest, run) -> bool:
|
||||||
|
"""True si l'unité systemd du module a une Condition= de démarrage qui
|
||||||
|
ÉCHOUE : `systemctl enable --now` réussit, mais systemd laisse alors l'unité
|
||||||
|
inactive VOLONTAIREMENT (le module ne doit pas tourner dans cet
|
||||||
|
environnement — arch, présence d'un chemin, etc.). Ce n'est PAS un échec
|
||||||
|
d'actionnement : attendre `active` sur une telle unité timeout à l'infini.
|
||||||
|
Ne concerne que les modules natifs (LXC/portail n'ont pas de Condition=)."""
|
||||||
|
if m.runtime != "native":
|
||||||
|
return False
|
||||||
|
for u in m.units:
|
||||||
|
rc, out = run(["systemctl", "show", u, "-p", "ConditionResult", "--value"])
|
||||||
|
if rc == 0 and out.strip() == "no":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from dataclasses import dataclass, field
|
||||||
|
|
||||||
from . import audit as _audit
|
from . import audit as _audit
|
||||||
from . import snapshot as _snapshot
|
from . import snapshot as _snapshot
|
||||||
from .actuate import ActuationError, actuate, wait_state
|
from .actuate import ActuationError, actuate, condition_failed, wait_state
|
||||||
from .diff import START, STOP, Change
|
from .diff import START, STOP, Change
|
||||||
from .manifest import Manifest
|
from .manifest import Manifest
|
||||||
|
|
||||||
|
|
@ -41,6 +41,12 @@ def _want_on(action: str) -> bool:
|
||||||
def _do_change(c: Change, m: Manifest, *, run, observe, routes_value, sleep, now,
|
def _do_change(c: Change, m: Manifest, *, run, observe, routes_value, sleep, now,
|
||||||
wait_timeout) -> None:
|
wait_timeout) -> None:
|
||||||
actuate(c, m, run=run, route_value=routes_value)
|
actuate(c, m, run=run, route_value=routes_value)
|
||||||
|
# Un START d'une unité dont la Condition= de démarrage échoue ne deviendra
|
||||||
|
# JAMAIS active (enable --now a réussi, systemd la laisse inactive par
|
||||||
|
# design). On ne l'attend pas (ça timeout) et on ne la traite PAS comme un
|
||||||
|
# échec : le module est délibérément off ici.
|
||||||
|
if c.action == START and condition_failed(m, run):
|
||||||
|
return
|
||||||
if not wait_state(m, _want_on(c.action), observe=observe, sleep=sleep, now=now,
|
if not wait_state(m, _want_on(c.action), observe=observe, sleep=sleep, now=now,
|
||||||
timeout=wait_timeout):
|
timeout=wait_timeout):
|
||||||
raise ActuationError(f"{c.id}: état non atteint (timeout)")
|
raise ActuationError(f"{c.id}: état non atteint (timeout)")
|
||||||
|
|
@ -109,8 +115,12 @@ def _rollback_applied(applied, manifests, snap, *, run, observe, sleep, clock,
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
actuate(rev, m, run=run, route_value=rv)
|
actuate(rev, m, run=run, route_value=rv)
|
||||||
if wait_state(m, want_on, observe=observe, sleep=sleep, now=clock,
|
# Même règle que le forward : re-démarrer une unité condition-gated
|
||||||
timeout=wait_timeout):
|
# ne la rend jamais active — c'est un succès, pas un timeout.
|
||||||
|
reached = (rev.action == START and condition_failed(m, run)) or \
|
||||||
|
wait_state(m, want_on, observe=observe, sleep=sleep, now=clock,
|
||||||
|
timeout=wait_timeout)
|
||||||
|
if reached:
|
||||||
_audit.record({"ts": now, "module": c.id, "action": rev.action,
|
_audit.record({"ts": now, "module": c.id, "action": rev.action,
|
||||||
"result": "rollback"}, path=audit_path)
|
"result": "rollback"}, path=audit_path)
|
||||||
rolled.append(c.id)
|
rolled.append(c.id)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
secubox-profiles (0.6.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Fix apply on condition-gated modules: a unit whose systemd start-condition
|
||||||
|
fails (e.g. hexo — ConditionResult=no) can never become active, so `enable
|
||||||
|
--now` succeeds but wait_state timed out → the whole apply spuriously
|
||||||
|
rolled back. Now a START of a condition-failed native unit is treated as
|
||||||
|
reached (the module is deliberately off in this environment), not a failure.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 19 Jul 2026 14:00:00 +0200
|
||||||
|
|
||||||
secubox-profiles (0.6.1-1~bookworm1) bookworm; urgency=medium
|
secubox-profiles (0.6.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Fix apply/rollback from the panel: the service runs ProtectSystem=strict, so a
|
* Fix apply/rollback from the panel: the service runs ProtectSystem=strict, so a
|
||||||
|
|
|
||||||
|
|
@ -140,3 +140,26 @@ def test_wait_state_times_out():
|
||||||
sleep=lambda s: None, now=iter([0, 10, 20, 31]).__next__,
|
sleep=lambda s: None, now=iter([0, 10, 20, 31]).__next__,
|
||||||
timeout=30, poll=10)
|
timeout=30, poll=10)
|
||||||
assert ok is False
|
assert ok is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_condition_failed_detects_gated_native_unit():
|
||||||
|
from api.actuate import condition_failed
|
||||||
|
|
||||||
|
def run(argv):
|
||||||
|
if argv[:2] == ["systemctl", "show"] and "ConditionResult" in argv:
|
||||||
|
return 0, "no\n" # systemd start-condition failed
|
||||||
|
return 0, ""
|
||||||
|
assert condition_failed(_m("hexo"), run) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_condition_failed_false_when_condition_ok():
|
||||||
|
from api.actuate import condition_failed
|
||||||
|
assert condition_failed(_m("x"), lambda a: (0, "yes\n")) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_condition_failed_false_for_lxc():
|
||||||
|
from api.actuate import condition_failed
|
||||||
|
# LXC/portal modules have no systemd start-condition; never gated.
|
||||||
|
called = []
|
||||||
|
condition_failed(_m("l", runtime="lxc", lxc="l"), lambda a: (called.append(a), (0, "no"))[1])
|
||||||
|
assert not called # short-circuits on runtime != native
|
||||||
|
|
|
||||||
|
|
@ -176,3 +176,24 @@ def test_rollback_to_restores_snapshot_state(tmp_path):
|
||||||
audit_path=tmp_path / "audit-dry.log", apply=False)
|
audit_path=tmp_path / "audit-dry.log", apply=False)
|
||||||
assert dry_rep.status == "planned"
|
assert dry_rep.status == "planned"
|
||||||
assert dry_calls == []
|
assert dry_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_of_condition_gated_native_module_is_not_a_failure(tmp_path):
|
||||||
|
# hexo-like: START issued, but its systemd start-condition fails so it never
|
||||||
|
# goes active. enable --now succeeded → not an actuation failure → no rollback.
|
||||||
|
calls = []
|
||||||
|
manifests = {"hexo": _m("hexo")}
|
||||||
|
plan = [Change("hexo", START, "", 50)]
|
||||||
|
|
||||||
|
def run(argv):
|
||||||
|
calls.append(argv)
|
||||||
|
if argv[:2] == ["systemctl", "show"] and "ConditionResult" in argv:
|
||||||
|
return 0, "no\n"
|
||||||
|
return 0, ""
|
||||||
|
|
||||||
|
rep = apply_plan(plan, manifests, {"hexo": Actual(enabled=False, active=False)},
|
||||||
|
run=run, observe=lambda m: Actual(enabled=True, active=False),
|
||||||
|
now="t", routes={}, snap_root=tmp_path,
|
||||||
|
audit_path=tmp_path / "audit.log", apply=True, wait_timeout=0)
|
||||||
|
assert rep.status == "applied" # NOT rolled_back
|
||||||
|
assert rep.changed == ["hexo"] and rep.failed == []
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user