From 0719671820054e662f01b0eb596bba19f3be04eb Mon Sep 17 00:00:00 2001 From: Lyncoln Date: Sun, 20 Sep 2026 23:30:15 -0400 Subject: [PATCH] Standardize Verizon plugin client boundary --- dns_sync/models.py | 30 --------- external_plugins/verizon/__init__.py | 23 +++---- .../verizon/{provider.py => client.py} | 49 ++++++-------- tests/test_verizon_adapter.py | 66 +++++++++---------- 4 files changed, 64 insertions(+), 104 deletions(-) delete mode 100644 dns_sync/models.py rename external_plugins/verizon/{provider.py => client.py} (88%) diff --git a/dns_sync/models.py b/dns_sync/models.py deleted file mode 100644 index 60aebec..0000000 --- a/dns_sync/models.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any - - -@dataclass(frozen=True) -class DesiredRecord: - hostname: str - ip: str - record_type: str = "A" - wildcard: bool = False - source: str = "" - source_id: str | None = None - - -@dataclass(frozen=True) -class ProviderRecord: - hostname: str - ip: str - record_type: str = "A" - metadata: dict[str, Any] = field(default_factory=dict) - - -@dataclass(frozen=True) -class ProviderCapabilities: - record_types: frozenset[str] = frozenset({"A"}) - wildcard_records: bool = False - supports_create: bool = True - supports_update: bool = True - supports_delete: bool = False - max_records: int | None = None diff --git a/external_plugins/verizon/__init__.py b/external_plugins/verizon/__init__.py index 1416f0e..f8d59e4 100644 --- a/external_plugins/verizon/__init__.py +++ b/external_plugins/verizon/__init__.py @@ -16,8 +16,7 @@ from dns_sync.adapter_support import ( require_endpoint_plugin, ) from dns_sync.config import parse_endpoint_list -from dns_sync.models import DesiredRecord -from .provider import Plugin as VerizonProvider +from .client import VerizonClient, VerizonRecord def configured_endpoints(config): @@ -53,12 +52,11 @@ class Plugin: def __init__(self, config, endpoint): require_endpoint_plugin(endpoint, self.name) - self.config = config self.endpoint = endpoint - self.provider = VerizonProvider(config, endpoint) + self.client = VerizonClient(endpoint) def connect(self) -> None: - self.provider.connect() + self.client.connect() def discover( self, @@ -72,7 +70,7 @@ class Plugin: discovered: list[DiscoveredObject] = [] records = sorted( - self.provider.list_records().values(), + self.client.list_records().values(), key=lambda record: record.hostname.lower(), ) @@ -101,7 +99,7 @@ class Plugin: def _desired_record( self, object: NormalizedObject, - ) -> DesiredRecord: + ) -> VerizonRecord: if object.object_type != DNS_RECORDS: raise RuntimeError( f"{self.endpoint.identity} cannot handle " @@ -129,11 +127,10 @@ class Plugin: "Verizon DNS does not support wildcard records" ) - return DesiredRecord( + return VerizonRecord( hostname=hostname, ip=str(payload.get("ip") or ""), record_type=record_type, - wildcard=False, ) def apply(self, action: PlanAction) -> ApplyResult | None: @@ -156,16 +153,16 @@ class Plugin: record = self._desired_record(desired) if action.operation == PlanOperation.CREATE: - detail = self.provider.add_record(record) + detail = self.client.add_record(record) elif action.operation == PlanOperation.UPDATE: - detail = self.provider.update_record(record) + detail = self.client.update_record(record) else: raise RuntimeError( f"Verizon adapter does not support " f"{action.operation.value} actions" ) - records = self.provider.list_records() + records = self.client.list_records() applied_record = records.get(record.hostname.lower()) if applied_record is None: applied_record = records.get(record.hostname) @@ -197,6 +194,6 @@ class Plugin: ) def verify(self, object: NormalizedObject) -> bool: - return self.provider.verify_record( + return self.client.verify_record( self._desired_record(object) ) diff --git a/external_plugins/verizon/provider.py b/external_plugins/verizon/client.py similarity index 88% rename from external_plugins/verizon/provider.py rename to external_plugins/verizon/client.py index 167b8c9..b938991 100644 --- a/external_plugins/verizon/provider.py +++ b/external_plugins/verizon/client.py @@ -1,13 +1,14 @@ import hashlib +from dataclasses import dataclass, field import os import re import time +from typing import Any import requests import urllib3 from dns_sync.adapters import Endpoint -from dns_sync.models import ProviderCapabilities, ProviderRecord from dns_sync.normalizer import normalize_hostname, normalize_ipv4 LUCI_USERNAME = ( @@ -17,6 +18,15 @@ LUCI_USERNAME = ( CFG_RE = re.compile(r'addCfg\("dns_(ip|name)(\d+)",\s*"([^"]*)",\s*"([^"]*)"\);') + +@dataclass(frozen=True) +class VerizonRecord: + hostname: str + ip: str + record_type: str = "A" + metadata: dict[str, Any] = field(default_factory=dict) + + class _Slot: def __init__(self, slot): self.slot = slot @@ -26,36 +36,19 @@ class _Slot: self.ip_token = "" -class Plugin: - name = "verizon" - capabilities = ProviderCapabilities( - record_types=frozenset({"A"}), - wildcard_records=False, - supports_create=True, - supports_update=True, - supports_delete=True, - max_records=64, - ) - +class VerizonClient: def __init__( self, - config, - endpoint: Endpoint | None = None, + endpoint: Endpoint, ): - self.config = config self.endpoint = endpoint - if endpoint is None: - self.router_url = os.getenv( - "ROUTER_URL", - "https://192.168.1.1", - ).rstrip("/") - else: - if endpoint.plugin != "verizon": - raise RuntimeError( - "Verizon provider requires a verizon endpoint" - ) - self.router_url = f"https://{endpoint.address}" + if endpoint.plugin != "verizon": + raise RuntimeError( + "VerizonClient requires a verizon endpoint" + ) + + self.router_url = f"https://{endpoint.address}" self.password = os.getenv("ROUTER_PASSWORD") if not self.password: @@ -161,7 +154,7 @@ class Plugin: # The firmware UI has no wildcard support, but normalize defensively. pass ip = normalize_ipv4(slot.ip, hostname) - result[hostname.lower()] = ProviderRecord( + result[hostname.lower()] = VerizonRecord( hostname=hostname, ip=ip, record_type="A", @@ -176,7 +169,7 @@ class Plugin: def _write_slot(self, slot, hostname, ip): if not self.auth_token: - raise RuntimeError("Verizon provider is not authenticated") + raise RuntimeError("Verizon client is not authenticated") if not slot.ip_token or not slot.name_token: raise RuntimeError(f"Slot {slot.slot} is missing dynamic field tokens") diff --git a/tests/test_verizon_adapter.py b/tests/test_verizon_adapter.py index e4360b0..1932a94 100644 --- a/tests/test_verizon_adapter.py +++ b/tests/test_verizon_adapter.py @@ -12,7 +12,7 @@ from dns_sync.adapters import ( PlanOperation, ) from dns_sync.config import AppConfig -from dns_sync.models import ProviderRecord +from external_plugins.verizon.client import VerizonRecord from dns_sync.registry import ( configured_adapter_endpoints, load_adapter, @@ -34,7 +34,7 @@ class VerizonAdapterTests(unittest.TestCase): ): return AppConfig.from_env() - def adapter(self, provider): + def adapter(self, client): config = self.config() import dns_sync.registry as registry @@ -58,8 +58,8 @@ class VerizonAdapterTests(unittest.TestCase): with patch.object( module, - "VerizonProvider", - return_value=provider, + "VerizonClient", + return_value=client, ): endpoint = configured_adapter_endpoints( "verizon", @@ -138,7 +138,7 @@ class VerizonAdapterTests(unittest.TestCase): ), ) - def test_generic_adapter_uses_endpoint_not_router_url(self): + def test_client_uses_explicit_endpoint(self): with patch.dict( os.environ, { @@ -169,33 +169,33 @@ class VerizonAdapterTests(unittest.TestCase): "verizon://192.168.1.1:8443", ) self.assertEqual( - adapter.provider.router_url, + adapter.client.router_url, "https://192.168.1.1:8443", ) - def test_connect_delegates_to_provider(self): - provider = Mock() - adapter = self.adapter(provider) + def test_connect_delegates_to_client(self): + client = Mock() + adapter = self.adapter(client) adapter.connect() - provider.connect.assert_called_once_with() + client.connect.assert_called_once_with() def test_discovers_dns_records_with_slot_local_keys(self): - provider = Mock() - provider.list_records.return_value = { - "second.example.com": ProviderRecord( + client = Mock() + client.list_records.return_value = { + "second.example.com": VerizonRecord( hostname="second.example.com", ip="192.168.1.101", metadata={"slot": 42}, ), - "first.example.com": ProviderRecord( + "first.example.com": VerizonRecord( hostname="first.example.com", ip="192.168.1.101", metadata={"slot": 41}, ), } - adapter = self.adapter(provider) + adapter = self.adapter(client) discovered = adapter.discover(DNS_RECORDS) @@ -213,17 +213,17 @@ class VerizonAdapterTests(unittest.TestCase): ) def test_create_and_update_delegate_without_backend_ip(self): - provider = Mock() - provider.add_record.return_value = "created" - provider.update_record.return_value = "updated" - provider.list_records.return_value = { - "app.example.com": ProviderRecord( + client = Mock() + client.add_record.return_value = "created" + client.update_record.return_value = "updated" + client.list_records.return_value = { + "app.example.com": VerizonRecord( hostname="app.example.com", ip="192.168.1.101", metadata={"slot": 41}, ), } - adapter = self.adapter(provider) + adapter = self.adapter(client) desired = self.dns_object(ip="192.168.1.101") create_result = adapter.apply( @@ -258,17 +258,17 @@ class VerizonAdapterTests(unittest.TestCase): "updated", ) self.assertEqual( - provider.add_record.call_args.args[0].ip, + client.add_record.call_args.args[0].ip, "192.168.1.101", ) self.assertEqual( - provider.update_record.call_args.args[0].ip, + client.update_record.call_args.args[0].ip, "192.168.1.101", ) def test_noop_does_not_mutate_router(self): - provider = Mock() - adapter = self.adapter(provider) + client = Mock() + adapter = self.adapter(client) result = adapter.apply( PlanAction( @@ -279,24 +279,24 @@ class VerizonAdapterTests(unittest.TestCase): ) self.assertIsNone(result) - provider.add_record.assert_not_called() - provider.update_record.assert_not_called() + client.add_record.assert_not_called() + client.update_record.assert_not_called() def test_verify_delegates_expected_dns_record(self): - provider = Mock() - provider.verify_record.return_value = True - adapter = self.adapter(provider) + client = Mock() + client.verify_record.return_value = True + adapter = self.adapter(client) verified = adapter.verify(self.dns_object()) self.assertTrue(verified) - record = provider.verify_record.call_args.args[0] + record = client.verify_record.call_args.args[0] self.assertEqual(record.hostname, "app.example.com") self.assertEqual(record.ip, "192.168.1.101") def test_unsupported_objects_actions_and_wildcards_fail(self): - provider = Mock() - adapter = self.adapter(provider) + client = Mock() + adapter = self.adapter(client) with self.assertRaisesRegex(RuntimeError, "cannot discover"): adapter.discover("certificates")