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:
+12
-2
@@ -197,10 +197,20 @@ def _apply_startup_enforcement():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Startup enforcement failed (non-fatal): {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'
|
COREFILE_PATH = '/app/config/dns/Corefile'
|
||||||
|
|
||||||
# Run in background so startup isn't blocked waiting on docker exec
|
# Run in background so startup isn't blocked waiting on docker exec
|
||||||
threading.Thread(target=_apply_startup_enforcement, daemon=True).start()
|
threading.Thread(target=_apply_startup_enforcement, daemon=True).start()
|
||||||
|
threading.Thread(target=_bootstrap_dns, daemon=True).start()
|
||||||
|
|
||||||
# Register services with service bus
|
# Register services with service bus
|
||||||
service_bus.register_service('network', network_manager)
|
service_bus.register_service('network', network_manager)
|
||||||
@@ -769,7 +779,7 @@ def add_dns_record():
|
|||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
if data is None:
|
if data is None:
|
||||||
return jsonify({"error": "No data provided"}), 400
|
return jsonify({"error": "No data provided"}), 400
|
||||||
result = network_manager.add_dns_record(data)
|
result = network_manager.add_dns_record(**data)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding DNS record: {e}")
|
logger.error(f"Error adding DNS record: {e}")
|
||||||
@@ -780,7 +790,7 @@ def remove_dns_record():
|
|||||||
"""Remove DNS record."""
|
"""Remove DNS record."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
result = network_manager.remove_dns_record(data)
|
result = network_manager.remove_dns_record(**data)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error removing DNS record: {e}")
|
logger.error(f"Error removing DNS record: {e}")
|
||||||
|
|||||||
@@ -118,6 +118,26 @@ class NetworkManager(BaseServiceManager):
|
|||||||
logger.error(f"Failed to remove DNS record: {e}")
|
logger.error(f"Failed to remove DNS record: {e}")
|
||||||
return False
|
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]:
|
def get_dns_records(self, zone: str = 'cell') -> List[Dict]:
|
||||||
"""Get all DNS records across all zones"""
|
"""Get all DNS records across all zones"""
|
||||||
all_records = []
|
all_records = []
|
||||||
|
|||||||
Reference in New Issue
Block a user