GRM-8: feat: add AnsibleExecutor and i18n modules

This commit is contained in:
Emil Simeonov
2026-06-18 20:16:50 +02:00
parent 6414f2306c
commit 564c12e782
3 changed files with 440 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Ansible execution with log capture."""
from __future__ import annotations
import os
import re
import subprocess
from datetime import datetime
from pathlib import Path
from .exceptions import AnsibleError
from .i18n import _
class AnsibleExecutor:
"""Executes Ansible playbooks, streaming output to log files."""
def __init__(self, log_dir: Path | None = None) -> None:
self._log_dir = log_dir or Path.home() / ".local" / "state" / "grm" / "logs"
def run(self, cmd: list[str], description: str = "") -> None:
"""Run an Ansible command, redirecting output to a log file."""
log_file = self._prepare_log(cmd)
desc = description or _("Running Ansible playbook")
print(f"[GRM] {desc}")
print(f"[GRM] {_('Full log: {log_file}', log_file=log_file)}")
returncode = self._stream(cmd, log_file)
if returncode != 0:
raise AnsibleError(
_(
"Ansible failed with exit code {code}. See full log: {log_file}",
code=returncode,
log_file=log_file,
)
)
status = self._extract_status(log_file)
if status:
print(f"[GRM] {status}")
print(f"[GRM] {_('Done. See full log: {log_file}', log_file=log_file)}")
def _prepare_log(self, cmd: list[str]) -> Path:
"""Create a log file with header."""
self._log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
log_file = self._log_dir / f"ansible-{timestamp}.log"
with open(log_file, "w") as f:
f.write(f"=== GRM Ansible Run: {timestamp} ===\n")
f.write(_("Command: {cmd}", cmd=" ".join(cmd)) + "\n\n")
return log_file
def _stream(self, cmd: list[str], log_file: Path) -> int:
"""Execute command, streaming stdout+stderr to log file. Returns exit code."""
env = os.environ.copy()
with open(log_file, "a") as f:
proc = subprocess.Popen(
cmd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
assert proc.stdout is not None
try:
for line in proc.stdout:
f.write(line)
f.flush()
finally:
proc.wait()
return proc.returncode
def _extract_status(self, log_file: Path) -> str | None:
"""Extract the runner status message from the Ansible log."""
try:
with open(log_file) as f:
content = f.read()
match = re.search(
r'TASK \[gitea-runner : Report runner status\].*?"msg":\s*"([^"]+)"',
content,
re.DOTALL,
)
if match:
return match.group(1).replace("\\n", " ").strip()
except Exception:
pass
return None