certs: async renewal via background task + status polling
- POST /acme/renew returns a request_id and spawns a background task (renew/deploy/refresh steps); dedups per-domain like issue - completes as "skipped" when acme.sh reports the renewal window has not been reached (no --force) - new GET /acme/renew/status endpoint (iface + handler + blueprint) - run acme.sh subprocesses off the event loop (asyncio.to_thread) in both issuance and renewal - ActionCell: busy/busyLabel props; certs page disables the Renew button and polls renewal status with toasts for success/skip/fail
This commit is contained in:
+32
-8
@@ -15,6 +15,7 @@ from daemon.iface import (
|
||||
GET_ACME_INFO,
|
||||
GET_ACME_ISSUE_STATUS,
|
||||
GET_ACME_LIST,
|
||||
GET_ACME_RENEW_STATUS,
|
||||
POST_ACME_ACCOUNT_REGISTER,
|
||||
POST_ACME_EMAIL,
|
||||
POST_ACME_ISSUE,
|
||||
@@ -144,19 +145,21 @@ def issue_status(request_id: str):
|
||||
|
||||
@bp.route("/<domain>/renew", methods=["POST"])
|
||||
def renew_bp(domain: str):
|
||||
"""POST /api/certs/<domain>/renew — renew an existing certificate.
|
||||
|
||||
Args:
|
||||
domain: Domain name whose certificate should be renewed.
|
||||
"""POST /api/certs/<domain>/renew — start an (async) certificate renewal.
|
||||
|
||||
Returns:
|
||||
Response confirming renewal or an error message.
|
||||
Response containing a renewal request ID (poll it at
|
||||
``/api/certs/renew/<request_id>``) or an error message.
|
||||
"""
|
||||
try:
|
||||
logger.info("Certificate renewal requested for '%s' via API", domain)
|
||||
post(POST_ACME_RENEW, {"domain": domain})
|
||||
logger.info("Certificate renewed for '%s'", domain)
|
||||
return _ok(None)
|
||||
result = post(POST_ACME_RENEW, {"domain": domain})
|
||||
logger.info(
|
||||
"Certificate renewal started for '%s' (id=%s)",
|
||||
domain,
|
||||
result.get("request_id"),
|
||||
)
|
||||
return _ok(result)
|
||||
except BadRequest as exc:
|
||||
logger.info("Cert renew for '%s' rejected: %s", domain, exc)
|
||||
return _error(str(exc), 400)
|
||||
@@ -165,6 +168,27 @@ def renew_bp(domain: str):
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/renew/<request_id>", methods=["GET"])
|
||||
def renew_status(request_id: str):
|
||||
"""GET /api/certs/renew/<request_id> — poll status of a certificate renewal.
|
||||
|
||||
Args:
|
||||
request_id: Renewal request identifier returned by renew_bp.
|
||||
|
||||
Returns:
|
||||
Response containing renewal status or an error message.
|
||||
"""
|
||||
try:
|
||||
result = get(GET_ACME_RENEW_STATUS, {"id": request_id})
|
||||
return _ok(result)
|
||||
except NotFound as exc:
|
||||
logger.info("Renewal request '%s' not found: %s", request_id, exc)
|
||||
return _error(str(exc), 404)
|
||||
except RuntimeError as exc:
|
||||
logger.error("Failed to get renewal status for '%s': %s", request_id, exc)
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/<domain>", methods=["DELETE"])
|
||||
def remove_bp(domain: str):
|
||||
"""DELETE /api/certs/<domain> — remove a certificate from ACME management.
|
||||
|
||||
Reference in New Issue
Block a user