Files
apex.ermnvldmr.com/engine/lib/sys.py
T
deytenit 6aaef35780 fix: resolve 17 confirmed bash-parity drifts from port review
Adversarial parity review of every ported action against its bash source
confirmed 17 real behavioral drifts (of 22 claimed); all fixed here:

- tiers/chown: defaults were compositions/apex/tierN (one level too deep);
  now the tier roots. shared/ base reset is non-recursive again.
- compose: --dry-run was swallowed by argparse.REMAINDER; now stripped.
- host.write_file: rendered files land 0644 (tempfile's 0600 made fresh
  systemd units/crowdsec configs owner-only).
- systemd.deploy_units: top-level non-hidden files only.
- ufw: missing docker/host config dir exits 66; missing /etc/ufw target
  aborts; empty rules dir warns; dotfile .rules excluded.
- sync/packages: Telegram error-trap parity and failed notify exits 1.
  Same notify-failure exit for backup/run and sync/repository.
  repository regains the node-dir guard.
- tiers/useradd: no more secret-bearing .env.bak per run.
- tiers/link: EOF on confirm prompt declines and continues.
- extract-traefik-certs: sudo mkdir inside the notify envelope.
- generate-happ-subscriptions: realpath parent-exists guards + docker check.
- configure/crowdsec: bouncer install goes through install_packages.
- lint-docker-compose: malformed/unreadable anchor-IP inputs are lint errors.
- core compose: APEX_RESTIC_REPOSITORY now ?error; RESTIC_COMPRESSION
  per-node via APEX_RESTIC_COMPRESSION (default auto).
- sys.service_exists line-anchored; tpl docstring corrected.
2026-07-05 10:50:40 +03:00

96 lines
3.8 KiB
Python

# 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 any(line.startswith(f"{name}.service")
for line in (cp.stdout or "").splitlines())
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])