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:
@@ -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"))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user