fix: install.sh loop abort, ACME poll sudo gate, /static/ sub-paths
- install.sh: the traversal-chmod loop assigned _d but looped over the never-set $d; under set -u every fresh install aborted with "d: unbound variable" at that line. Loop over $_d. - acme collector: the self-heal normalize (sudo chmod g+rwX) now runs only when a no-sudo group-read-bit probe detects a lost bit — acme.sh re-hardens the tree 600 on every run, so the steady-state poll makes no sudo call. The group bit (not daemon readability) is what the two-user model keeps for the WebUI user. - lib.acme: new get_acme_home() accessor (ACME_HOME env, default data/acme), reused by _run_acme; _summarize_acme_output preserves a "Permission denied" line even when it is not among the final two, so the collector's actionable-error matcher keeps firing. - nginx template: emit location /static/ for any is_management path (not only '/'); the SPA references /static/... at the domain root regardless of the management backend path. - tests: probe, summarizer, and nginx-subpath cases in test_state.py, test_acme.py, test_nginx.py.
This commit is contained in:
+17
-3
@@ -32,6 +32,11 @@ _ACME_ENVIRON = {
|
||||
_WEBROOT = PROJECT_DIR / "data" / "acme" / "www"
|
||||
|
||||
|
||||
def get_acme_home() -> Path:
|
||||
"""Resolve the ACME home directory (``ACME_HOME`` env, default ``data/acme``)."""
|
||||
return Path(os.environ.get("ACME_HOME", str(_ACME_HOME)))
|
||||
|
||||
|
||||
def _find_acme() -> str:
|
||||
"""Locate the acme.sh binary on the system.
|
||||
|
||||
@@ -87,7 +92,7 @@ def _run_acme(args: list[str]) -> str:
|
||||
acme_bin = _find_acme()
|
||||
|
||||
# Check for ACME_HOME env var (set by systemd in production)
|
||||
acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
|
||||
acme_home_env = str(get_acme_home())
|
||||
|
||||
cmd: list[str] = [
|
||||
acme_bin,
|
||||
@@ -143,14 +148,22 @@ def _summarize_acme_output(output: str) -> str:
|
||||
in the final lines (e.g. "The retryafter=86400 value is too large
|
||||
(> 600), will not retry anymore."). Strips per-line timestamps and
|
||||
the "Please check log file" pointer so the summary stays toast-
|
||||
sized. The full transcript remains in the log and acme.sh.log.
|
||||
sized. A "Permission denied" diagnostic is preserved even when it
|
||||
is not among the final lines — the actionable-error matcher in
|
||||
daemon/collectors/acme.py keys off it. The full transcript remains
|
||||
in the log and acme.sh.log.
|
||||
"""
|
||||
lines = [line.strip() for line in output.strip().splitlines() if line.strip()]
|
||||
lines = [re.sub(r"^\[[^\]]*\] ", "", line) for line in lines]
|
||||
lines = [line for line in lines if not line.startswith("Please check log file")]
|
||||
if not lines:
|
||||
return "(no output)"
|
||||
return "; ".join(lines[-2:])
|
||||
tail = list(lines[-2:])
|
||||
for line in reversed(lines):
|
||||
if "Permission denied" in line and line not in tail:
|
||||
tail.insert(0, line)
|
||||
break
|
||||
return "; ".join(tail)
|
||||
|
||||
|
||||
def set_email(email: str) -> None:
|
||||
@@ -649,6 +662,7 @@ __all__ = [
|
||||
"days_until_expiry",
|
||||
"deploy",
|
||||
"find_cert_dir",
|
||||
"get_acme_home",
|
||||
"get_cert_info",
|
||||
"get_cert_paths",
|
||||
"get_email",
|
||||
|
||||
Reference in New Issue
Block a user