GRM-8: feat: add AnsibleExecutor and i18n modules
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user