feat: dynamic ip_range propagation to DNS, firewall, and docker-compose
When ip_range changes in Settings, the new subnet is now applied to: - DNS zone records (network_manager.apply_ip_range) - Caddy virtual IPs (firewall_manager.ensure_caddy_virtual_ips) - iptables per-service rules (firewall_manager.update_service_ips) - docker-compose.yml static IPs if writable (ip_utils.update_docker_compose_ips) New module ip_utils.py derives all container IPs from the subnet using fixed offsets so the entire stack stays consistent from one setting. 321 tests pass (72 new tests added for ip_utils, apply_ip_range, update_service_ips). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+32
-11
@@ -118,26 +118,47 @@ class NetworkManager(BaseServiceManager):
|
||||
logger.error(f"Failed to remove DNS record: {e}")
|
||||
return False
|
||||
|
||||
def bootstrap_dns_records(self, cell_name: str, domain: str) -> None:
|
||||
def bootstrap_dns_records(self, cell_name: str, domain: str,
|
||||
ip_range: str = '172.20.0.0/16') -> None:
|
||||
"""Create default service A records the first time the cell starts up.
|
||||
Skipped if a zone file already exists (idempotent)."""
|
||||
zone_file = os.path.join(self.dns_zones_dir, f'{domain}.zone')
|
||||
if os.path.exists(zone_file):
|
||||
return
|
||||
logger.info(f"Bootstrapping DNS records for zone '{domain}'")
|
||||
records = [
|
||||
{'name': cell_name, 'type': 'A', 'value': '172.20.0.2'}, # cell hostname → Caddy
|
||||
{'name': 'api', 'type': 'A', 'value': '172.20.0.10'}, # REST API
|
||||
{'name': 'webui', 'type': 'A', 'value': '172.20.0.11'}, # Web UI
|
||||
{'name': 'calendar', 'type': 'A', 'value': '172.20.0.21'}, # Caddy vIP → Radicale
|
||||
{'name': 'files', 'type': 'A', 'value': '172.20.0.22'}, # Caddy vIP → Filegator
|
||||
{'name': 'mail', 'type': 'A', 'value': '172.20.0.23'}, # Caddy vIP → Rainloop
|
||||
{'name': 'webmail', 'type': 'A', 'value': '172.20.0.23'}, # alias for mail
|
||||
{'name': 'webdav', 'type': 'A', 'value': '172.20.0.24'}, # Caddy vIP → WebDAV
|
||||
]
|
||||
records = self._build_dns_records(cell_name, ip_range)
|
||||
self.update_dns_zone(domain, records)
|
||||
logger.info(f"Created {len(records)} default DNS records for zone '{domain}'")
|
||||
|
||||
def apply_ip_range(self, ip_range: str, cell_name: str, domain: str) -> Dict[str, Any]:
|
||||
"""Rewrite the primary DNS zone file with IPs derived from the new subnet."""
|
||||
restarted: List[str] = []
|
||||
warnings: List[str] = []
|
||||
try:
|
||||
records = self._build_dns_records(cell_name, ip_range)
|
||||
if self.update_dns_zone(domain, records):
|
||||
restarted.append('cell-dns (reloaded)')
|
||||
else:
|
||||
warnings.append('DNS zone update failed')
|
||||
except Exception as e:
|
||||
warnings.append(f'apply_ip_range failed: {e}')
|
||||
return {'restarted': restarted, 'warnings': warnings}
|
||||
|
||||
def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]:
|
||||
"""Build the standard set of DNS A records for the given subnet."""
|
||||
import ip_utils
|
||||
ips = ip_utils.get_service_ips(ip_range)
|
||||
return [
|
||||
{'name': cell_name, 'type': 'A', 'value': ips['caddy']},
|
||||
{'name': 'api', 'type': 'A', 'value': ips['api']},
|
||||
{'name': 'webui', 'type': 'A', 'value': ips['webui']},
|
||||
{'name': 'calendar', 'type': 'A', 'value': ips['vip_calendar']},
|
||||
{'name': 'files', 'type': 'A', 'value': ips['vip_files']},
|
||||
{'name': 'mail', 'type': 'A', 'value': ips['vip_mail']},
|
||||
{'name': 'webmail', 'type': 'A', 'value': ips['vip_mail']},
|
||||
{'name': 'webdav', 'type': 'A', 'value': ips['vip_webdav']},
|
||||
]
|
||||
|
||||
def get_dns_records(self, zone: str = 'cell') -> List[Dict]:
|
||||
"""Get all DNS records across all zones"""
|
||||
all_records = []
|
||||
|
||||
Reference in New Issue
Block a user