rollback_to's own default wait_timeout=30.0 re-capped the standalone/panel
rollback path (cli.py's rollback --yes calls rollback_to without passing
wait_timeout), defeating the derived-timeout wiring on that path. Also adds
an integration test that only passes because the derived timeout (105s)
exceeds a flat 30s, using apply_plan's injected clock/sleep — the previous
happy-path test converged on the first probe and would have passed even
without the wiring.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The lite mass-apply test spuriously aborted because the actuator judged a
STOP/START by the command return code, not the observed state (metrics'
90s TimeoutStopSec > _run's 15s timeout -> rc=None -> spurious rollback).
Design: non-blocking commands + wait_state as sole arbiter (LXC via
lxc-info as root), wait_timeout derived per-module from systemd, _run
distinguishes TimeoutExpired from OSError, /apply reverts active on
rolled_back.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
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>
A sudo child inherits the service's read-only mount namespace (EROFS); systemd-run
runs the ctl in PID 1's context, outside the sandbox. Found in secubox-profiles 0.6.1.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The panel's sudo child inherited the service's read-only mount namespace →
EROFS writing the 4R snapshot (and it couldn't drive systemd/LXC). Wrap the CLI in
systemd-run --wait --pipe so it runs in PID 1's context, outside the sandbox. Sudoers
grants updated to the systemd-run-wrapped exact commands. Found on the first real
panel apply.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Final-review finding 1: _run_ctl_json parsed the --json report only after mapping
rc!=0 to 500, so a rolled_back apply showed a raw truncated string instead of the
report banner. Now parse first: a valid {status,...} report returns 200 and the panel
classifies applied-vs-failed. rc=3 (protected refusal, no report) still 409.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Task-4 review: /apply now takes {profile}, rewrites active + actuates under a shared
async lock (with /active + /rollback) so a concurrent tab can't make it apply the wrong
profile. Panel POSTs the previewed profile, previews into its own #basculeDiffList (poll
can't clobber it) + re-fetches the plan before confirm, and uses errorToast on any
non-success report. 157 tests.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
secubox-core.service is a Type=oneshot (mkdir+chown) that RemainAfterExit=yes. A hard
Requires= on ~108 units cascade-stops them all if core is restarted/fails (e.g. a
secubox-core package upgrade) — a thundering-herd outage. After= keeps the ordering;
Wants= keeps the soft dependency without the cascade. Prereq for mass native apply
(Phase 3). Scaffolds (new-module.sh/new-package.sh) updated so future units use Wants=.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Found in the lyrion board validation: lxc-update-config -n -x is not a key-set tool
(rc=1). Edit <lxcpath>/<name>/config atomically (lxcpath via lxc-config lxc.lxcpath,
/data/lxc on gk2). Set autostart=0 before lxc-stop to beat the watchdog revive race.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Finding 1: load_routes() only returns a set of domain names (for
portal_routed), so _cmd_apply/_cmd_rollback were feeding {} into
apply_plan/rollback_to's routes=, meaning snapshot.capture always
recorded route: None for portal modules and rollback could never
restore a stopped module's WAF route. Added observe.load_route_values()
(dict domain -> [host, port]) and wired the CLI to pass it instead.
Finding 2: json.JSONDecodeError is a ValueError, not caught by
apply_plan's/_rollback_applied's except (ActuationError, OSError)
tuples nor by cli.main()'s handlers — a corrupt routes/snapshot file
escaped as a raw traceback. Added ValueError to both tuples in
api/apply.py and a rc-2 handler in cli.main().
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
_rollback_applied reversed changes from the pre-apply snapshot without
re-checking m.protected, so a protected module started successfully by
the plan could be STOPped if a later module then failed — the exact
lockout the forward-plan guard exists to prevent. Skip (audit
skipped-protected) any reverse STOP against a protected module instead.
Also stop discarding wait_state's return value in the same function:
a rollback step that timed out was unconditionally recorded as
"rollback" and counted in rolled_back, falsely claiming recovery.
Now audits rollback-timeout and excludes it from rolled_back.
Added tests for both fixes plus a missing rollback_to() coverage test.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The LXC actuator only toggled the container, but is_on() reflects the host
systemd unit; STOP left the unit enabled+active for a dead container so
wait_state never converged and the orchestrator would roll back spuriously.
LXC START/STOP now also enable/disable the module's host units, container
first on START, host API first on STOP.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The webui/API runs unprivileged (secubox); operations touching root-owned config
or driving systemd/LXC/apps MUST delegate to the root helper secubox-<module>ctl
via scoped exact-command sudoers (shipped by the package), which acts as root and
audits each change. Documented in MODULE-COMPLIANCE (authoritative), MODULE-GUIDELINES
§7 (CTL), WEBUI-PANEL-GUIDELINES, and wiki/Architecture. Ref: secubox-cvectl fix.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>