feat: auto-generate DNS records on first API startup

- NetworkManager.bootstrap_dns_records(): creates A records for all
  cell services (api, webui, calendar, files, mail, webmail, webdav,
  <cell_name>) using their static container IPs — only runs when the
  zone file doesn't exist yet (idempotent)
- API startup: _bootstrap_dns() thread reads cell_name/domain from
  config_manager and calls bootstrap — runs alongside enforcement thread
- Fix: add_dns_record(data) and remove_dns_record(data) now correctly
  unpack dict kwargs instead of passing dict as positional arg
- Fix: remove duplicate cell{} block in config/dns/Corefile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 10:00:56 -04:00
parent 16af657376
commit 8e741b5729
2 changed files with 32 additions and 2 deletions
+20
View File
@@ -118,6 +118,26 @@ 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:
"""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
]
self.update_dns_zone(domain, records)
logger.info(f"Created {len(records)} default DNS records for zone '{domain}'")
def get_dns_records(self, zone: str = 'cell') -> List[Dict]:
"""Get all DNS records across all zones"""
all_records = []