feat: port engine lib modules (log, sys, tpl, host)

This commit is contained in:
deytenit committed 2026-07-04 21:23:48 +03:00
1 parent 12eac9772b
commit 72eec40ef3
6 files changed
+362

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
# engine/lib/host.py
"""ctx.host — idempotent host mutation: backup, write_file, inject_block, install_crontab.
Ports root_template_crontab (install + rollback) and the ufw block-injection logic
(root_template.sh _root_template_inject_rules_to_ufw_file), generalized + self-healing.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
import time
from typing import List, Optional, Sequence, Tuple
def apply_block(text: str, content: str, start: str, end: str,
anchor: Optional[str] = None,
old_pairs: Sequence[Tuple[str, str]] = ()) -> Tuple[str, bool]:
"""Pure transform: remove existing (start,end) and old_pairs blocks, insert a fresh
block after the first anchor line (or append if anchor is None). If an anchor is
given but not present, return (text, False) unchanged (matches bash skip behavior)."""
lines = text.splitlines()
if anchor is not None and not any(anchor in ln for ln in lines):
return text, False
pairs = [(start, end), *old_pairs]
stripped_starts = {s: e for s, e in pairs}
out: List[str] = []
skip_until: Optional[str] = None
for ln in lines:
if skip_until is None:
s = ln.strip()
if s in stripped_starts:
skip_until = stripped_starts[s]
continue
out.append(ln)
else:
if ln.strip() == skip_until:
skip_until = None
continue
block = [start, *content.splitlines(), end]
if anchor is None:
out.extend(block)
return "\n".join(out) + "\n", True
result: List[str] = []
inserted = False
for ln in out:
result.append(ln)
if not inserted and anchor in ln:
result.extend(block)
inserted = True
return "\n".join(result) + "\n", True
class Host:
def __init__(self, log, sys_):
self.log = log
self.sys = sys_
# --- read/write with sudo awareness ---
def _read(self, path: str, sudo: bool = False) -> str:
if sudo:
cp = self.sys.run(["sudo", "cat", path], check=True, capture=True)
return cp.stdout or ""
with open(path) as f:
return f.read()
def backup(self, path: str, sudo: bool = False) -> Optional[str]:
exists = (self.sys.ok(["sudo", "test", "-f", path]) if sudo
else os.path.isfile(path))
if not exists:
return None
bkp = f"{path}.{int(time.time())}.bak"
if sudo:
self.sys.sudo(["cp", "-a", path, bkp])
else:
shutil.copy2(path, bkp)
self.log.info(f"Backed up {path} -> {bkp}")
return bkp
def write_file(self, path: str, content: str, backup: bool = True,
dry_run: bool = False, sudo: bool = False) -> None:
if dry_run:
self.log.info(f"[DRY-RUN] would write {len(content)} bytes to {path}")
return
if backup:
self.backup(path, sudo=sudo)
tmp = tempfile.NamedTemporaryFile("w", delete=False)
tmp.write(content)
tmp.close()
if sudo:
self.sys.sudo(["mkdir", "-p", os.path.dirname(path)])
self.sys.sudo(["cp", tmp.name, path])
os.unlink(tmp.name)
else:
os.makedirs(os.path.dirname(path), exist_ok=True)
shutil.move(tmp.name, path)
self.log.success(f"Wrote {path}")
def inject_block(self, target: str, content: str, start: str, end: str,
anchor: Optional[str] = None,
old_pairs: Sequence[Tuple[str, str]] = (),
dry_run: bool = False, sudo: bool = True) -> bool:
exists = (self.sys.ok(["sudo", "test", "-f", target]) if sudo
else os.path.isfile(target))
if not exists:
self.log.error(f"Target file does not exist: {target}")
return False
text = self._read(target, sudo=sudo)
new_text, injected = apply_block(text, content, start, end, anchor, old_pairs)
if not injected:
self.log.warn(f"Anchor {anchor!r} not found in {target}; skipping.")
return False
if dry_run:
self.log.info(f"[DRY-RUN] would inject block into {target}")
return True
self.write_file(target, new_text, backup=True, sudo=sudo)
self.log.success(f"Block injected into {target}")
return True
def install_crontab(self, text: str, dry_run: bool = False) -> None:
"""Port of root_template_crontab: backup, install via `crontab -`, rollback."""
backup = subprocess.run(["crontab", "-l"], text=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
prev = backup.stdout if backup.returncode == 0 else ""
if not text.strip():
self.log.error("Rendered crontab is empty; aborting (previous kept).")
raise SystemExit(65) # EX_DATAERR
self.log.info("Rendered crontab preview:\n" + text)
if dry_run:
self.log.info("[DRY-RUN] would install the crontab above.")
return
res = subprocess.run(["crontab", "-"], input=text, text=True)
if res.returncode != 0:
self.log.error("crontab install failed; restoring previous.")
subprocess.run(["crontab", "-"], input=prev, text=True)
raise SystemExit(1)
self.log.success("Crontab installed.")
+19
View File
@@ -0,0 +1,19 @@
# engine/lib/log.py
"""ctx.log — [node] [action] [level] message (port of root_log_*)."""
from __future__ import annotations
import sys
class Log:
def __init__(self, node: str = "global", action: str = "core"):
self.node = node
self.action = action
def _emit(self, level: str, msg: str, stream=None) -> None:
print(f"[{self.node}] [{self.action}] [{level}] {msg}",
file=stream or sys.stdout, flush=True)
def info(self, msg): self._emit("INFO", msg)
def warn(self, msg): self._emit("WARN", msg)
def error(self, msg): self._emit("ERROR", msg, sys.stderr)
def success(self, msg): self._emit("SUCCESS", msg)
+94
View File
@@ -0,0 +1,94 @@
# engine/lib/sys.py
"""ctx.sys — command execution + systemd service management (port of root_sys.sh)."""
from __future__ import annotations
import shutil
import subprocess
import time
from typing import List, Optional, Sequence
class Sys:
def __init__(self, log):
self.log = log
# --- command execution ---
def run(self, cmd: Sequence[str], check: bool = True, capture: bool = False,
cwd: Optional[str] = None, env: Optional[dict] = None,
input: Optional[str] = None) -> subprocess.CompletedProcess:
return subprocess.run(list(cmd), check=check, cwd=cwd, env=env, text=True,
input=input,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None)
def sudo(self, cmd: Sequence[str], **kw) -> subprocess.CompletedProcess:
return self.run(["sudo", *cmd], **kw)
def ok(self, cmd: Sequence[str], **kw) -> bool:
"""Run, return True on exit 0, never raising."""
return self.run(cmd, check=False, **kw).returncode == 0
# --- systemd (port of root_sys_*) ---
def service_exists(self, name: str) -> bool:
cp = self.run(["systemctl", "list-unit-files", "--type=service"],
check=False, capture=True)
return f"{name}.service" in (cp.stdout or "")
def service_is_active(self, name: str) -> bool:
return self.ok(["systemctl", "is-active", "--quiet", name])
def service_is_enabled(self, name: str) -> bool:
return self.ok(["systemctl", "is-enabled", "--quiet", name])
def start_and_enable(self, name: str) -> bool:
if not self.service_exists(name):
self.log.warn(f"Service {name} does not exist on this system")
return False
if not self.service_is_active(name):
self.log.info(f"Starting {name} service...")
self.sudo(["systemctl", "start", name])
if not self.service_is_enabled(name):
self.log.info(f"Enabling {name} service...")
self.sudo(["systemctl", "enable", name])
return self.service_is_active(name)
def ensure_running(self, name: str) -> None:
"""Port of root_init_ensure_service_running: enable --now if not active."""
if not self.service_is_active(name):
self.log.info(f"Starting {name} service...")
self.sudo(["systemctl", "enable", "--now", name])
else:
self.log.info(f"{name} service already running.")
def restart(self, name: str) -> bool:
self.log.info(f"Restarting {name} service...")
self.sudo(["systemctl", "restart", name])
return self.service_is_active(name)
def reload(self, name: str, dry_run: bool = False) -> None:
if dry_run:
self.log.info(f"[DRY-RUN] systemctl reload {name}")
return
self.log.info(f"Reloading {name} configuration...")
self.sudo(["systemctl", "reload", name])
def daemon_reload(self) -> None:
self.sudo(["systemctl", "daemon-reload"])
def wait_for(self, name: str, timeout: int = 30, interval: int = 2) -> bool:
elapsed = 0
while elapsed < timeout:
if self.service_is_active(name):
return True
time.sleep(interval)
elapsed += interval
return False
def install_packages(self, packages: List[str]) -> None:
"""Port of root_init_install_packages (install if the binary is absent)."""
for pkg in packages:
if shutil.which(pkg):
self.log.info(f"{pkg} already installed.")
continue
self.log.info(f"Installing {pkg}...")
self.sudo(["apt-get", "update", "-y"])
self.sudo(["apt-get", "install", "-y", pkg])
+37
View File
@@ -0,0 +1,37 @@
# engine/lib/tpl.py
"""ctx.tpl — stdlib string.Template rendering (envsubst successor, SP2 decision #3).
safe_substitute matches envsubst's forgiving behavior: known $VAR/${VAR} replaced,
unknown left literal, and shell $(...) command-substitution survives untouched.
Loops/conditionals live in the python action, never in the template file.
"""
from __future__ import annotations
import os
from string import Template
from typing import Dict
class Tpl:
def __init__(self, configs_dir: str, log):
self.configs_dir = configs_dir
self.log = log
def _resolve(self, path: str) -> str:
return path if os.path.isabs(path) else os.path.join(self.configs_dir, path)
def render(self, path: str, vars: Dict[str, str]) -> str:
full = self._resolve(path)
with open(full) as f:
return Template(f.read()).safe_substitute(vars)
def render_dir(self, rel_dir: str, vars: Dict[str, str]) -> Dict[str, str]:
"""Render every file under configs/<rel_dir>. Returns {relpath: content}."""
base = self._resolve(rel_dir)
out: Dict[str, str] = {}
for dirpath, _dirs, files in os.walk(base):
for fn in sorted(files):
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, base).replace(os.sep, "/")
with open(full) as f:
out[rel] = Template(f.read()).safe_substitute(vars)
return out
+39
View File
@@ -0,0 +1,39 @@
# tests/test_host.py
import unittest
from engine.lib.host import apply_block
class TestApplyBlock(unittest.TestCase):
def test_insert_after_anchor(self):
text = "line1\n# End required lines\nline2\n"
out, injected = apply_block(text, "ruleA\nruleB",
"# START APEX.ERMNVLDMR.COM RULES",
"# END APEX.ERMNVLDMR.COM RULES",
anchor="# End required lines")
self.assertTrue(injected)
self.assertIn("# End required lines\n# START APEX.ERMNVLDMR.COM RULES\nruleA\nruleB\n# END APEX.ERMNVLDMR.COM RULES", out)
def test_self_heal_removes_old_and_new_blocks(self):
text = ("a\n# End required lines\n"
"# START ROOT.ERMNVLDMR.COM RULES\nold\n# END ROOT.ERMNVLDMR.COM RULES\n"
"# START APEX.ERMNVLDMR.COM RULES\nprev\n# END APEX.ERMNVLDMR.COM RULES\n"
"b\n")
out, injected = apply_block(text, "new",
"# START APEX.ERMNVLDMR.COM RULES",
"# END APEX.ERMNVLDMR.COM RULES",
anchor="# End required lines",
old_pairs=[("# START ROOT.ERMNVLDMR.COM RULES",
"# END ROOT.ERMNVLDMR.COM RULES")])
self.assertTrue(injected)
self.assertNotIn("old", out)
self.assertNotIn("prev", out)
self.assertEqual(out.count("# START APEX.ERMNVLDMR.COM RULES"), 1)
self.assertIn("new", out)
def test_anchor_missing_does_not_inject(self):
text = "a\nb\n"
out, injected = apply_block(text, "x", "S", "E", anchor="# End required lines")
self.assertFalse(injected)
self.assertEqual(out, text)
if __name__ == "__main__":
unittest.main()
+33
View File
@@ -0,0 +1,33 @@
# tests/test_tpl.py
import os, tempfile, unittest
from engine.lib.tpl import Tpl
from engine.lib.log import Log
class TestTpl(unittest.TestCase):
def setUp(self):
self.cfg = tempfile.mkdtemp()
self.tpl = Tpl(self.cfg, Log())
def _w(self, rel, body):
p = os.path.join(self.cfg, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, "w").write(body)
def test_render_substitutes_known_vars(self):
self._w("cron/crontab", "root=${APEX_TIER1} node=$APEX_NODE\n")
out = self.tpl.render("cron/crontab", {"APEX_TIER1": "/mnt/t1", "APEX_NODE": "daedalus"})
self.assertEqual(out, "root=/mnt/t1 node=daedalus\n")
def test_render_leaves_shell_command_substitution(self):
self._w("c", "x $(cat /f) ${APEX_NODE}\n")
out = self.tpl.render("c", {"APEX_NODE": "d"})
self.assertEqual(out, "x $(cat /f) d\n") # $(...) survives; ${APEX_NODE} replaced
def test_render_dir_returns_relpath_map(self):
self._w("fail2ban/jail.local", "a=${X}\n")
self._w("fail2ban/sub/extra.conf", "b=${Y}\n")
out = self.tpl.render_dir("fail2ban", {"X": "1", "Y": "2"})
self.assertEqual(out, {"jail.local": "a=1\n", "sub/extra.conf": "b=2\n"})
if __name__ == "__main__":
unittest.main()