diff --git a/api/app.py b/api/app.py index c965a72..b3159b7 100644 --- a/api/app.py +++ b/api/app.py @@ -197,10 +197,20 @@ def _apply_startup_enforcement(): except Exception as e: logger.warning(f"Startup enforcement failed (non-fatal): {e}") +def _bootstrap_dns(): + try: + identity = config_manager.configs.get('_identity', {}) + cell_name = identity.get('cell_name', os.environ.get('CELL_NAME', 'mycell')) + domain = identity.get('domain', os.environ.get('CELL_DOMAIN', 'cell')) + network_manager.bootstrap_dns_records(cell_name, domain) + except Exception as e: + logger.warning(f"DNS bootstrap failed (non-fatal): {e}") + COREFILE_PATH = '/app/config/dns/Corefile' # Run in background so startup isn't blocked waiting on docker exec threading.Thread(target=_apply_startup_enforcement, daemon=True).start() +threading.Thread(target=_bootstrap_dns, daemon=True).start() # Register services with service bus service_bus.register_service('network', network_manager) @@ -769,7 +779,7 @@ def add_dns_record(): data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 - result = network_manager.add_dns_record(data) + result = network_manager.add_dns_record(**data) return jsonify(result) except Exception as e: logger.error(f"Error adding DNS record: {e}") @@ -780,7 +790,7 @@ def remove_dns_record(): """Remove DNS record.""" try: data = request.get_json(silent=True) - result = network_manager.remove_dns_record(data) + result = network_manager.remove_dns_record(**data) return jsonify(result) except Exception as e: logger.error(f"Error removing DNS record: {e}") diff --git a/api/network_manager.py b/api/network_manager.py index ef9d815..18696f2 100644 --- a/api/network_manager.py +++ b/api/network_manager.py @@ -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 = []