#!/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.) 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