Baseline modular DNS sync
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
from .models import DesiredRecord, ProviderRecord, SyncAction
|
||||
|
||||
|
||||
def hostname_matches_domain(hostname: str, domain: str) -> bool:
|
||||
"""Match a domain apex and any subdomain, without suffix false positives."""
|
||||
candidate = hostname.lower().rstrip(".")
|
||||
if candidate.startswith("*."):
|
||||
candidate = candidate[2:]
|
||||
|
||||
base = domain.lower().rstrip(".")
|
||||
return candidate == base or candidate.endswith("." + base)
|
||||
|
||||
|
||||
def filter_desired_by_domain(
|
||||
records: list[DesiredRecord],
|
||||
domain: str | None,
|
||||
enabled: bool,
|
||||
) -> tuple[list[DesiredRecord], int]:
|
||||
if not enabled:
|
||||
return records, 0
|
||||
|
||||
if not domain:
|
||||
raise RuntimeError(
|
||||
"Domain filtering enabled without a configured DOMAIN"
|
||||
)
|
||||
|
||||
matched = [
|
||||
record
|
||||
for record in records
|
||||
if hostname_matches_domain(record.hostname, domain)
|
||||
]
|
||||
return matched, len(records) - len(matched)
|
||||
|
||||
|
||||
|
||||
|
||||
def validate_desired(records: list[DesiredRecord]) -> tuple[dict[str, DesiredRecord], list[str]]:
|
||||
desired: dict[str, DesiredRecord] = {}
|
||||
warnings: list[str] = []
|
||||
|
||||
for record in records:
|
||||
key = record.hostname.lower()
|
||||
if key in desired:
|
||||
warnings.append(
|
||||
f"DUPLICATE {record.hostname} from {record.source}"
|
||||
+ (f" id={record.source_id}" if record.source_id else "")
|
||||
+ "; keeping first occurrence"
|
||||
)
|
||||
continue
|
||||
desired[key] = record
|
||||
|
||||
return dict(sorted(desired.items())), warnings
|
||||
|
||||
|
||||
def build_sync_plan(desired, current: dict[str, ProviderRecord], capabilities):
|
||||
plan: list[SyncAction] = []
|
||||
|
||||
for hostname, record in desired.items():
|
||||
if record.record_type not in capabilities.record_types:
|
||||
plan.append(SyncAction("SKIP", record, reason=f"record type {record.record_type} unsupported"))
|
||||
continue
|
||||
|
||||
if record.wildcard and not capabilities.wildcard_records:
|
||||
plan.append(SyncAction("SKIP", record, reason="wildcard records unsupported"))
|
||||
continue
|
||||
|
||||
existing = current.get(hostname.lower())
|
||||
if existing is None:
|
||||
if not capabilities.supports_create:
|
||||
plan.append(SyncAction("SKIP", record, reason="provider cannot create records"))
|
||||
else:
|
||||
plan.append(SyncAction("ADD", record))
|
||||
elif existing.ip == record.ip and existing.record_type == record.record_type:
|
||||
plan.append(SyncAction("UNCHANGED", record, current=existing))
|
||||
elif not capabilities.supports_update:
|
||||
plan.append(SyncAction("SKIP", record, current=existing, reason="provider cannot update records"))
|
||||
else:
|
||||
plan.append(SyncAction("UPDATE", record, current=existing))
|
||||
|
||||
return plan
|
||||
|
||||
|
||||
def summarize_plan(plan):
|
||||
counts = {name: 0 for name in ("ADD", "UPDATE", "UNCHANGED", "SKIP")}
|
||||
for item in plan:
|
||||
counts[item.action] = counts.get(item.action, 0) + 1
|
||||
return counts
|
||||
|
||||
|
||||
def execute_sync(provider, plan):
|
||||
counts = summarize_plan(plan)
|
||||
|
||||
for item in plan:
|
||||
if item.action in {"UNCHANGED", "SKIP"}:
|
||||
continue
|
||||
|
||||
if item.action == "ADD":
|
||||
detail = provider.add_record(item.desired)
|
||||
elif item.action == "UPDATE":
|
||||
detail = provider.update_record(item.desired)
|
||||
else:
|
||||
raise RuntimeError(f"Unknown sync action: {item.action}")
|
||||
|
||||
if not provider.verify_record(item.desired):
|
||||
raise RuntimeError(f"{item.desired.hostname}: {item.action.lower()} did not verify")
|
||||
|
||||
suffix = f" ({detail})" if detail else ""
|
||||
print(f"APPLIED {item.action:<6} {item.desired.hostname} -> {item.desired.ip}{suffix}")
|
||||
|
||||
return counts
|
||||
Reference in New Issue
Block a user