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:
@@ -5,6 +5,8 @@ from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from haproxy_mcp.file_ops import add_domain_to_map, add_server_to_config
|
||||
|
||||
|
||||
class TestRestoreServersFromConfig:
|
||||
"""Tests for restore_servers_from_config function."""
|
||||
@@ -19,12 +21,12 @@ class TestRestoreServersFromConfig:
|
||||
|
||||
def test_restore_servers_success(self, mock_socket_class, mock_select, patch_config_paths, sample_servers_config):
|
||||
"""Restore servers successfully."""
|
||||
# Write config and map
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(sample_servers_config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
f.write("api.example.com pool_2\n")
|
||||
# Add domains and servers to database
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
add_server_to_config("example.com", 1, "10.0.0.1", 80)
|
||||
add_server_to_config("example.com", 2, "10.0.0.2", 80)
|
||||
add_domain_to_map("api.example.com", "pool_2")
|
||||
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
|
||||
|
||||
mock_sock = mock_socket_class(responses={
|
||||
"set server": "",
|
||||
@@ -40,9 +42,8 @@ class TestRestoreServersFromConfig:
|
||||
|
||||
def test_restore_servers_skip_missing_domain(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Skip domains not in map file."""
|
||||
config = {"unknown.com": {"1": {"ip": "10.0.0.1", "http_port": 80}}}
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(config, f)
|
||||
# Add server for unknown.com but no map entry (simulates missing domain)
|
||||
add_server_to_config("unknown.com", 1, "10.0.0.1", 80)
|
||||
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
|
||||
@@ -55,11 +56,9 @@ class TestRestoreServersFromConfig:
|
||||
|
||||
def test_restore_servers_skip_empty_ip(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Skip servers with empty IP."""
|
||||
config = {"example.com": {"1": {"ip": "", "http_port": 80}}}
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
# Add domain to map and server with empty IP (will be skipped during restore)
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
add_server_to_config("example.com", 1, "", 80)
|
||||
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
|
||||
@@ -321,11 +320,12 @@ class TestHaproxyRestoreState:
|
||||
|
||||
def test_restore_state_success(self, mock_socket_class, mock_select, patch_config_paths, sample_servers_config):
|
||||
"""Restore state successfully."""
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(sample_servers_config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
f.write("api.example.com pool_2\n")
|
||||
# Add domains and servers to database
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
add_server_to_config("example.com", 1, "10.0.0.1", 80)
|
||||
add_server_to_config("example.com", 2, "10.0.0.2", 80)
|
||||
add_domain_to_map("api.example.com", "pool_2")
|
||||
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
|
||||
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
|
||||
@@ -373,17 +373,10 @@ class TestRestoreServersFromConfigBatchFailure:
|
||||
|
||||
def test_restore_servers_batch_failure_fallback(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Fall back to individual commands when batch fails."""
|
||||
# Create config with servers
|
||||
config = {
|
||||
"example.com": {
|
||||
"1": {"ip": "10.0.0.1", "http_port": 80},
|
||||
"2": {"ip": "10.0.0.2", "http_port": 80},
|
||||
}
|
||||
}
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
# Add domain and servers to database
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
add_server_to_config("example.com", 1, "10.0.0.1", 80)
|
||||
add_server_to_config("example.com", 2, "10.0.0.2", 80)
|
||||
|
||||
# Track call count to simulate batch failure then individual success
|
||||
call_count = [0]
|
||||
@@ -457,51 +450,51 @@ class TestRestoreServersFromConfigBatchFailure:
|
||||
|
||||
def test_restore_servers_invalid_slot(self, mock_socket_class, mock_select, patch_config_paths):
|
||||
"""Skip servers with invalid slot number."""
|
||||
# Add domain to map
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Mock load_servers_config to return config with invalid slot
|
||||
config = {
|
||||
"example.com": {
|
||||
"invalid": {"ip": "10.0.0.1", "http_port": 80}, # Invalid slot
|
||||
"1": {"ip": "10.0.0.2", "http_port": 80}, # Valid slot
|
||||
}
|
||||
}
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
with patch("haproxy_mcp.tools.configuration.load_servers_config", return_value=config):
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.configuration import restore_servers_from_config
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
from haproxy_mcp.tools.configuration import restore_servers_from_config
|
||||
result = restore_servers_from_config()
|
||||
|
||||
result = restore_servers_from_config()
|
||||
|
||||
# Should only restore the valid server
|
||||
assert result == 1
|
||||
# Should only restore the valid server
|
||||
assert result == 1
|
||||
|
||||
def test_restore_servers_invalid_port(self, mock_socket_class, mock_select, patch_config_paths, caplog):
|
||||
"""Skip servers with invalid port."""
|
||||
import logging
|
||||
# Add domain to map
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
|
||||
# Mock load_servers_config to return config with invalid port
|
||||
config = {
|
||||
"example.com": {
|
||||
"1": {"ip": "10.0.0.1", "http_port": "invalid"}, # Invalid port
|
||||
"2": {"ip": "10.0.0.2", "http_port": 80}, # Valid port
|
||||
}
|
||||
}
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
with patch("haproxy_mcp.tools.configuration.load_servers_config", return_value=config):
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
|
||||
mock_sock = mock_socket_class(responses={"set server": ""})
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
with caplog.at_level(logging.WARNING, logger="haproxy_mcp"):
|
||||
from haproxy_mcp.tools.configuration import restore_servers_from_config
|
||||
|
||||
with patch("socket.socket", return_value=mock_sock):
|
||||
with caplog.at_level(logging.WARNING, logger="haproxy_mcp"):
|
||||
from haproxy_mcp.tools.configuration import restore_servers_from_config
|
||||
result = restore_servers_from_config()
|
||||
|
||||
result = restore_servers_from_config()
|
||||
|
||||
# Should only restore the valid server
|
||||
assert result == 1
|
||||
# Should only restore the valid server
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestStartupRestoreFailures:
|
||||
@@ -658,11 +651,12 @@ class TestHaproxyRestoreStateFailures:
|
||||
"""Handle HAProxy error when restoring state."""
|
||||
from haproxy_mcp.exceptions import HaproxyError
|
||||
|
||||
with open(patch_config_paths["servers_file"], "w") as f:
|
||||
json.dump(sample_servers_config, f)
|
||||
with open(patch_config_paths["map_file"], "w") as f:
|
||||
f.write("example.com pool_1\n")
|
||||
f.write("api.example.com pool_2\n")
|
||||
# Add domains and servers to database
|
||||
add_domain_to_map("example.com", "pool_1")
|
||||
add_server_to_config("example.com", 1, "10.0.0.1", 80)
|
||||
add_server_to_config("example.com", 2, "10.0.0.2", 80)
|
||||
add_domain_to_map("api.example.com", "pool_2")
|
||||
add_server_to_config("api.example.com", 1, "10.0.0.10", 8080)
|
||||
|
||||
with patch("haproxy_mcp.tools.configuration.restore_servers_from_config", side_effect=HaproxyError("Connection refused")):
|
||||
from haproxy_mcp.tools.configuration import register_config_tools
|
||||
|
||||
Reference in New Issue
Block a user