refactor: migrate data storage from JSON/map files to SQLite
Replace servers.json, certificates.json, and map file parsing with SQLite (WAL mode) as single source of truth. HAProxy map files are now generated from SQLite via sync_map_files(). Key changes: - Add db.py with schema, connection management, and JSON migration - Add DB_FILE config constant - Delegate file_ops.py functions to db.py - Refactor domains.py to use file_ops instead of direct list manipulation - Fix subprocess.TimeoutExpired not caught (doesn't inherit TimeoutError) - Add DB health check in health.py - Init DB on startup in server.py and __main__.py - Update all 359 tests to use SQLite-backed functions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
from haproxy_mcp.exceptions import HaproxyError
|
||||
from haproxy_mcp.file_ops import add_domain_to_map, load_servers_config
|
||||
|
||||
|
||||
class TestHaproxyListServers:
|
||||
@@ -33,8 +34,7 @@ class TestHaproxyListServers:
|
||||
|
||||
def test_list_servers_empty_backend(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List servers for domain with no servers."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
@@ -63,8 +63,7 @@ class TestHaproxyListServers:
|
||||
|
||||
def test_list_servers_with_active_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""List servers with active servers."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
@@ -224,8 +223,7 @@ class TestHaproxyAddServer:
|
||||
|
||||
def test_add_server_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Successfully add server."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"set server": "",
|
||||
@@ -258,8 +256,7 @@ class TestHaproxyAddServer:
|
||||
|
||||
def test_add_server_auto_slot(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Auto-select slot when slot=0."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
@@ -413,8 +410,7 @@ class TestHaproxyAddServers:
|
||||
|
||||
def test_add_servers_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Successfully add multiple servers."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"set server": "",
|
||||
@@ -495,8 +491,7 @@ class TestHaproxyRemoveServer:
|
||||
|
||||
def test_remove_server_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Successfully remove server."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"set server": "",
|
||||
@@ -689,8 +684,7 @@ class TestHaproxyAddServersRollback:
|
||||
|
||||
def test_add_servers_partial_failure_rollback(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Rollback only failed slots when HAProxy error occurs."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Mock configure_server_slot to fail on second slot
|
||||
call_count = [0]
|
||||
@@ -735,18 +729,16 @@ class TestHaproxyAddServersRollback:
|
||||
assert "slot 2" in result # Failed
|
||||
|
||||
# Verify servers.json only has successfully added server
|
||||
with open(patch_config_paths["servers_file"], "r") as f:
|
||||
config = json.load(f)
|
||||
config = load_servers_config()
|
||||
assert "example.com" in config
|
||||
assert "1" in config["example.com"] # Successfully added stays
|
||||
assert "2" not in config["example.com"] # Failed one was rolled back
|
||||
assert "2" not in config.get("example.com", {}) # Failed one was rolled back
|
||||
|
||||
def test_add_servers_unexpected_error_rollback_only_successful(
|
||||
self, mock_socket_class, mock_select, patch_config_paths
|
||||
):
|
||||
"""Rollback only successfully added servers on unexpected error."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Track which servers were configured
|
||||
configured_slots = []
|
||||
@@ -793,8 +785,7 @@ class TestHaproxyAddServersRollback:
|
||||
assert "Unexpected system error" in result
|
||||
|
||||
# Verify servers.json is empty (all rolled back)
|
||||
with open(patch_config_paths["servers_file"], "r") as f:
|
||||
config = json.load(f)
|
||||
config = load_servers_config()
|
||||
assert config == {} or "example.com" not in config or config.get("example.com") == {}
|
||||
|
||||
def test_add_servers_rollback_failure_logged(
|
||||
@@ -802,8 +793,7 @@ class TestHaproxyAddServersRollback:
|
||||
):
|
||||
"""Log rollback failures during error recovery."""
|
||||
import logging
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
def mock_configure_server_slot(backend, server_prefix, slot, ip, http_port):
|
||||
if slot == 2:
|
||||
@@ -858,8 +848,7 @@ class TestHaproxyAddServerAutoSlot:
|
||||
|
||||
def test_add_server_auto_slot_all_used(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Auto-select slot fails when all slots are in use."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Build response with all 10 slots used
|
||||
servers = []
|
||||
@@ -902,8 +891,7 @@ class TestHaproxyAddServerAutoSlot:
|
||||
|
||||
def test_add_server_negative_slot_auto_select(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Negative slot number triggers auto-selection."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
@@ -970,8 +958,7 @@ class TestHaproxyWaitDrain:
|
||||
|
||||
def test_wait_drain_success(self, patch_config_paths):
|
||||
"""Successfully wait for connections to drain."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Mock haproxy_cmd to return 0 connections
|
||||
with patch("haproxy_mcp.tools.servers.haproxy_cmd") as mock_cmd:
|
||||
@@ -1000,8 +987,7 @@ class TestHaproxyWaitDrain:
|
||||
|
||||
def test_wait_drain_timeout(self, patch_config_paths):
|
||||
"""Timeout when connections don't drain."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
time_values = [0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] # Simulate time passing
|
||||
time_iter = iter(time_values)
|
||||
@@ -1087,9 +1073,6 @@ class TestHaproxyWaitDrain:
|
||||
def test_wait_drain_domain_not_found(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Error when domain not found in map."""
|
||||
# Empty map file - domain not configured
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("")
|
||||
|
||||
from haproxy_mcp.tools.servers import register_server_tools
|
||||
mcp = MagicMock()
|
||||
registered_tools = {}
|
||||
@@ -1202,8 +1185,7 @@ class TestHaproxySetDomainState:
|
||||
|
||||
def test_set_domain_state_success(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Set all servers of a domain to a state."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([
|
||||
@@ -1283,8 +1265,7 @@ class TestHaproxySetDomainState:
|
||||
|
||||
def test_set_domain_state_no_active_servers(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""No active servers found for domain."""
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# All servers have 0.0.0.0 address (not configured)
|
||||
mock_sock = mock_socket_class(responses={
|
||||
@@ -1318,9 +1299,6 @@ class TestHaproxySetDomainState:
|
||||
def test_set_domain_state_domain_not_found(self, mock_socket_class, mock_select, patch_config_paths, response_builder):
|
||||
"""Handle domain not found in map - shows no active servers."""
|
||||
# Empty map file
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("")
|
||||
|
||||
# Mock should show no servers for unknown domain's backend
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"show servers state": response_builder.servers_state([]),
|
||||
|
||||
Reference in New Issue
Block a user