feat: Add pool sharing for domains
Allow multiple domains to share the same backend pool using share_with parameter. This saves pool slots when domains point to the same servers. - Add share_with parameter to haproxy_add_domain - Add helper functions for shared domain management - Protect shared pools from being cleared on domain removal - Update documentation with pool sharing examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
27
CLAUDE.md
27
CLAUDE.md
@@ -102,6 +102,31 @@ This will:
|
|||||||
|
|
||||||
**No HAProxy reload required!**
|
**No HAProxy reload required!**
|
||||||
|
|
||||||
|
### Pool Sharing
|
||||||
|
|
||||||
|
Multiple domains can share the same pool/backend servers using `share_with`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First domain gets its own pool
|
||||||
|
haproxy_add_domain("example.com", "10.0.0.1")
|
||||||
|
# Creates: example.com → pool_5
|
||||||
|
|
||||||
|
# Additional domains share the same pool
|
||||||
|
haproxy_add_domain("www.example.com", share_with="example.com")
|
||||||
|
haproxy_add_domain("api.example.com", share_with="example.com")
|
||||||
|
# Both use: pool_5 (same backend servers)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Saves pool slots (100 pools can serve unlimited domains)
|
||||||
|
- Multiple domains → same backend servers
|
||||||
|
- Shared domains stored as `_shares` reference in `servers.json`
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Shared domains cannot specify `ip` (use existing servers)
|
||||||
|
- Removing a shared domain keeps servers intact
|
||||||
|
- Removing the last domain using a pool clears the servers
|
||||||
|
|
||||||
### Backend Configuration
|
### Backend Configuration
|
||||||
- Backends always use HTTP (port 80 or custom)
|
- Backends always use HTTP (port 80 or custom)
|
||||||
- SSL/TLS termination happens at HAProxy frontend
|
- SSL/TLS termination happens at HAProxy frontend
|
||||||
@@ -247,7 +272,7 @@ Returns backend server status for a specific domain:
|
|||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `haproxy_list_domains` | List domains (use `include_wildcards=true` for wildcards) |
|
| `haproxy_list_domains` | List domains (use `include_wildcards=true` for wildcards) |
|
||||||
| `haproxy_add_domain` | Add domain to available pool (no reload), supports IPv6 |
|
| `haproxy_add_domain` | Add domain to pool (no reload), supports `share_with` for pool sharing |
|
||||||
| `haproxy_remove_domain` | Remove domain from pool (no reload) |
|
| `haproxy_remove_domain` | Remove domain from pool (no reload) |
|
||||||
|
|
||||||
### Server Management
|
### Server Management
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
# Uses map_dom for suffix matching
|
# Uses map_dom for suffix matching
|
||||||
|
|
||||||
.actions.it.com pool_3
|
.actions.it.com pool_3
|
||||||
.anvil.it.com pool_4
|
.anvil.it.com pool_9
|
||||||
.bench.inouter.com pool_5
|
.bench.inouter.com pool_5
|
||||||
.mcp.inouter.com pool_2
|
.mcp.inouter.com pool_2
|
||||||
.nas.inouter.com pool_1
|
.nas.inouter.com pool_1
|
||||||
|
.ssh.inouter.com pool_11
|
||||||
|
|||||||
@@ -332,6 +332,63 @@ def remove_domain_from_config(domain: str) -> None:
|
|||||||
save_servers_config(config)
|
save_servers_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shared_domain(domain: str) -> Optional[str]:
|
||||||
|
"""Get the parent domain that this domain shares a pool with.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: Domain name to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parent domain name if sharing, None otherwise
|
||||||
|
"""
|
||||||
|
config = load_servers_config()
|
||||||
|
domain_config = config.get(domain, {})
|
||||||
|
return domain_config.get("_shares")
|
||||||
|
|
||||||
|
|
||||||
|
def add_shared_domain_to_config(domain: str, shares_with: str) -> None:
|
||||||
|
"""Add a domain that shares a pool with another domain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: New domain name
|
||||||
|
shares_with: Existing domain to share pool with
|
||||||
|
"""
|
||||||
|
with file_lock(f"{SERVERS_FILE}.lock"):
|
||||||
|
config = load_servers_config()
|
||||||
|
config[domain] = {"_shares": shares_with}
|
||||||
|
save_servers_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def get_domains_sharing_pool(pool: str) -> list[str]:
|
||||||
|
"""Get all domains that use a specific pool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pool: Pool name (e.g., 'pool_5')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of domain names using this pool
|
||||||
|
"""
|
||||||
|
domains = []
|
||||||
|
for domain, backend in get_map_contents():
|
||||||
|
if backend == pool and not domain.startswith("."):
|
||||||
|
domains.append(domain)
|
||||||
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
def is_shared_domain(domain: str) -> bool:
|
||||||
|
"""Check if a domain is sharing another domain's pool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: Domain name to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if domain has _shares reference, False otherwise
|
||||||
|
"""
|
||||||
|
config = load_servers_config()
|
||||||
|
domain_config = config.get(domain, {})
|
||||||
|
return "_shares" in domain_config
|
||||||
|
|
||||||
|
|
||||||
# Certificate configuration functions
|
# Certificate configuration functions
|
||||||
|
|
||||||
def load_certs_config() -> list[str]:
|
def load_certs_config() -> list[str]:
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ from ..file_ops import (
|
|||||||
add_server_to_config,
|
add_server_to_config,
|
||||||
remove_server_from_config,
|
remove_server_from_config,
|
||||||
remove_domain_from_config,
|
remove_domain_from_config,
|
||||||
|
add_shared_domain_to_config,
|
||||||
|
get_domains_sharing_pool,
|
||||||
|
is_shared_domain,
|
||||||
)
|
)
|
||||||
from ..utils import parse_servers_state, disable_server_slot
|
from ..utils import parse_servers_state, disable_server_slot
|
||||||
|
|
||||||
@@ -204,13 +207,18 @@ def register_domain_tools(mcp):
|
|||||||
def haproxy_add_domain(
|
def haproxy_add_domain(
|
||||||
domain: Annotated[str, Field(description="Domain name to add (e.g., api.example.com, example.com)")],
|
domain: Annotated[str, Field(description="Domain name to add (e.g., api.example.com, example.com)")],
|
||||||
ip: Annotated[str, Field(default="", description="Optional: Initial server IP. If provided, adds server to slot 1")],
|
ip: Annotated[str, Field(default="", description="Optional: Initial server IP. If provided, adds server to slot 1")],
|
||||||
http_port: Annotated[int, Field(default=80, description="HTTP port for backend server (default: 80)")]
|
http_port: Annotated[int, Field(default=80, description="HTTP port for backend server (default: 80)")],
|
||||||
|
share_with: Annotated[str, Field(default="", description="Optional: Existing domain to share pool with. New domain uses same backend servers.")]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Add a new domain to HAProxy (no reload required).
|
"""Add a new domain to HAProxy (no reload required).
|
||||||
|
|
||||||
Creates domain→pool mapping. Use haproxy_add_server to add more servers later.
|
Creates domain→pool mapping. Use haproxy_add_server to add more servers later.
|
||||||
|
|
||||||
|
Pool sharing: Use share_with to reuse an existing domain's pool. This saves pool
|
||||||
|
slots when multiple domains point to the same backend servers.
|
||||||
|
|
||||||
Example: haproxy_add_domain("api.example.com", ip="10.0.0.1", http_port=8080)
|
Example: haproxy_add_domain("api.example.com", ip="10.0.0.1", http_port=8080)
|
||||||
|
Example: haproxy_add_domain("www.example.com", share_with="example.com")
|
||||||
"""
|
"""
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
if domain.startswith("."):
|
if domain.startswith("."):
|
||||||
@@ -221,6 +229,10 @@ def register_domain_tools(mcp):
|
|||||||
return "Error: Invalid IP address format"
|
return "Error: Invalid IP address format"
|
||||||
if not validate_port_int(http_port):
|
if not validate_port_int(http_port):
|
||||||
return "Error: Port must be between 1 and 65535"
|
return "Error: Port must be between 1 and 65535"
|
||||||
|
if share_with and not validate_domain(share_with):
|
||||||
|
return "Error: Invalid share_with domain format"
|
||||||
|
if share_with and ip:
|
||||||
|
return "Error: Cannot specify both ip and share_with (shared domains use existing servers)"
|
||||||
|
|
||||||
# Use file locking for the entire pool allocation operation
|
# Use file locking for the entire pool allocation operation
|
||||||
lock_path = f"{MAP_FILE}.lock"
|
lock_path = f"{MAP_FILE}.lock"
|
||||||
@@ -244,10 +256,19 @@ def register_domain_tools(mcp):
|
|||||||
if not entry_domain.startswith("."):
|
if not entry_domain.startswith("."):
|
||||||
registered_domains.add(entry_domain)
|
registered_domains.add(entry_domain)
|
||||||
|
|
||||||
# Find available pool
|
# Handle share_with: reuse existing domain's pool
|
||||||
pool = _find_available_pool(entries, used_pools)
|
if share_with:
|
||||||
if not pool:
|
share_backend = get_domain_backend(share_with)
|
||||||
return f"Error: All {POOL_COUNT} pool backends are in use"
|
if not share_backend:
|
||||||
|
return f"Error: Domain {share_with} not found"
|
||||||
|
if not share_backend.startswith("pool_"):
|
||||||
|
return f"Error: Cannot share with legacy backend {share_backend}"
|
||||||
|
pool = share_backend
|
||||||
|
else:
|
||||||
|
# Find available pool
|
||||||
|
pool = _find_available_pool(entries, used_pools)
|
||||||
|
if not pool:
|
||||||
|
return f"Error: All {POOL_COUNT} pool backends are in use"
|
||||||
|
|
||||||
# Check if this is a subdomain of an existing domain
|
# Check if this is a subdomain of an existing domain
|
||||||
is_subdomain, parent_domain = _check_subdomain(domain, registered_domains)
|
is_subdomain, parent_domain = _check_subdomain(domain, registered_domains)
|
||||||
@@ -269,8 +290,13 @@ def register_domain_tools(mcp):
|
|||||||
_rollback_domain_addition(domain, entries)
|
_rollback_domain_addition(domain, entries)
|
||||||
return f"Error: Failed to update HAProxy map: {e}"
|
return f"Error: Failed to update HAProxy map: {e}"
|
||||||
|
|
||||||
# If IP provided, add server to slot 1
|
# Handle server configuration based on mode
|
||||||
if ip:
|
if share_with:
|
||||||
|
# Save shared domain reference
|
||||||
|
add_shared_domain_to_config(domain, share_with)
|
||||||
|
result = f"Domain {domain} added, sharing pool {pool} with {share_with}"
|
||||||
|
elif ip:
|
||||||
|
# Add server to slot 1
|
||||||
add_server_to_config(domain, 1, ip, http_port)
|
add_server_to_config(domain, 1, ip, http_port)
|
||||||
try:
|
try:
|
||||||
server = f"{pool}_1"
|
server = f"{pool}_1"
|
||||||
@@ -318,6 +344,13 @@ def register_domain_tools(mcp):
|
|||||||
return f"Error: Cannot remove legacy domain {domain} (uses static backend {backend})"
|
return f"Error: Cannot remove legacy domain {domain} (uses static backend {backend})"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Check if this domain is sharing another domain's pool
|
||||||
|
domain_is_shared = is_shared_domain(domain)
|
||||||
|
|
||||||
|
# Check if other domains are sharing this pool
|
||||||
|
domains_using_pool = get_domains_sharing_pool(backend)
|
||||||
|
other_domains = [d for d in domains_using_pool if d != domain]
|
||||||
|
|
||||||
# Save to disk first (atomic write for persistence)
|
# Save to disk first (atomic write for persistence)
|
||||||
entries = get_map_contents()
|
entries = get_map_contents()
|
||||||
new_entries = [(d, b) for d, b in entries if d != domain and d != f".{domain}"]
|
new_entries = [(d, b) for d, b in entries if d != domain and d != f".{domain}"]
|
||||||
@@ -334,6 +367,14 @@ def register_domain_tools(mcp):
|
|||||||
except HaproxyError as e:
|
except HaproxyError as e:
|
||||||
logger.warning("Failed to remove wildcard entry for %s: %s", domain, e)
|
logger.warning("Failed to remove wildcard entry for %s: %s", domain, e)
|
||||||
|
|
||||||
|
# Only clear servers if no other domains are using this pool
|
||||||
|
if other_domains:
|
||||||
|
return f"Domain {domain} removed from {backend} (pool still used by: {', '.join(other_domains)})"
|
||||||
|
|
||||||
|
# If this domain was sharing another domain's pool, don't clear servers
|
||||||
|
if domain_is_shared:
|
||||||
|
return f"Domain {domain} removed from shared pool {backend}"
|
||||||
|
|
||||||
# Disable all servers in the pool (reset to 0.0.0.0:0)
|
# Disable all servers in the pool (reset to 0.0.0.0:0)
|
||||||
for slot in range(1, MAX_SLOTS + 1):
|
for slot in range(1, MAX_SLOTS + 1):
|
||||||
server = f"{backend}_{slot}"
|
server = f"{backend}_{slot}"
|
||||||
|
|||||||
Reference in New Issue
Block a user