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: 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") 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") 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 configuration
POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100")) POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100"))

View File

@@ -9,6 +9,7 @@ from typing import Any, Optional
from .config import ( from .config import (
MAP_FILE, MAP_FILE,
SERVERS_FILE, SERVERS_FILE,
CERTS_FILE,
logger, logger,
) )
from .validation import domain_to_backend from .validation import domain_to_backend
@@ -260,3 +261,77 @@ def remove_domain_from_config(domain: str) -> None:
save_servers_config(config) save_servers_config(config)
finally: finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) 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.""" """Certificate management tools for HAProxy MCP Server."""
import json
import os import os
import subprocess import subprocess
from datetime import datetime from datetime import datetime
@@ -11,14 +10,17 @@ from pydantic import Field
from ..config import logger, SUBPROCESS_TIMEOUT from ..config import logger, SUBPROCESS_TIMEOUT
from ..validation import validate_domain from ..validation import validate_domain
from ..haproxy_client import haproxy_cmd 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 # Certificate paths
ACME_SH = os.path.expanduser("~/.acme.sh/acme.sh") ACME_SH = os.path.expanduser("~/.acme.sh/acme.sh")
ACME_HOME = os.path.expanduser("~/.acme.sh") ACME_HOME = os.path.expanduser("~/.acme.sh")
CERTS_DIR = "/opt/haproxy/certs" CERTS_DIR = "/opt/haproxy/certs"
CERTS_DIR_CONTAINER = "/etc/haproxy/certs" CERTS_DIR_CONTAINER = "/etc/haproxy/certs"
CERTS_JSON = "/opt/haproxy/conf/certificates.json"
# Longer timeout for certificate operations (ACME can be slow) # Longer timeout for certificate operations (ACME can be slow)
CERT_TIMEOUT = 120 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]: def load_cert_to_haproxy(domain: str) -> tuple[bool, str]:
"""Load a certificate into HAProxy via Runtime API (zero-downtime). """Load a certificate into HAProxy via Runtime API (zero-downtime).
@@ -149,7 +109,7 @@ def restore_certificates() -> int:
Returns: Returns:
Number of certificates restored Number of certificates restored
""" """
domains = load_cert_config() domains = load_certs_config()
restored = 0 restored = 0
for domain in domains: for domain in domains:
@@ -436,7 +396,7 @@ def register_certificate_tools(mcp):
# Reload any renewed certs into HAProxy # Reload any renewed certs into HAProxy
if renewed > 0: if renewed > 0:
domains = load_cert_config() domains = load_certs_config()
reloaded = 0 reloaded = 0
for domain in domains: for domain in domains:
success, _ = load_cert_to_haproxy(domain) success, _ = load_cert_to_haproxy(domain)