From 6ced2b42d46592ce1e7f0e13e57c3996798cafc3 Mon Sep 17 00:00:00 2001 From: kaffa Date: Mon, 2 Feb 2026 04:26:55 +0000 Subject: [PATCH] refactor: Move certificate config functions to file_ops.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- haproxy_mcp/config.py | 1 + haproxy_mcp/file_ops.py | 75 +++++++++++++++++++++++++++++++ haproxy_mcp/tools/certificates.py | 54 +++------------------- 3 files changed, 83 insertions(+), 47 deletions(-) diff --git a/haproxy_mcp/config.py b/haproxy_mcp/config.py index 07d5fd2..e3d1902 100644 --- a/haproxy_mcp/config.py +++ b/haproxy_mcp/config.py @@ -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")) diff --git a/haproxy_mcp/file_ops.py b/haproxy_mcp/file_ops.py index 10b6478..d96e535 100644 --- a/haproxy_mcp/file_ops.py +++ b/haproxy_mcp/file_ops.py @@ -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) diff --git a/haproxy_mcp/tools/certificates.py b/haproxy_mcp/tools/certificates.py index 31541f7..ebf76f9 100644 --- a/haproxy_mcp/tools/certificates.py +++ b/haproxy_mcp/tools/certificates.py @@ -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)