Add interactive output when 'enroll diff --enforce' is invoking Ansible.
All checks were successful
CI / test (push) Successful in 8m18s
Lint / test (push) Successful in 32s
Trivy / test (push) Successful in 24s

This commit is contained in:
Miguel Jacq 2026-01-11 10:01:16 +11:00
parent d172d848c4
commit 5754ef1aad
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
5 changed files with 114 additions and 11 deletions

View file

@ -1,3 +1,7 @@
# 0.4.1
* Add interactive output when 'enroll diff --enforce' is invoking Ansible.
# 0.4.0 # 0.4.0
* Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest. * Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest.

6
debian/changelog vendored
View file

@ -1,5 +1,9 @@
enroll (0.4.0) unstable; urgency=medium enroll (0.4.1) unstable; urgency=medium
* Add interactive output when 'enroll diff --enforce' is invoking Ansible.
-- Miguel Jacq <mig@mig5.net> Sun, 11 Jan 2026 10:00:00 +1100
enroll (0.4.0) unstable; urgency=medium
* Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest. * Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest.
* Attempt to generate Jinja2 templates of systemd unit files and Postfix main.cf (now that JinjaTurtle supports it) * Attempt to generate Jinja2 templates of systemd unit files and Postfix main.cf (now that JinjaTurtle supports it)
* Update pynacl dependency to resolve CVE-2025-69277 * Update pynacl dependency to resolve CVE-2025-69277

View file

@ -8,6 +8,10 @@ import shutil
import subprocess # nosec import subprocess # nosec
import tarfile import tarfile
import tempfile import tempfile
import sys
import threading
import time
import itertools
import urllib.request import urllib.request
from contextlib import ExitStack from contextlib import ExitStack
from dataclasses import dataclass from dataclasses import dataclass
@ -21,6 +25,69 @@ from .pathfilter import PathFilter
from .sopsutil import decrypt_file_binary_to, require_sops_cmd from .sopsutil import decrypt_file_binary_to, require_sops_cmd
def _progress_enabled() -> bool:
"""Return True if we should display interactive progress UI on the CLI.
We only emit progress when stderr is a TTY, so it won't pollute JSON/text reports
captured by systemd, CI, webhooks, etc. Users can also disable this explicitly via
ENROLL_NO_PROGRESS=1.
"""
if os.environ.get("ENROLL_NO_PROGRESS", "").strip() in {"1", "true", "yes"}:
return False
try:
return sys.stderr.isatty()
except Exception:
return False
class _Spinner:
"""A tiny terminal spinner with an elapsed-time counter (stderr-only)."""
def __init__(self, message: str, *, interval: float = 0.12) -> None:
self.message = message.rstrip()
self.interval = interval
self._stop = threading.Event()
self._thread: Optional[threading.Thread] = None
self._last_len = 0
self._start = 0.0
def start(self) -> None:
if self._thread is not None:
return
self._start = time.monotonic()
self._thread = threading.Thread(
target=self._run, name="enroll-spinner", daemon=True
)
self._thread.start()
def stop(self, final_line: Optional[str] = None) -> None:
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=1.0)
# Clear spinner line.
try:
sys.stderr.write("\r" + (" " * max(self._last_len, 0)) + "\r")
if final_line:
sys.stderr.write(final_line.rstrip() + "\n")
sys.stderr.flush()
except Exception:
pass # nosec
def _run(self) -> None:
frames = itertools.cycle("|/-\\")
while not self._stop.is_set():
elapsed = time.monotonic() - self._start
line = f"{self.message} {next(frames)} {elapsed:0.1f}s"
try:
sys.stderr.write("\r" + line)
sys.stderr.flush()
self._last_len = max(self._last_len, len(line))
except Exception:
return
self._stop.wait(self.interval)
def _utc_now_iso() -> str: def _utc_now_iso() -> str:
return datetime.now(tz=timezone.utc).isoformat() return datetime.now(tz=timezone.utc).isoformat()
@ -772,6 +839,22 @@ def enforce_old_harvest(
] ]
if tags: if tags:
cmd.extend(["--tags", ",".join(tags)]) cmd.extend(["--tags", ",".join(tags)])
spinner: Optional[_Spinner] = None
p: Optional[subprocess.CompletedProcess[str]] = None
t0 = time.monotonic()
if _progress_enabled():
if tags:
sys.stderr.write(
f"Enforce: running ansible-playbook (tags: {','.join(tags)})\n",
)
else:
sys.stderr.write("Enforce: running ansible-playbook\n")
sys.stderr.flush()
spinner = _Spinner(" ansible-playbook")
spinner.start()
try:
p = subprocess.run( p = subprocess.run(
cmd, cmd,
cwd=str(td_path), cwd=str(td_path),
@ -780,6 +863,16 @@ def enforce_old_harvest(
text=True, text=True,
check=False, check=False,
) # nosec ) # nosec
finally:
if spinner:
elapsed = time.monotonic() - t0
rc = p.returncode if p is not None else None
spinner.stop(
final_line=(
f"Enforce: ansible-playbook finished in {elapsed:0.1f}s"
+ (f" (rc={rc})" if rc is not None else ""),
),
)
finished_at = _utc_now_iso() finished_at = _utc_now_iso()

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "enroll" name = "enroll"
version = "0.4.0" version = "0.4.1"
description = "Enroll a server's running state retrospectively into Ansible" description = "Enroll a server's running state retrospectively into Ansible"
authors = ["Miguel Jacq <mig@mig5.net>"] authors = ["Miguel Jacq <mig@mig5.net>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View file

@ -1,4 +1,4 @@
%global upstream_version 0.4.0 %global upstream_version 0.4.1
Name: enroll Name: enroll
Version: %{upstream_version} Version: %{upstream_version}
@ -43,6 +43,8 @@ Enroll a server's running state retrospectively into Ansible.
%{_bindir}/enroll %{_bindir}/enroll
%changelog %changelog
* Sun Jan 11 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
- Add interactive output when 'enroll diff --enforce' is invoking Ansible.
* Sat Jan 10 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release} * Sat Jan 10 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
- Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest. - Introduce `enroll validate` - a tool to validate a harvest against the state schema, or check for missing or orphaned obsolete artifacts in a harvest.
- Attempt to generate Jinja2 templates of systemd unit files and Postfix main.cf (now that JinjaTurtle supports it) - Attempt to generate Jinja2 templates of systemd unit files and Postfix main.cf (now that JinjaTurtle supports it)