refactor: Move certificate config functions to file_ops.py

- Move load_certs_config, save_certs_config, add_cert_to_config,
  remove_cert_from_config from certificates.py to file_ops.py
- Add CERTS_FILE constant to config.py
- Add file locking for certificate config operations (was missing)
- Consistent pattern with servers.json handling

certificates.py: 543 → 503 lines
file_ops.py: 263 → 337 lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-02 04:26:55 +00:00
parent 79254835e9
commit 6ced2b42d4
3 changed files with 83 additions and 47 deletions

View File

@@ -27,6 +27,7 @@ STATE_FILE: str = os.getenv("HAPROXY_STATE_FILE", "/opt/haproxy/data/servers.sta
MAP_FILE: str = os.getenv("HAPROXY_MAP_FILE", "/opt/haproxy/conf/domains.map")
MAP_FILE_CONTAINER: str = os.getenv("HAPROXY_MAP_FILE_CONTAINER", "/usr/local/etc/haproxy/domains.map")
SERVERS_FILE: str = os.getenv("HAPROXY_SERVERS_FILE", "/opt/haproxy/conf/servers.json")
CERTS_FILE: str = os.getenv("HAPROXY_CERTS_FILE", "/opt/haproxy/conf/certificates.json")
# Pool configuration
POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100"))

View File

@@ -9,6 +9,7 @@ from typing import Any, Optional
from .config import (
MAP_FILE,
SERVERS_FILE,
CERTS_FILE,
logger,
)
from .validation import domain_to_backend
@@ -260,3 +261,77 @@ def remove_domain_from_config(domain: str) -> None:
save_servers_config(config)
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
# Certificate configuration functions
def load_certs_config() -> list[str]:
"""Load certificate domain list from JSON file.
Returns:
List of domain names
"""
try:
with open(CERTS_FILE, "r", encoding="utf-8") as f:
try:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
except OSError:
pass
try:
data = json.load(f)
return data.get("domains", [])
finally:
try:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
except OSError:
pass
except FileNotFoundError:
return []
except json.JSONDecodeError as e:
logger.warning("Corrupt certificates config %s: %s", CERTS_FILE, e)
return []
def save_certs_config(domains: list[str]) -> None:
"""Save certificate domain list to JSON file atomically.
Args:
domains: List of domain names
"""
atomic_write_file(CERTS_FILE, json.dumps({"domains": sorted(domains)}, indent=2))
def add_cert_to_config(domain: str) -> None:
"""Add a domain to the certificate config.
Args:
domain: Domain name to add
"""
lock_path = f"{CERTS_FILE}.lock"
with open(lock_path, 'w') as lock_file:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
try:
domains = load_certs_config()
if domain not in domains:
domains.append(domain)
save_certs_config(domains)
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def remove_cert_from_config(domain: str) -> None:
"""Remove a domain from the certificate config.
Args:
domain: Domain name to remove
"""
lock_path = f"{CERTS_FILE}.lock"
with open(lock_path, 'w') as lock_file:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
try:
domains = load_certs_config()
if domain in domains:
domains.remove(domain)
save_certs_config(domains)
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)

View File

@@ -1,6 +1,5 @@
"""Certificate management tools for HAProxy MCP Server."""
import json
import os
import subprocess
from datetime import datetime
@@ -11,14 +10,17 @@ from pydantic import Field
from ..config import logger, SUBPROCESS_TIMEOUT
from ..validation import validate_domain
from ..haproxy_client import haproxy_cmd
from ..file_ops import atomic_write_file
from ..file_ops import (
load_certs_config,
add_cert_to_config,
remove_cert_from_config,
)
# Certificate paths
ACME_SH = os.path.expanduser("~/.acme.sh/acme.sh")
ACME_HOME = os.path.expanduser("~/.acme.sh")
CERTS_DIR = "/opt/haproxy/certs"
CERTS_DIR_CONTAINER = "/etc/haproxy/certs"
CERTS_JSON = "/opt/haproxy/conf/certificates.json"
# Longer timeout for certificate operations (ACME can be slow)
CERT_TIMEOUT = 120
@@ -39,48 +41,6 @@ def get_pem_paths(domain: str) -> tuple[str, str]:
)
def load_cert_config() -> list[str]:
"""Load certificate domain list from JSON file.
Returns:
List of domain names
"""
try:
with open(CERTS_JSON, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("domains", [])
except FileNotFoundError:
return []
except json.JSONDecodeError as e:
logger.warning("Corrupt certificates.json: %s", e)
return []
def save_cert_config(domains: list[str]) -> None:
"""Save certificate domain list to JSON file atomically.
Args:
domains: List of domain names
"""
atomic_write_file(CERTS_JSON, json.dumps({"domains": sorted(domains)}, indent=2))
def add_cert_to_config(domain: str) -> None:
"""Add a domain to the certificate config."""
domains = load_cert_config()
if domain not in domains:
domains.append(domain)
save_cert_config(domains)
def remove_cert_from_config(domain: str) -> None:
"""Remove a domain from the certificate config."""
domains = load_cert_config()
if domain in domains:
domains.remove(domain)
save_cert_config(domains)
def load_cert_to_haproxy(domain: str) -> tuple[bool, str]:
"""Load a certificate into HAProxy via Runtime API (zero-downtime).
@@ -149,7 +109,7 @@ def restore_certificates() -> int:
Returns:
Number of certificates restored
"""
domains = load_cert_config()
domains = load_certs_config()
restored = 0
for domain in domains:
@@ -436,7 +396,7 @@ def register_certificate_tools(mcp):
# Reload any renewed certs into HAProxy
if renewed > 0:
domains = load_cert_config()
domains = load_certs_config()
reloaded = 0
for domain in domains:
success, _ = load_cert_to_haproxy(domain)