feat: add config write API (POST /api/0/config, POST /api/0/config/rollback)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1075,6 +1075,92 @@ async def start(
|
||||
backups = configio_mod.list_backups(_config_path)
|
||||
return web.json_response({"backups": backups})
|
||||
|
||||
async def api_config_post(request):
|
||||
"""POST /api/0/config — publish staged changes to .hb.yaml. Admin only."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
if user and not user.admin:
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
if not _config_path:
|
||||
return web.json_response({"error": "Config path not available"}, status=503)
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
try:
|
||||
data = configio_mod.read_roundtrip(_config_path)
|
||||
|
||||
if "server" in payload:
|
||||
configio_mod.apply_structured_section(data, "server", payload["server"])
|
||||
|
||||
if "users" in payload:
|
||||
# Hash any plaintext passwords; preserve existing hashes when omitted or "•••"
|
||||
existing_users = data.get("users") or {}
|
||||
users_payload = payload["users"]
|
||||
for username, attrs in users_payload.items():
|
||||
pw = attrs.get("password", "")
|
||||
if pw and pw != "•••" and not pw.startswith("pbkdf2:"):
|
||||
attrs["password"] = users_mod.hash_password(pw)
|
||||
elif not pw or pw == "•••":
|
||||
existing_hash = (existing_users.get(username) or {}).get("password", "")
|
||||
if existing_hash:
|
||||
attrs["password"] = existing_hash
|
||||
else:
|
||||
attrs.pop("password", None)
|
||||
configio_mod.apply_structured_section(data, "users", users_payload)
|
||||
|
||||
if "oauth" in payload:
|
||||
data["oauth"] = payload["oauth"]
|
||||
|
||||
for section in ("notification_channels", "thresholds", "hosts", "dns"):
|
||||
if section in payload:
|
||||
configio_mod.apply_yaml_section(data, section, payload[section])
|
||||
|
||||
configio_mod.write_config(_config_path, data)
|
||||
except Exception as exc:
|
||||
logger.error("Config write failed: %s", exc)
|
||||
return web.json_response({"error": str(exc)}, status=500)
|
||||
|
||||
if hasattr(config, "reload"):
|
||||
await config.reload()
|
||||
users_mod.load_users(config)
|
||||
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
async def api_config_rollback(request):
|
||||
"""POST /api/0/config/rollback — restore a backup. Admin only."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
if user and not user.admin:
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
if not _config_path:
|
||||
return web.json_response({"error": "Config path not available"}, status=503)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
backup = body.get("backup", "")
|
||||
expected_prefix = _config_path + ".bak."
|
||||
if not backup or not backup.startswith(expected_prefix) or not os.path.exists(backup):
|
||||
return web.json_response({"error": "Invalid or missing backup"}, status=400)
|
||||
|
||||
try:
|
||||
backup_data = configio_mod.read_roundtrip(backup)
|
||||
configio_mod.write_config(_config_path, backup_data)
|
||||
except Exception as exc:
|
||||
logger.error("Rollback failed: %s", exc)
|
||||
return web.json_response({"error": str(exc)}, status=500)
|
||||
|
||||
if hasattr(config, "reload"):
|
||||
await config.reload()
|
||||
users_mod.load_users(config)
|
||||
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
app = web.Application()
|
||||
app.add_routes(
|
||||
[
|
||||
@@ -1096,6 +1182,8 @@ async def start(
|
||||
web.get("/api/0/config", api_config_get),
|
||||
web.get("/api/0/config/section/{name}", api_config_section_get),
|
||||
web.get("/api/0/config/backups", api_config_backups_get),
|
||||
web.post("/api/0/config", api_config_post),
|
||||
web.post("/api/0/config/rollback", api_config_rollback),
|
||||
# Hosts
|
||||
web.get("/api/0/hosts", api_hosts),
|
||||
web.get("/api/0/alert_summary", api_alert_summary),
|
||||
|
||||
Reference in New Issue
Block a user