Baseline modular DNS sync
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from .config import AppConfig
|
||||
from .engine import (
|
||||
build_sync_plan,
|
||||
execute_sync,
|
||||
filter_desired_by_domain,
|
||||
summarize_plan,
|
||||
validate_desired,
|
||||
)
|
||||
from .registry import load_provider, load_source
|
||||
from .reporting import print_plan
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(prog="dns-sync", description="Synchronize reverse-proxy hostnames to a pluggable DNS provider")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
sync = sub.add_parser("sync", help="Discover, normalize, reconcile, and apply DNS records")
|
||||
sync.add_argument("--dry-run", action="store_true", help="Show the plan without modifying DNS")
|
||||
sync.add_argument("--include-disabled", action="store_true", help="Include disabled source entries when supported")
|
||||
|
||||
sub.add_parser("plugins", help="Show the configured source/provider plugins")
|
||||
return parser
|
||||
|
||||
|
||||
def run_sync(args):
|
||||
config = AppConfig.from_env(include_disabled=args.include_disabled)
|
||||
source = load_source(config.source_name, config)
|
||||
provider = load_provider(config.provider_name, config)
|
||||
|
||||
discovered = source.discover()
|
||||
filtered, filtered_count = filter_desired_by_domain(
|
||||
discovered,
|
||||
domain=config.domain,
|
||||
enabled=config.domain_matching,
|
||||
)
|
||||
desired, warnings = validate_desired(filtered)
|
||||
for warning in warnings:
|
||||
print(f"WARNING: {warning}", file=sys.stderr)
|
||||
|
||||
provider.connect()
|
||||
current = provider.list_records()
|
||||
plan = build_sync_plan(desired, current, provider.capabilities)
|
||||
counts = summarize_plan(plan)
|
||||
|
||||
used, free = provider.capacity()
|
||||
print(f"Source: {source.name}:{config.proxy_url} -> {config.target_ip}")
|
||||
print(f"Provider: {provider.name}")
|
||||
if config.domain_matching:
|
||||
print(
|
||||
f"Domain matching: enabled ({config.domain} + subdomains)"
|
||||
)
|
||||
print(f"Filtered out: {filtered_count}")
|
||||
else:
|
||||
print("Domain matching: disabled")
|
||||
print(f"Desired records: {len(desired)}")
|
||||
if free is None:
|
||||
print(f"Provider capacity: {used} used, unlimited/unknown free")
|
||||
else:
|
||||
print(f"Provider capacity: {used} used, {free} free")
|
||||
|
||||
if free is not None and counts["ADD"] > free:
|
||||
raise RuntimeError(f"Sync requires {counts['ADD']} new records but provider has only {free} free slots")
|
||||
|
||||
print("\nReconciliation plan:")
|
||||
print_plan(plan)
|
||||
print()
|
||||
print(
|
||||
"Plan summary: "
|
||||
f"{counts['ADD']} add, {counts['UPDATE']} update, "
|
||||
f"{counts['UNCHANGED']} unchanged, {counts['SKIP']} skipped"
|
||||
)
|
||||
print("Deletion policy: disabled; provider records absent from desired state are untouched.")
|
||||
|
||||
if args.dry_run:
|
||||
print("DRY RUN: no changes were made.")
|
||||
return 0
|
||||
|
||||
if counts["ADD"] == 0 and counts["UPDATE"] == 0:
|
||||
print("Already synchronized. No changes required.")
|
||||
return 0
|
||||
|
||||
print("\nApplying changes...")
|
||||
execute_sync(provider, plan)
|
||||
print(
|
||||
"\nSync complete: "
|
||||
f"{counts['ADD']} added, {counts['UPDATE']} updated, "
|
||||
f"{counts['UNCHANGED']} unchanged, {counts['SKIP']} skipped"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == "sync":
|
||||
return run_sync(args)
|
||||
if args.command == "plugins":
|
||||
config = AppConfig.from_env()
|
||||
print(f"Source plugin: {config.source_name}")
|
||||
print(f"Provider plugin: {config.provider_name}")
|
||||
return 0
|
||||
parser.error("unknown command")
|
||||
except Exception as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
Reference in New Issue
Block a user