status: cancel-all reverts pending changes to last applied config
- lib.common.revert_to_applied(): restore a config file from its
_last_applied_config snapshot (stamped hash); no baseline -> skip with
reason, file untouched
- firewall config_apply now stamps the applied baseline like the other
subsystems; GET /firewall/config and the state collector strip the
internal _last_applied_* keys
- POST /status/cancel-all + /api/status/cancel-all: revert pending
subsystems, {cancelled, skipped, errors}, partial-failure safe
- dashboard: "Cancel All Changes" button with confirm modal
(CancelConfirm, reuses the pending-changes modal rows); the pending
changes card is hidden entirely when nothing is pending
- tests: revert_to_applied, status_cancel_all, firewall stamping/meta
stripping, /api/status/cancel-all route, node tests for CancelConfirm;
firewall _config_apply tests no longer write the real repo config
- docs: api.md, state-model.md, config.md, hoover.md
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Tests for lib/firewall.py (pure logic) and daemon/handlers/firewall.py (privilege boundary)."""
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
@@ -7,6 +8,12 @@ import pytest
|
||||
from daemon.handlers import firewall as daemonfirewall
|
||||
from daemon.server import ConflictError, NotFoundError
|
||||
from lib import firewall
|
||||
from lib.common import (
|
||||
_APPLY_HASH_KEY,
|
||||
_LAST_APPLIED_CONFIG_KEY,
|
||||
config_hash,
|
||||
strip_apply_meta,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# lib/firewall.py — pure parsing (no sudo)
|
||||
@@ -531,6 +538,11 @@ class TestDaemonConfigApply:
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
patch(
|
||||
"daemon.handlers.firewall._get_config",
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall._save_config"),
|
||||
):
|
||||
result = daemonfirewall._config_apply()
|
||||
assert result["applied_zones"] == ["public"]
|
||||
@@ -661,10 +673,87 @@ class TestDaemonMgmtLockoutGuard:
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
patch(
|
||||
"daemon.handlers.firewall._get_config",
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall._save_config"),
|
||||
):
|
||||
result = daemonfirewall._config_apply(force=True)
|
||||
assert result["applied_zones"] == ["public"]
|
||||
|
||||
|
||||
_STAMP_TEST_CFG = {
|
||||
"zones": {
|
||||
"public": {
|
||||
"target": "DEFAULT",
|
||||
"interfaces": ["eth0"],
|
||||
"services": ["http"],
|
||||
"masquerade": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestDaemonConfigApplyStamp:
|
||||
"""Verify _config_apply records the applied baseline in the config file."""
|
||||
|
||||
ZONE_LIST_ALL_OUT = (
|
||||
"target: default\ninterfaces: \nsources: \nservices: \nports: \n"
|
||||
"protocols: \nforward-ports: \nmasquerade: no\nics: no\nrich-rules: \n"
|
||||
"icmp-blocks: \nmodule: \n"
|
||||
)
|
||||
|
||||
@patch(
|
||||
"lib.firewall.get_config",
|
||||
return_value=_STAMP_TEST_CFG,
|
||||
create=True,
|
||||
)
|
||||
@patch(
|
||||
"daemon.handlers.firewall.run",
|
||||
return_value=ZONE_LIST_ALL_OUT,
|
||||
)
|
||||
def test_stamps_applied_baseline(self, mock_run, mock_cfg):
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.firewall._save_backup",
|
||||
return_value="/tmp/rules.json",
|
||||
),
|
||||
patch(
|
||||
"daemon.handlers.firewall._get_state",
|
||||
return_value={"zones": {"public": {}}},
|
||||
),
|
||||
patch("daemon.handlers.firewall.refresh_state"),
|
||||
patch(
|
||||
"daemon.handlers.firewall._get_config",
|
||||
return_value=deepcopy(_STAMP_TEST_CFG),
|
||||
),
|
||||
patch("daemon.handlers.firewall._save_config") as mock_save,
|
||||
):
|
||||
result = daemonfirewall._config_apply()
|
||||
|
||||
assert result["applied_zones"] == ["public"]
|
||||
saved = mock_save.call_args[0][0]
|
||||
assert saved[_LAST_APPLIED_CONFIG_KEY] == strip_apply_meta(saved)
|
||||
assert saved[_APPLY_HASH_KEY] == config_hash(saved)
|
||||
# The snapshot is the applied (meta-stripped) config.
|
||||
assert saved[_LAST_APPLIED_CONFIG_KEY] == _STAMP_TEST_CFG
|
||||
|
||||
|
||||
class TestDaemonGetConfigEndpoint:
|
||||
def test_strips_apply_meta(self):
|
||||
with patch.object(
|
||||
daemonfirewall,
|
||||
"_get_config",
|
||||
return_value={
|
||||
"zones": {},
|
||||
_APPLY_HASH_KEY: "h",
|
||||
_LAST_APPLIED_CONFIG_KEY: {"zones": {}},
|
||||
},
|
||||
):
|
||||
result = daemonfirewall.get_config(None, None)
|
||||
assert result == {"zones": {}}
|
||||
|
||||
@patch(
|
||||
"daemon.handlers.firewall._config_apply",
|
||||
return_value={"applied_zones": ["public"], "backup": "/tmp/rules.json"},
|
||||
|
||||
Reference in New Issue
Block a user