75b86fd60d
acme:
- acme.sh chmods its tree to owner-only (700/600) every run, which
broke the two-user model: a tree left owner-only by one user made
every acme.sh call of the other exit 2
- normalize_acme_home() reopens group access (sudo chmod g+rwX,
files only — setgid dirs trip RestrictSUIDSGID); _run_acme_preflight
is the choke point before every daemon acme.sh call + startup
- acme service now runs as the daemon user; --log persists the raw CA
transcript; SYS_LOG=6 journals manual issue/renew runs
- timer daily-only: two runs/day landed inside ZeroSSL's 24h
validation backoff (Retry-After: 86400) — a permanent renewal lockout
- _collect_acme no longer raises on cert-list failure; reports
status.error (AcmeState.status) so the certs page can surface it
firewall: re-stamp the applied baseline on live zone mutations
(interfaces/services/rich-rules/masquerade/forward-ports) so cancel-all
reverts to post-mutation state, not a stale install-era snapshot;
set_masquerade syncs the declarative config for existing zones;
add_forward_port records toaddr only with toport
status: apply-all accepts {"force": true} (forwarded to the firewall
apply only); ApplyConfirm force checkbox; applyResultToasts() — the
errors map wins over the 200; ActionButton checks errors before the
success toast; dashboard uses ApplyConfirm
system_import: drift re-imports carry the existing apply-meta; first
import stamps the adopted content as applied (it is the running state)
— no phantom pending changes
nginx: get_config only re-saves when migration actually changed the
config (no more owner/mtime churn on every read)
install: repair mis-owned top-level system dirs (tmpfiles
unsafe-path-transition), warn with a full-repair command for deeper
mis-ownership
daemon/server: loop.get_exception_handler() (aiohttp API fix)
tests: 888 pytest + 24 node passing; ruff clean
114 lines
3.0 KiB
Python
114 lines
3.0 KiB
Python
"""Aggregate status API blueprint.
|
|
|
|
Exposed at /api/status/* and delegates all operations to vacuum-walld.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from flask import Blueprint, request
|
|
|
|
from daemon.client import get, post
|
|
from daemon.iface import (
|
|
GET_STATUS_PENDING,
|
|
GET_SYSTEM_METRICS,
|
|
POST_STATUS_APPLY_ALL,
|
|
POST_STATUS_CANCEL_ALL,
|
|
POST_STATUS_REFRESH,
|
|
)
|
|
from webui.api.common import _error, _ok
|
|
|
|
logger = logging.getLogger(__name__)
|
|
bp = Blueprint("status", __name__)
|
|
|
|
|
|
@bp.route("/pending", methods=["GET"])
|
|
def pending():
|
|
"""Retrieve aggregate pending changes across all subsystems.
|
|
|
|
Endpoint:
|
|
GET /api/status/pending
|
|
|
|
Returns:
|
|
JSON response with per-subsystem pending status and total change count.
|
|
"""
|
|
try:
|
|
return _ok(get(GET_STATUS_PENDING))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to get pending status: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@bp.route("/apply-all", methods=["POST"])
|
|
def apply_all():
|
|
"""Apply pending changes for all subsystems in dependency order.
|
|
|
|
Endpoint:
|
|
POST /api/status/apply-all
|
|
Body:
|
|
{"force": true} (optional) — overrides the firewall safety guards
|
|
(management lockout, interface coverage) for this apply.
|
|
|
|
Returns:
|
|
JSON response with applied subsystems list and any errors encountered.
|
|
"""
|
|
body = request.get_json(silent=True)
|
|
try:
|
|
return _ok(post(POST_STATUS_APPLY_ALL, body))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to apply all pending changes: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@bp.route("/cancel-all", methods=["POST"])
|
|
def cancel_all():
|
|
"""Revert pending changes for all subsystems to the last applied config.
|
|
|
|
Endpoint:
|
|
POST /api/status/cancel-all
|
|
|
|
Returns:
|
|
JSON response with the reverted subsystems, skipped subsystems
|
|
(label -> reason), and any errors encountered.
|
|
"""
|
|
try:
|
|
return _ok(post(POST_STATUS_CANCEL_ALL))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to cancel all pending changes: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@bp.route("/refresh", methods=["POST"])
|
|
def refresh():
|
|
"""Re-collect state from the daemon, optionally filtered by subsystem.
|
|
|
|
Endpoint:
|
|
POST /api/status/refresh
|
|
Body:
|
|
{"subsystems": ["firewall"]} or {} for all.
|
|
"""
|
|
body = request.get_json(silent=True) or {}
|
|
try:
|
|
return _ok(post(POST_STATUS_REFRESH, body))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to refresh state: %s", exc)
|
|
return _error(str(exc), 500)
|
|
|
|
|
|
@bp.route("/system-metrics", methods=["GET"])
|
|
def system_metrics():
|
|
"""Retrieve system-wide metrics.
|
|
|
|
Endpoint:
|
|
GET /api/status/system-metrics
|
|
|
|
Returns:
|
|
JSON response with CPU load, memory usage, and network traffic stats.
|
|
"""
|
|
try:
|
|
return _ok(get(GET_SYSTEM_METRICS))
|
|
except RuntimeError as exc:
|
|
logger.error("Failed to get system metrics: %s", exc)
|
|
return _error(str(exc), 500)
|