1c939249e4
docker-compose.yml now uses ${VAR:-default} for every container IP and
the network subnet, so there are no hardcoded addresses in the YAML.
How it works:
- setup_cell.py generates .env at project root from ip_range (gitignored).
- docker-compose reads .env automatically at startup.
- When ip_range changes in Settings, the API writes a new .env via
ip_utils.write_env_file(); DNS/firewall/vIPs update immediately.
- User runs `make start` to recreate containers with the new IPs.
api/ip_utils.py gains ENV_VAR_NAMES dict and write_env_file(ip_range, path).
The old update_docker_compose_ips() direct-patch approach is removed from app.py.
3 new tests added (TestWriteEnvFile); total 324 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.2 KiB
Python
103 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
IP utility functions for PIC — derive all container and virtual IPs from the
|
|
docker network subnet so that one ip_range setting drives everything.
|
|
|
|
The canonical source of IPs is the .env file at the project root.
|
|
docker-compose.yml uses ${VAR:-default} substitution to read from it.
|
|
"""
|
|
|
|
import ipaddress
|
|
import os
|
|
from typing import Dict
|
|
|
|
# Fixed host-number offsets within the subnet (e.g. 172.20.0.0/16 → 172.20.0.<offset>)
|
|
CONTAINER_OFFSETS: Dict[str, int] = {
|
|
'caddy': 2,
|
|
'dns': 3,
|
|
'dhcp': 4,
|
|
'ntp': 5,
|
|
'mail': 6,
|
|
'radicale': 7,
|
|
'webdav': 8,
|
|
'wireguard': 9,
|
|
'api': 10,
|
|
'webui': 11,
|
|
'rainloop': 12,
|
|
'filegator': 13,
|
|
# Caddy virtual IPs — each service gets its own IP so Caddy can route by dst addr
|
|
'vip_calendar': 21,
|
|
'vip_files': 22,
|
|
'vip_mail': 23,
|
|
'vip_webdav': 24,
|
|
}
|
|
|
|
# Mapping from service key → docker-compose env var name (static containers only)
|
|
ENV_VAR_NAMES: Dict[str, str] = {
|
|
'caddy': 'CADDY_IP',
|
|
'dns': 'DNS_IP',
|
|
'dhcp': 'DHCP_IP',
|
|
'ntp': 'NTP_IP',
|
|
'mail': 'MAIL_IP',
|
|
'radicale': 'RADICALE_IP',
|
|
'webdav': 'WEBDAV_IP',
|
|
'wireguard': 'WG_IP',
|
|
'api': 'API_IP',
|
|
'webui': 'WEBUI_IP',
|
|
'rainloop': 'RAINLOOP_IP',
|
|
'filegator': 'FILEGATOR_IP',
|
|
}
|
|
|
|
|
|
def get_service_ips(ip_range: str) -> Dict[str, str]:
|
|
"""
|
|
Derive all container and virtual IPs from the docker network subnet.
|
|
|
|
Example: '172.20.0.0/16' → {'caddy': '172.20.0.2', 'dns': '172.20.0.3', ...}
|
|
The offset of each service within the subnet is fixed (see CONTAINER_OFFSETS).
|
|
"""
|
|
network = ipaddress.IPv4Network(ip_range, strict=False)
|
|
base = int(network.network_address)
|
|
return {
|
|
name: str(ipaddress.IPv4Address(base + offset))
|
|
for name, offset in CONTAINER_OFFSETS.items()
|
|
}
|
|
|
|
|
|
def get_virtual_ips(ip_range: str) -> Dict[str, str]:
|
|
"""
|
|
Return only the four Caddy virtual IPs keyed by service name.
|
|
Used by firewall_manager to set per-service iptables rules.
|
|
"""
|
|
ips = get_service_ips(ip_range)
|
|
return {
|
|
'calendar': ips['vip_calendar'],
|
|
'files': ips['vip_files'],
|
|
'mail': ips['vip_mail'],
|
|
'webdav': ips['vip_webdav'],
|
|
}
|
|
|
|
|
|
def write_env_file(ip_range: str, path: str) -> bool:
|
|
"""
|
|
Write (or overwrite) the docker-compose .env file with IPs derived from ip_range.
|
|
|
|
docker-compose reads this file automatically at startup to substitute
|
|
${VAR:-default} placeholders in docker-compose.yml. Call this at setup
|
|
time and whenever ip_range changes so containers get the right IPs on
|
|
the next `docker-compose up -d`.
|
|
|
|
Returns True on success, False if the path is not writable.
|
|
"""
|
|
try:
|
|
ips = get_service_ips(ip_range)
|
|
lines = [f'CELL_NETWORK={ip_range}\n']
|
|
for svc, var in ENV_VAR_NAMES.items():
|
|
lines.append(f'{var}={ips[svc]}\n')
|
|
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
with open(path, 'w') as f:
|
|
f.writelines(lines)
|
|
return True
|
|
except Exception:
|
|
return False
|