Remove legacy source provider sync path
This commit is contained in:
@@ -4,27 +4,17 @@ A plugin-based DNS reconciliation service.
|
|||||||
|
|
||||||
## Plugin contract
|
## Plugin contract
|
||||||
|
|
||||||
Source modules live under `dns_sync/plugins/sources/` and export a class named `Plugin` with:
|
The reconciliation runtime is adapter-based.
|
||||||
|
|
||||||
- `name`
|
Adapter plugins expose:
|
||||||
- `discover() -> list[DesiredRecord]`
|
|
||||||
|
|
||||||
Provider modules live under `dns_sync/plugins/providers/` and export `Plugin` with:
|
- `Plugin`
|
||||||
|
- `configured_endpoints(config)`
|
||||||
|
|
||||||
- `name`
|
Each adapter declares object-specific discovery, apply, and verification
|
||||||
- `capabilities`
|
capabilities. Built-in adapters live under `dns_sync/plugins/adapters/`.
|
||||||
- `connect()`
|
External adapters are loaded from `DNS_SYNC_PLUGIN_PATH` and must be
|
||||||
- `list_records()`
|
explicitly enabled through `ENABLED_PLUGINS`.
|
||||||
- `add_record()`
|
|
||||||
- `update_record()`
|
|
||||||
- `delete_record()`
|
|
||||||
- `verify_record()`
|
|
||||||
- `capacity()`
|
|
||||||
|
|
||||||
Plugins are convention-loaded from environment variables. No registry edit is needed:
|
|
||||||
|
|
||||||
- `PROXY=npm` loads `dns_sync.plugins.sources.npm.Plugin`
|
|
||||||
- `DNS_PROVIDER=verizon` loads `dns_sync.plugins.providers.verizon.Plugin`
|
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
@@ -109,8 +99,8 @@ ROUTER_PASSWORD=...
|
|||||||
|
|
||||||
## Domain filtering
|
## Domain filtering
|
||||||
|
|
||||||
Filtering is performed by the central engine after source discovery and before
|
Filtering is performed centrally after adapter discovery and before
|
||||||
reconciliation, so all source plugins get identical behavior.
|
reconciliation, so endpoint-specific adapters get identical behavior.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DOMAIN_MATCHING=true
|
DOMAIN_MATCHING=true
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .config import AppConfig
|
from .config import AppConfig
|
||||||
from .engine import (
|
|
||||||
build_sync_plan,
|
|
||||||
execute_sync,
|
|
||||||
filter_desired_by_domain,
|
|
||||||
summarize_plan,
|
|
||||||
validate_desired,
|
|
||||||
)
|
|
||||||
from .executor import execute_endpoint_plans
|
from .executor import execute_endpoint_plans
|
||||||
from .orchestrator import discover_reconcile_plan
|
from .orchestrator import discover_reconcile_plan
|
||||||
from .persistence import SQLiteStateStore
|
from .persistence import SQLiteStateStore
|
||||||
@@ -17,8 +10,6 @@ from .registry import (
|
|||||||
configured_adapter_endpoints,
|
configured_adapter_endpoints,
|
||||||
enabled_plugins,
|
enabled_plugins,
|
||||||
load_adapter,
|
load_adapter,
|
||||||
load_provider,
|
|
||||||
load_source,
|
|
||||||
)
|
)
|
||||||
from .reporting import print_endpoint_plans, print_plan
|
from .reporting import print_endpoint_plans, print_plan
|
||||||
|
|
||||||
@@ -41,10 +32,6 @@ def build_parser():
|
|||||||
parser = argparse.ArgumentParser(prog="dns-sync", description="Synchronize reverse-proxy hostnames to a pluggable DNS provider")
|
parser = argparse.ArgumentParser(prog="dns-sync", description="Synchronize reverse-proxy hostnames to a pluggable DNS provider")
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
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")
|
|
||||||
|
|
||||||
reconcile = sub.add_parser(
|
reconcile = sub.add_parser(
|
||||||
"reconcile",
|
"reconcile",
|
||||||
help="Run the persistent generic reconciliation engine",
|
help="Run the persistent generic reconciliation engine",
|
||||||
@@ -74,77 +61,9 @@ def build_parser():
|
|||||||
help="Maximum planned mutations allowed with --apply",
|
help="Maximum planned mutations allowed with --apply",
|
||||||
)
|
)
|
||||||
|
|
||||||
sub.add_parser("plugins", help="Show the configured source/provider plugins")
|
|
||||||
return parser
|
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 _configured_adapters(config):
|
def _configured_adapters(config):
|
||||||
adapters = []
|
adapters = []
|
||||||
|
|
||||||
@@ -275,15 +194,8 @@ def main():
|
|||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
if args.command == "sync":
|
|
||||||
return run_sync(args)
|
|
||||||
if args.command == "reconcile":
|
if args.command == "reconcile":
|
||||||
return run_reconcile(args)
|
return run_reconcile(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")
|
parser.error("unknown command")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"ERROR: {exc}", file=sys.stderr)
|
print(f"ERROR: {exc}", file=sys.stderr)
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ def env_bool(name: str, default: bool) -> bool:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
source_name: str
|
|
||||||
provider_name: str
|
|
||||||
npm_endpoints: tuple[Endpoint, ...]
|
npm_endpoints: tuple[Endpoint, ...]
|
||||||
proxy_ip: str
|
proxy_ip: str
|
||||||
proxy_api_port: int
|
proxy_api_port: int
|
||||||
@@ -118,9 +116,6 @@ class AppConfig:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls, include_disabled: bool = False) -> "AppConfig":
|
def from_env(cls, include_disabled: bool = False) -> "AppConfig":
|
||||||
source_name = os.getenv("PROXY", "npm").strip().lower()
|
|
||||||
provider_name = os.getenv("DNS_PROVIDER", "verizon").strip().lower()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
port = int(os.getenv("PROXY_API_PORT", "81"))
|
port = int(os.getenv("PROXY_API_PORT", "81"))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -185,8 +180,6 @@ class AppConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
source_name=source_name,
|
|
||||||
provider_name=provider_name,
|
|
||||||
npm_endpoints=npm_endpoints,
|
npm_endpoints=npm_endpoints,
|
||||||
proxy_ip=proxy_ip,
|
proxy_ip=proxy_ip,
|
||||||
proxy_api_port=port,
|
proxy_api_port=port,
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -8,7 +8,6 @@ from .adapters import (
|
|||||||
NormalizedObject,
|
NormalizedObject,
|
||||||
PlanAction,
|
PlanAction,
|
||||||
)
|
)
|
||||||
from .models import DesiredRecord, ProviderCapabilities, ProviderRecord
|
|
||||||
|
|
||||||
|
|
||||||
class AdapterPlugin(Protocol):
|
class AdapterPlugin(Protocol):
|
||||||
@@ -20,24 +19,3 @@ class AdapterPlugin(Protocol):
|
|||||||
def discover(self, object_type: str) -> list[DiscoveredObject]: ...
|
def discover(self, object_type: str) -> list[DiscoveredObject]: ...
|
||||||
def apply(self, action: PlanAction) -> ApplyResult | None: ...
|
def apply(self, action: PlanAction) -> ApplyResult | None: ...
|
||||||
def verify(self, object: NormalizedObject) -> bool: ...
|
def verify(self, object: NormalizedObject) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
# Compatibility protocols for the existing DNS-only execution path. They remain
|
|
||||||
# in place until the current plugins and CLI are migrated to AdapterPlugin.
|
|
||||||
class SourcePlugin(Protocol):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def discover(self) -> list[DesiredRecord]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderPlugin(Protocol):
|
|
||||||
name: str
|
|
||||||
capabilities: ProviderCapabilities
|
|
||||||
|
|
||||||
def connect(self) -> None: ...
|
|
||||||
def list_records(self) -> dict[str, ProviderRecord]: ...
|
|
||||||
def add_record(self, record: DesiredRecord) -> str | None: ...
|
|
||||||
def update_record(self, record: DesiredRecord) -> str | None: ...
|
|
||||||
def delete_record(self, hostname: str) -> str | None: ...
|
|
||||||
def verify_record(self, record: DesiredRecord) -> bool: ...
|
|
||||||
def capacity(self) -> tuple[int, int | None]: ...
|
|
||||||
|
|||||||
@@ -28,11 +28,3 @@ class ProviderCapabilities:
|
|||||||
supports_update: bool = True
|
supports_update: bool = True
|
||||||
supports_delete: bool = False
|
supports_delete: bool = False
|
||||||
max_records: int | None = None
|
max_records: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SyncAction:
|
|
||||||
action: str
|
|
||||||
desired: DesiredRecord
|
|
||||||
current: ProviderRecord | None = None
|
|
||||||
reason: str | None = None
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from .adapters import (
|
|||||||
NormalizedObject,
|
NormalizedObject,
|
||||||
Observation,
|
Observation,
|
||||||
)
|
)
|
||||||
from .engine import hostname_matches_domain
|
from .normalizer import hostname_matches_domain
|
||||||
from .object_normalizer import normalize_observations
|
from .object_normalizer import normalize_observations
|
||||||
from .persistence import SQLiteStateStore
|
from .persistence import SQLiteStateStore
|
||||||
from .projections import project_proxy_hosts_to_dns
|
from .projections import project_proxy_hosts_to_dns
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from ...models import DesiredRecord
|
|
||||||
from ...normalizer import normalize_hostname, normalize_ipv4
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
|
||||||
name = "file"
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.path = os.getenv("DNS_SYNC_FILE")
|
|
||||||
if not self.path:
|
|
||||||
raise RuntimeError("DNS_SYNC_FILE is required when PROXY=file")
|
|
||||||
|
|
||||||
def discover(self):
|
|
||||||
try:
|
|
||||||
with open(self.path, "r", encoding="utf-8") as fh:
|
|
||||||
data = yaml.safe_load(fh)
|
|
||||||
except (OSError, yaml.YAMLError) as exc:
|
|
||||||
raise RuntimeError(f"Could not read desired-state file {self.path}: {exc}") from exc
|
|
||||||
|
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("records"), dict):
|
|
||||||
raise RuntimeError("Desired-state YAML must contain a top-level records mapping")
|
|
||||||
|
|
||||||
result = []
|
|
||||||
for raw_hostname, raw_ip in data["records"].items():
|
|
||||||
hostname, wildcard = normalize_hostname(str(raw_hostname), allow_wildcard=True)
|
|
||||||
result.append(
|
|
||||||
DesiredRecord(
|
|
||||||
hostname=hostname,
|
|
||||||
ip=normalize_ipv4(str(raw_ip), hostname),
|
|
||||||
wildcard=wildcard,
|
|
||||||
source=self.name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from ...models import DesiredRecord
|
|
||||||
from ...normalizer import normalize_hostname
|
|
||||||
from ...npm_client import NpmClient
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
|
||||||
name = "npm"
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.client = NpmClient(config)
|
|
||||||
|
|
||||||
def discover(self):
|
|
||||||
records: list[DesiredRecord] = []
|
|
||||||
|
|
||||||
for host in self.client.proxy_hosts():
|
|
||||||
enabled = bool(host.get("enabled", False))
|
|
||||||
|
|
||||||
if not enabled and not self.config.include_disabled:
|
|
||||||
continue
|
|
||||||
|
|
||||||
source_id = str(host.get("id", "")) or None
|
|
||||||
|
|
||||||
for raw_domain in host.get("domain_names", []):
|
|
||||||
hostname, wildcard = normalize_hostname(
|
|
||||||
str(raw_domain),
|
|
||||||
allow_wildcard=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
records.append(
|
|
||||||
DesiredRecord(
|
|
||||||
hostname=hostname,
|
|
||||||
ip=self.config.target_ip,
|
|
||||||
record_type="A",
|
|
||||||
wildcard=wildcard,
|
|
||||||
source=self.name,
|
|
||||||
source_id=source_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return records
|
|
||||||
@@ -258,14 +258,6 @@ def _load(kind: str, name: str, config, *args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_source(name: str, config):
|
|
||||||
return _load("sources", name, config)
|
|
||||||
|
|
||||||
|
|
||||||
def load_provider(name: str, config):
|
|
||||||
return _load("providers", name, config)
|
|
||||||
|
|
||||||
|
|
||||||
def load_adapter(name: str, config, endpoint):
|
def load_adapter(name: str, config, endpoint):
|
||||||
return _load(
|
return _load(
|
||||||
"adapters",
|
"adapters",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class Plugin:
|
|||||||
|
|
||||||
self.password = os.getenv("ROUTER_PASSWORD")
|
self.password = os.getenv("ROUTER_PASSWORD")
|
||||||
if not self.password:
|
if not self.password:
|
||||||
raise RuntimeError("ROUTER_PASSWORD is required when DNS_PROVIDER=verizon")
|
raise RuntimeError("ROUTER_PASSWORD is required by the Verizon adapter")
|
||||||
|
|
||||||
self.timeout = int(os.getenv("ROUTER_TIMEOUT", "10"))
|
self.timeout = int(os.getenv("ROUTER_TIMEOUT", "10"))
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from dns_sync.engine import build_sync_plan
|
|
||||||
from dns_sync.models import (
|
|
||||||
DesiredRecord,
|
|
||||||
ProviderCapabilities,
|
|
||||||
ProviderRecord,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyDnsEngineTests(unittest.TestCase):
|
|
||||||
def test_existing_dns_plan_still_adds_updates_and_preserves_matches(self):
|
|
||||||
desired = {
|
|
||||||
"add.example.com": DesiredRecord(
|
|
||||||
hostname="add.example.com",
|
|
||||||
ip="192.168.1.101",
|
|
||||||
),
|
|
||||||
"keep.example.com": DesiredRecord(
|
|
||||||
hostname="keep.example.com",
|
|
||||||
ip="192.168.1.101",
|
|
||||||
),
|
|
||||||
"update.example.com": DesiredRecord(
|
|
||||||
hostname="update.example.com",
|
|
||||||
ip="192.168.1.101",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
current = {
|
|
||||||
"keep.example.com": ProviderRecord(
|
|
||||||
hostname="keep.example.com",
|
|
||||||
ip="192.168.1.101",
|
|
||||||
),
|
|
||||||
"update.example.com": ProviderRecord(
|
|
||||||
hostname="update.example.com",
|
|
||||||
ip="192.168.1.50",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
plan = build_sync_plan(
|
|
||||||
desired,
|
|
||||||
current,
|
|
||||||
ProviderCapabilities(),
|
|
||||||
)
|
|
||||||
|
|
||||||
actions = {
|
|
||||||
item.desired.hostname: item.action
|
|
||||||
for item in plan
|
|
||||||
}
|
|
||||||
self.assertEqual(actions["add.example.com"], "ADD")
|
|
||||||
self.assertEqual(actions["keep.example.com"], "UNCHANGED")
|
|
||||||
self.assertEqual(actions["update.example.com"], "UPDATE")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user