enroll/enroll/jinjaturtle.py
Miguel Jacq 8daed96b7c
All checks were successful
CI / test (push) Successful in 8m13s
Lint / test (push) Successful in 31s
Trivy / test (push) Successful in 23s
Attempt to generate Jinja2 templates of systemd unit files and Postfix main.cf (now that JinjaTurtle supports it)
2026-01-06 12:47:12 +11:00

126 lines
3 KiB
Python

from __future__ import annotations
import shutil
import subprocess # nosec
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
SYSTEMD_SUFFIXES = {
".service",
".socket",
".target",
".timer",
".path",
".mount",
".automount",
".slice",
".swap",
".scope",
".link",
".netdev",
".network",
}
SUPPORTED_SUFFIXES = {
".ini",
".cfg",
".json",
".toml",
".yaml",
".yml",
".xml",
".repo",
} | SYSTEMD_SUFFIXES
def infer_other_formats(dest_path: str) -> Optional[str]:
p = Path(dest_path)
name = p.name.lower()
suffix = p.suffix.lower()
# postfix
if name == "main.cf":
return "postfix"
# systemd units
if suffix in SYSTEMD_SUFFIXES:
return "systemd"
return None
@dataclass(frozen=True)
class JinjifyResult:
template_text: str
vars_text: str # YAML mapping text (no leading --- expected)
def find_jinjaturtle_cmd() -> Optional[str]:
"""Return the executable path for jinjaturtle if found on PATH."""
return shutil.which("jinjaturtle")
def can_jinjify_path(dest_path: str) -> bool:
p = Path(dest_path)
suffix = p.suffix.lower()
if infer_other_formats(dest_path):
return True
# allow unambiguous structured formats
if suffix in SUPPORTED_SUFFIXES:
return True
return False
def run_jinjaturtle(
jt_exe: str,
src_path: str,
*,
role_name: str,
force_format: Optional[str] = None,
) -> JinjifyResult:
"""
Run jinjaturtle against src_path and return (template, defaults-yaml).
Uses tempfiles and captures outputs.
jinjaturtle CLI:
jinjaturtle <config> -r <role> [-f <format>] [-d <defaults-output>] [-t <template-output>]
"""
src = Path(src_path)
if not src.is_file():
raise FileNotFoundError(src_path)
with tempfile.TemporaryDirectory(prefix="enroll-jt-") as td:
td_path = Path(td)
defaults_out = td_path / "defaults.yml"
template_out = td_path / "template.j2"
cmd = [
jt_exe,
str(src),
"-r",
role_name,
"-d",
str(defaults_out),
"-t",
str(template_out),
]
if force_format:
cmd.extend(["-f", force_format])
p = subprocess.run(cmd, text=True, capture_output=True) # nosec
if p.returncode != 0:
raise RuntimeError(
"jinjaturtle failed for %s (role=%s)\ncmd=%r\nstdout=%s\nstderr=%s"
% (src_path, role_name, cmd, p.stdout, p.stderr)
)
vars_text = defaults_out.read_text(encoding="utf-8").strip()
template_text = template_out.read_text(encoding="utf-8")
# jinjaturtle outputs a YAML mapping; strip leading document marker if present
if vars_text.startswith("---"):
vars_text = "\n".join(vars_text.splitlines()[1:]).lstrip()
return JinjifyResult(
template_text=template_text, vars_text=vars_text.rstrip() + "\n"
)