mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 14:31:00 +00:00
Expose 28 tools with shared REST operations, role-scoped bearer authentication, revisions, durable retries, snapshots, jobs and timed rollback. Add Docker verification harnesses and setup/workflow documentation in docs/mcp.md and inst.md. Validated with 330 unit tests, two consecutive 25-case MCP/import runs, and restart recovery; the full upstream regression run remains incomplete.
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""An edit refused by a rebuild, and a service that could not start at all.
|
|
|
|
Both used to leave the same thing behind: a service that was not running, while what the
|
|
operator was shown said otherwise. An edit whose rebuild failed was answered "refused" —
|
|
which reads as "nothing happened" — after the rebuild had already stopped the service. And
|
|
a service that could not come back at boot kept `active` in the database, which is what
|
|
the list showed, with nothing filtering it.
|
|
"""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from modules.services import firewall
|
|
from modules.services.models import STATUS, TRANSPORT, Service
|
|
|
|
|
|
def _link(name: str):
|
|
return SimpleNamespace(name=name, filter=SimpleNamespace(name=name, active=True))
|
|
|
|
|
|
class _Queued:
|
|
"""Stands in for the NFQUEUE layer: a new shape cannot be pushed, only rebuilt, and a
|
|
chain holding the filter called `broken` does not start."""
|
|
|
|
def __init__(self, started: list):
|
|
self.started = started
|
|
|
|
async def start(self, chain):
|
|
names = [link.name for link in chain]
|
|
self.started.append(names)
|
|
if "broken" in names:
|
|
raise Exception("the nfqueue binary rejected the filters")
|
|
return {}
|
|
|
|
async def reload(self, chain):
|
|
raise firewall.transports.ChainShapeChanged()
|
|
|
|
async def stop(self):
|
|
pass
|
|
|
|
async def retire(self, keep_filtering, then=None):
|
|
if then:
|
|
then()
|
|
|
|
|
|
class _Quiet:
|
|
"""The rules and the log: none of them is the question."""
|
|
|
|
def add(self, *args, **kwargs):
|
|
pass
|
|
|
|
def delete(self, *args, **kwargs):
|
|
return set()
|
|
|
|
def release_guards(self, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
class _Db:
|
|
def __init__(self):
|
|
self.written = []
|
|
|
|
def query(self, sql, *args):
|
|
if sql.startswith("UPDATE"):
|
|
self.written.append((sql, args))
|
|
return []
|
|
|
|
|
|
@pytest.fixture
|
|
def service(monkeypatch):
|
|
started = []
|
|
state = {"chain": ["ok"]}
|
|
monkeypatch.setattr(firewall, "nft", _Quiet())
|
|
monkeypatch.setattr(firewall, "log_for", lambda service_id: _Quiet())
|
|
monkeypatch.setattr(firewall.transports, "build", lambda srv, **kw: _Queued(started))
|
|
srv = Service(service_id="s", name="n", status=STATUS.STOP, proto="tcp",
|
|
transport=TRANSPORT.NFQUEUE)
|
|
db = _Db()
|
|
manager = firewall.ServiceManager(srv, db)
|
|
manager.chain = lambda: [_link(name) for name in state["chain"]]
|
|
return manager, state, started, db
|
|
|
|
|
|
def test_a_rebuild_the_edit_cannot_survive_starts_the_service_again_as_it_was(service):
|
|
manager, state, started, _ = service
|
|
|
|
async def run():
|
|
await manager.enable()
|
|
state["chain"] = ["ok", "broken"]
|
|
with pytest.raises(Exception):
|
|
await manager.update_chain(lambda: state.update(chain=["ok"]))
|
|
|
|
asyncio.run(run())
|
|
assert manager.active, "an edit that was refused left the service stopped"
|
|
assert started == [["ok"], ["ok", "broken"], ["ok"]], started
|
|
|
|
|
|
def test_the_undo_is_run_once_whoever_asks_for_it():
|
|
"""The manager runs it before starting the service again; the router's own `except`,
|
|
which has always run it, now arrives second."""
|
|
from routers.services import _once
|
|
|
|
ran = []
|
|
undo = _once(lambda: ran.append(1))
|
|
undo()
|
|
undo()
|
|
assert ran == [1]
|
|
_once(None)() # nothing to undo is not an error
|
|
|
|
|
|
def test_stopping_a_service_that_could_not_start_is_remembered(service):
|
|
"""It is not running, so stopping it changed nothing in the kernel — but it is still
|
|
a decision, and kept as `active` the next boot would try to start it again."""
|
|
manager, state, _, db = service
|
|
manager.srv.status = STATUS.ACTIVE # what the database says: meant to run
|
|
state["chain"] = ["broken"]
|
|
|
|
async def run():
|
|
with pytest.raises(Exception):
|
|
await manager.enable()
|
|
assert not manager.active
|
|
await manager.disable()
|
|
|
|
asyncio.run(run())
|
|
assert manager.srv.status == STATUS.STOP
|
|
assert any(args[0] == STATUS.STOP for _, args in db.written), db.written
|
|
|
|
|
|
def test_the_list_says_whether_a_service_is_running_not_whether_it_is_meant_to(monkeypatch):
|
|
import application.services as router
|
|
|
|
class Manager:
|
|
active = False
|
|
|
|
monkeypatch.setitem(router.firewall.services, "x", Manager())
|
|
assert router._running_status({"service_id": "x", "status": STATUS.ACTIVE}) == STATUS.STOP
|
|
Manager.active = True
|
|
assert router._running_status({"service_id": "x", "status": STATUS.STOP}) == STATUS.ACTIVE
|
|
assert router._running_status({"service_id": "nobody", "status": STATUS.ACTIVE}) \
|
|
== STATUS.ACTIVE, "a service the firewall does not know keeps what it stored"
|