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.
134 lines
5.9 KiB
Python
134 lines
5.9 KiB
Python
"""Configuration snapshots and compensation. No authentication state is copied."""
|
|
import copy
|
|
from application import services as svc, firewall as fw
|
|
from modules.services.models import STATUS
|
|
from modules.firewall.models import FirewallSettings
|
|
|
|
METRICS = {"blocked", "filtering_since", "over_limit_hits", "over_limit_first", "over_limit_last"}
|
|
|
|
|
|
def configuration(value):
|
|
if isinstance(value, dict):
|
|
return {k: configuration(v) for k, v in value.items() if k not in METRICS}
|
|
if isinstance(value, list):
|
|
return [configuration(v) for v in value]
|
|
if isinstance(value, bool):
|
|
return int(value)
|
|
return value
|
|
|
|
|
|
def public(value):
|
|
if isinstance(value, dict):
|
|
return {k: ("[redacted]" if v else None) if k in {"tls_key", "tls_cert", "password", "access_token"}
|
|
else public(v) for k, v in value.items()}
|
|
if isinstance(value, list):
|
|
return [public(v) for v in value]
|
|
return value
|
|
|
|
|
|
def service_snapshot(service_id):
|
|
svc.firewall.flush_blocks()
|
|
rows = svc.db.query("SELECT * FROM services WHERE service_id=?", service_id)
|
|
result = {"scope": "service", "service_id": service_id, "service": rows[0] if rows else None,
|
|
"addresses": [], "filters": [], "regexes": [], "functions": [], "code": {}}
|
|
if not rows:
|
|
return result
|
|
result["addresses"] = svc.db.query("SELECT * FROM service_addresses WHERE service_id=? ORDER BY address_id", service_id)
|
|
result["filters"] = svc.db.query("SELECT * FROM filters WHERE service_id=? ORDER BY position", service_id)
|
|
for row in result["filters"]:
|
|
fid = row["filter_id"]
|
|
result["regexes"] += svc.db.query("SELECT * FROM regexes WHERE filter_id=? ORDER BY regex_id", fid)
|
|
result["functions"] += svc.db.query("SELECT * FROM pyfilters WHERE filter_id=? ORDER BY position", fid)
|
|
if row["kind"] == "pyfilter":
|
|
result["code"][fid] = svc.read_code(fid)
|
|
return result
|
|
|
|
|
|
def firewall_snapshot():
|
|
policy = fw.firewall.policy
|
|
return {"scope": "firewall", "rules": svc_copy_rules(), "policy": getattr(policy, "value", policy),
|
|
"settings": fw.firewall.settings.model_dump(mode="json"), "enabled": fw.firewall.enabled}
|
|
|
|
|
|
def svc_copy_rules():
|
|
return [{k: v for k, v in r.items() if k != "rule_id"}
|
|
for r in fw.db.query("SELECT * FROM rules ORDER BY rule_id")]
|
|
|
|
|
|
def snapshot(scope, service_id=None):
|
|
if scope == "service":
|
|
return service_snapshot(service_id)
|
|
if scope == "firewall":
|
|
return firewall_snapshot()
|
|
return {"scope": "instance", "firewall": firewall_snapshot(), "services": [
|
|
service_snapshot(r["service_id"]) for r in svc.db.query("SELECT service_id FROM services ORDER BY service_id")]}
|
|
|
|
|
|
def insert_row(table, row):
|
|
# Tables and columns only come from our own snapshots/prepared candidates.
|
|
columns = list(row)
|
|
return (f'INSERT INTO "{table}" ({", ".join(chr(34)+k+chr(34) for k in columns)}) '
|
|
f'VALUES ({", ".join("?" for _ in columns)})', *[row[k] for k in columns])
|
|
|
|
|
|
def write_service_data(value):
|
|
"""Replace this service's rows, preserving live counters for identities that survive."""
|
|
value = copy.deepcopy(value)
|
|
sid = value["service_id"]
|
|
current = service_snapshot(sid)
|
|
tables = {"addresses": "service_addresses", "filters": "filters", "regexes": "regexes", "functions": "pyfilters"}
|
|
for key in ("filters", "regexes", "functions"):
|
|
identity = lambda r: (r.get("filter_id"), r.get("regex_id"), r.get("name") if key == "functions" else None)
|
|
old = {identity(r): r for r in current[key]}
|
|
for row in value[key]:
|
|
if identity(row) in old:
|
|
for metric in METRICS & row.keys():
|
|
row[metric] = old[identity(row)].get(metric, row[metric])
|
|
if current["service"] and value["service"]:
|
|
for metric in METRICS & value["service"].keys():
|
|
value["service"][metric] = current["service"].get(metric)
|
|
queries = [
|
|
("DELETE FROM regexes WHERE filter_id IN (SELECT filter_id FROM filters WHERE service_id=?)", sid),
|
|
("DELETE FROM pyfilters WHERE filter_id IN (SELECT filter_id FROM filters WHERE service_id=?)", sid),
|
|
("DELETE FROM filters WHERE service_id=?", sid),
|
|
("DELETE FROM service_addresses WHERE service_id=?", sid),
|
|
("DELETE FROM services WHERE service_id=?", sid),
|
|
]
|
|
if value["service"]:
|
|
queries.append(insert_row("services", value["service"]))
|
|
for key, table in tables.items():
|
|
queries.extend(insert_row(table, row) for row in value[key])
|
|
svc.db.queries(queries)
|
|
for fid in current["code"].keys() - value["code"].keys():
|
|
svc.clear_code(fid)
|
|
for fid, source in value["code"].items():
|
|
svc.write_code(fid, source)
|
|
|
|
|
|
async def restore(value):
|
|
if value["scope"] == "instance":
|
|
wanted = {s["service_id"] for s in value["services"]}
|
|
for row in svc.db.query("SELECT service_id FROM services"):
|
|
if row["service_id"] not in wanted:
|
|
await svc.delete_service(row["service_id"])
|
|
for service in value["services"]:
|
|
await restore(service)
|
|
await restore(value["firewall"])
|
|
elif value["scope"] == "firewall":
|
|
fw._write_rules(value["rules"])
|
|
fw.firewall.policy = value["policy"]
|
|
fw.firewall.settings = FirewallSettings(**value["settings"])
|
|
fw.firewall.enabled = value["enabled"]
|
|
await fw.firewall.reload()
|
|
await fw.refresh_frontend()
|
|
else:
|
|
sid = value["service_id"]
|
|
await svc.firewall.remove(sid, persist=False)
|
|
write_service_data(value)
|
|
if value["service"]:
|
|
await svc.firewall.reload()
|
|
manager = svc.firewall.get(sid)
|
|
if value["service"]["status"] == STATUS.ACTIVE and not manager.active:
|
|
raise RuntimeError("Restored configuration could not start its datapath")
|
|
await svc.refresh_frontend()
|