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
* 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.
* 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

View file

@ -8,6 +8,10 @@ import shutil
import subprocess # nosec
import tarfile
import tempfile
import sys
import threading
import time
import itertools
import urllib.request
from contextlib import ExitStack
from dataclasses import dataclass
@ -21,6 +25,69 @@ from .pathfilter import PathFilter
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:
return datetime.now(tz=timezone.utc).isoformat()
@ -772,14 +839,40 @@ def enforce_old_harvest(
]
if tags:
cmd.extend(["--tags", ",".join(tags)])
p = subprocess.run(
cmd,
cwd=str(td_path),
env=env,
capture_output=True,
text=True,
check=False,
) # nosec
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(
cmd,
cwd=str(td_path),
env=env,
capture_output=True,
text=True,
check=False,
) # 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()

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "enroll"
version = "0.4.0"
version = "0.4.1"
description = "Enroll a server's running state retrospectively into Ansible"
authors = ["Miguel Jacq <mig@mig5.net>"]
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
Version: %{upstream_version}
@ -43,6 +43,8 @@ Enroll a server's running state retrospectively into Ansible.
%{_bindir}/enroll
%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}
- 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)