refactor: Extract large functions, improve exception handling, remove duplicates

## Large function extraction
- servers.py: Extract 8 _impl functions from register_server_tools (449 lines)
- certificates.py: Extract 7 _impl functions from register_certificate_tools (386 lines)
- MCP tool wrappers now delegate to module-level implementation functions

## Exception handling improvements
- Replace 11 broad `except Exception` with specific types
- health.py: (OSError, subprocess.SubprocessError)
- configuration.py: (HaproxyError, IOError, OSError, ValueError)
- servers.py: (IOError, OSError, ValueError)
- certificates.py: FileNotFoundError, (subprocess.SubprocessError, OSError)

## Duplicate code extraction
- Add parse_servers_state() to utils.py (replaces 4 duplicate parsers)
- Add disable_server_slot() to utils.py (replaces duplicate patterns)
- Update health.py, servers.py, domains.py to use new helpers

## Other improvements
- Add TypedDict types in file_ops.py and health.py
- Set file permissions (0o600) for sensitive files
- Update tests to use specific exception types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-03 13:23:51 +09:00
parent e66c5ddc7f
commit 06ab47aca8
12 changed files with 891 additions and 723 deletions

View File

@@ -13,14 +13,12 @@ from ..config import (
WILDCARDS_MAP_FILE_CONTAINER,
POOL_COUNT,
MAX_SLOTS,
StateField,
STATE_MIN_COLUMNS,
SUBPROCESS_TIMEOUT,
CERTS_DIR,
logger,
)
from ..exceptions import HaproxyError
from ..validation import validate_domain, validate_ip
from ..validation import validate_domain, validate_ip, validate_port_int
from ..haproxy_client import haproxy_cmd
from ..file_ops import (
get_map_contents,
@@ -31,6 +29,7 @@ from ..file_ops import (
remove_server_from_config,
remove_domain_from_config,
)
from ..utils import parse_servers_state, disable_server_slot
def _find_available_pool(entries: list[tuple[str, str]], used_pools: set[str]) -> Optional[str]:
@@ -166,18 +165,18 @@ def register_domain_tools(mcp):
try:
domains = []
state = haproxy_cmd("show servers state")
parsed_state = parse_servers_state(state)
# Build server map from HAProxy state
server_map: dict[str, list] = {}
for line in state.split("\n"):
parts = line.split()
if len(parts) >= STATE_MIN_COLUMNS and parts[StateField.SRV_ADDR] != "0.0.0.0":
backend = parts[StateField.BE_NAME]
if backend not in server_map:
server_map[backend] = []
server_map[backend].append(
f"{parts[StateField.SRV_NAME]}={parts[StateField.SRV_ADDR]}:{parts[StateField.SRV_PORT]}"
)
server_map: dict[str, list[str]] = {}
for backend, servers_dict in parsed_state.items():
for server_name, srv_info in servers_dict.items():
if srv_info["addr"] != "0.0.0.0":
if backend not in server_map:
server_map[backend] = []
server_map[backend].append(
f"{server_name}={srv_info['addr']}:{srv_info['port']}"
)
# Read from domains.map
seen_domains: set[str] = set()
@@ -220,7 +219,7 @@ def register_domain_tools(mcp):
return "Error: Invalid domain format"
if not validate_ip(ip, allow_empty=True):
return "Error: Invalid IP address format"
if not (1 <= http_port <= 65535):
if not validate_port_int(http_port):
return "Error: Port must be between 1 and 65535"
# Use file locking for the entire pool allocation operation
@@ -339,8 +338,7 @@ def register_domain_tools(mcp):
for slot in range(1, MAX_SLOTS + 1):
server = f"{backend}_{slot}"
try:
haproxy_cmd(f"set server {backend}/{server} state maint")
haproxy_cmd(f"set server {backend}/{server} addr 0.0.0.0 port 0")
disable_server_slot(backend, server)
except HaproxyError as e:
logger.warning(
"Failed to clear server %s/%s for domain %s: %s",