Expose persistent read-only reconciliation CLI
This commit is contained in:
+97
-2
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import AppConfig
|
||||
@@ -9,8 +10,10 @@ from .engine import (
|
||||
summarize_plan,
|
||||
validate_desired,
|
||||
)
|
||||
from .registry import load_provider, load_source
|
||||
from .reporting import print_plan
|
||||
from .orchestrator import discover_reconcile_plan
|
||||
from .persistence import SQLiteStateStore
|
||||
from .registry import load_adapter, load_provider, load_source
|
||||
from .reporting import print_endpoint_plans, print_plan
|
||||
|
||||
|
||||
def build_parser():
|
||||
@@ -21,6 +24,24 @@ def build_parser():
|
||||
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")
|
||||
|
||||
reconcile = sub.add_parser(
|
||||
"reconcile",
|
||||
help="Run the persistent generic engine without applying changes",
|
||||
)
|
||||
reconcile.add_argument(
|
||||
"--state-db",
|
||||
default=os.getenv(
|
||||
"DNS_SYNC_STATE_DB",
|
||||
"dns-sync-state.sqlite3",
|
||||
),
|
||||
help="SQLite state database path",
|
||||
)
|
||||
reconcile.add_argument(
|
||||
"--include-disabled",
|
||||
action="store_true",
|
||||
help="Include disabled proxy hosts in desired DNS state",
|
||||
)
|
||||
|
||||
sub.add_parser("plugins", help="Show the configured source/provider plugins")
|
||||
return parser
|
||||
|
||||
@@ -92,12 +113,86 @@ def run_sync(args):
|
||||
return 0
|
||||
|
||||
|
||||
def _configured_adapters(config):
|
||||
endpoints = (
|
||||
*config.npm_endpoints,
|
||||
*config.verizon_endpoints,
|
||||
)
|
||||
return [
|
||||
load_adapter(endpoint.plugin, config, endpoint)
|
||||
for endpoint in endpoints
|
||||
]
|
||||
|
||||
|
||||
def run_reconcile(args):
|
||||
config = AppConfig.from_env(
|
||||
include_disabled=args.include_disabled
|
||||
)
|
||||
adapters = _configured_adapters(config)
|
||||
|
||||
with SQLiteStateStore(args.state_db) as store:
|
||||
result = discover_reconcile_plan(
|
||||
adapters,
|
||||
store,
|
||||
target_ip=config.target_ip,
|
||||
include_disabled=config.include_disabled,
|
||||
domain_matching=config.domain_matching,
|
||||
domain=config.domain,
|
||||
)
|
||||
|
||||
for failure in result.failures:
|
||||
print(
|
||||
"WARNING: discovery failed for "
|
||||
f"{failure.endpoint_id} "
|
||||
f"{failure.object_type}: {failure.error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print("Generic reconciliation mode: READ ONLY")
|
||||
print(f"State database: {args.state_db}")
|
||||
print(f"Configured endpoints: {len(adapters)}")
|
||||
print(f"Cached/live observations: {len(result.observations)}")
|
||||
print(f"Canonical DNS records: {len(result.desired)}")
|
||||
print(f"Discovery failures: {len(result.failures)}")
|
||||
print()
|
||||
print("Per-endpoint plans:")
|
||||
print_endpoint_plans(result.plans)
|
||||
|
||||
create_count = sum(
|
||||
action.operation.value == "create"
|
||||
for plan in result.plans
|
||||
for action in plan.actions
|
||||
)
|
||||
update_count = sum(
|
||||
action.operation.value == "update"
|
||||
for plan in result.plans
|
||||
for action in plan.actions
|
||||
)
|
||||
noop_count = sum(
|
||||
action.operation.value == "noop"
|
||||
for plan in result.plans
|
||||
for action in plan.actions
|
||||
)
|
||||
|
||||
print()
|
||||
print(
|
||||
"Plan summary: "
|
||||
f"{create_count} create, "
|
||||
f"{update_count} update, "
|
||||
f"{noop_count} unchanged"
|
||||
)
|
||||
print("READ ONLY: no endpoint changes were applied.")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == "sync":
|
||||
return run_sync(args)
|
||||
if args.command == "reconcile":
|
||||
return run_reconcile(args)
|
||||
if args.command == "plugins":
|
||||
config = AppConfig.from_env()
|
||||
print(f"Source plugin: {config.source_name}")
|
||||
|
||||
@@ -13,3 +13,39 @@ def print_plan(plan):
|
||||
print(f"{'SKIP':<10} {h:<45} {item.reason}")
|
||||
else:
|
||||
print(f"{'UNCHANGED':<10} {h:<45} {item.desired.ip}")
|
||||
|
||||
|
||||
def print_endpoint_plans(plans):
|
||||
if not plans:
|
||||
print("No apply-capable endpoints have planned actions.")
|
||||
return
|
||||
|
||||
for plan in plans:
|
||||
print(f"Endpoint: {plan.endpoint_id}")
|
||||
if not plan.actions:
|
||||
print(" No desired objects for this endpoint.")
|
||||
continue
|
||||
|
||||
for action in plan.actions:
|
||||
desired = action.desired
|
||||
if desired is None:
|
||||
identity = "(no desired object)"
|
||||
else:
|
||||
identity = (
|
||||
f"{desired.object_type} "
|
||||
f"{desired.logical_key}"
|
||||
)
|
||||
|
||||
suffix = ""
|
||||
if (
|
||||
desired is not None
|
||||
and desired.object_type == "dns_records"
|
||||
):
|
||||
target_ip = desired.payload.get("ip")
|
||||
if target_ip:
|
||||
suffix = f" -> {target_ip}"
|
||||
|
||||
print(
|
||||
f" {action.operation.value.upper():<8} "
|
||||
f"{identity}{suffix}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user