Standardize Verizon plugin client boundary

This commit is contained in:
Lyncoln
2026-09-20 23:30:15 -04:00
parent c9a3898055
commit 0719671820
4 changed files with 64 additions and 104 deletions
-30
View File
@@ -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
+10 -13
View File
@@ -16,8 +16,7 @@ from dns_sync.adapter_support import (
require_endpoint_plugin, require_endpoint_plugin,
) )
from dns_sync.config import parse_endpoint_list from dns_sync.config import parse_endpoint_list
from dns_sync.models import DesiredRecord from .client import VerizonClient, VerizonRecord
from .provider import Plugin as VerizonProvider
def configured_endpoints(config): def configured_endpoints(config):
@@ -53,12 +52,11 @@ class Plugin:
def __init__(self, config, endpoint): def __init__(self, config, endpoint):
require_endpoint_plugin(endpoint, self.name) require_endpoint_plugin(endpoint, self.name)
self.config = config
self.endpoint = endpoint self.endpoint = endpoint
self.provider = VerizonProvider(config, endpoint) self.client = VerizonClient(endpoint)
def connect(self) -> None: def connect(self) -> None:
self.provider.connect() self.client.connect()
def discover( def discover(
self, self,
@@ -72,7 +70,7 @@ class Plugin:
discovered: list[DiscoveredObject] = [] discovered: list[DiscoveredObject] = []
records = sorted( records = sorted(
self.provider.list_records().values(), self.client.list_records().values(),
key=lambda record: record.hostname.lower(), key=lambda record: record.hostname.lower(),
) )
@@ -101,7 +99,7 @@ class Plugin:
def _desired_record( def _desired_record(
self, self,
object: NormalizedObject, object: NormalizedObject,
) -> DesiredRecord: ) -> VerizonRecord:
if object.object_type != DNS_RECORDS: if object.object_type != DNS_RECORDS:
raise RuntimeError( raise RuntimeError(
f"{self.endpoint.identity} cannot handle " f"{self.endpoint.identity} cannot handle "
@@ -129,11 +127,10 @@ class Plugin:
"Verizon DNS does not support wildcard records" "Verizon DNS does not support wildcard records"
) )
return DesiredRecord( return VerizonRecord(
hostname=hostname, hostname=hostname,
ip=str(payload.get("ip") or ""), ip=str(payload.get("ip") or ""),
record_type=record_type, record_type=record_type,
wildcard=False,
) )
def apply(self, action: PlanAction) -> ApplyResult | None: def apply(self, action: PlanAction) -> ApplyResult | None:
@@ -156,16 +153,16 @@ class Plugin:
record = self._desired_record(desired) record = self._desired_record(desired)
if action.operation == PlanOperation.CREATE: if action.operation == PlanOperation.CREATE:
detail = self.provider.add_record(record) detail = self.client.add_record(record)
elif action.operation == PlanOperation.UPDATE: elif action.operation == PlanOperation.UPDATE:
detail = self.provider.update_record(record) detail = self.client.update_record(record)
else: else:
raise RuntimeError( raise RuntimeError(
f"Verizon adapter does not support " f"Verizon adapter does not support "
f"{action.operation.value} actions" f"{action.operation.value} actions"
) )
records = self.provider.list_records() records = self.client.list_records()
applied_record = records.get(record.hostname.lower()) applied_record = records.get(record.hostname.lower())
if applied_record is None: if applied_record is None:
applied_record = records.get(record.hostname) applied_record = records.get(record.hostname)
@@ -197,6 +194,6 @@ class Plugin:
) )
def verify(self, object: NormalizedObject) -> bool: def verify(self, object: NormalizedObject) -> bool:
return self.provider.verify_record( return self.client.verify_record(
self._desired_record(object) self._desired_record(object)
) )
@@ -1,13 +1,14 @@
import hashlib import hashlib
from dataclasses import dataclass, field
import os import os
import re import re
import time import time
from typing import Any
import requests import requests
import urllib3 import urllib3
from dns_sync.adapters import Endpoint from dns_sync.adapters import Endpoint
from dns_sync.models import ProviderCapabilities, ProviderRecord
from dns_sync.normalizer import normalize_hostname, normalize_ipv4 from dns_sync.normalizer import normalize_hostname, normalize_ipv4
LUCI_USERNAME = ( LUCI_USERNAME = (
@@ -17,6 +18,15 @@ LUCI_USERNAME = (
CFG_RE = re.compile(r'addCfg\("dns_(ip|name)(\d+)",\s*"([^"]*)",\s*"([^"]*)"\);') 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: class _Slot:
def __init__(self, slot): def __init__(self, slot):
self.slot = slot self.slot = slot
@@ -26,36 +36,19 @@ class _Slot:
self.ip_token = "" self.ip_token = ""
class Plugin: class VerizonClient:
name = "verizon"
capabilities = ProviderCapabilities(
record_types=frozenset({"A"}),
wildcard_records=False,
supports_create=True,
supports_update=True,
supports_delete=True,
max_records=64,
)
def __init__( def __init__(
self, self,
config, endpoint: Endpoint,
endpoint: Endpoint | None = None,
): ):
self.config = config
self.endpoint = endpoint self.endpoint = endpoint
if endpoint is None: if endpoint.plugin != "verizon":
self.router_url = os.getenv( raise RuntimeError(
"ROUTER_URL", "VerizonClient requires a verizon endpoint"
"https://192.168.1.1", )
).rstrip("/")
else: self.router_url = f"https://{endpoint.address}"
if endpoint.plugin != "verizon":
raise RuntimeError(
"Verizon provider requires a verizon endpoint"
)
self.router_url = f"https://{endpoint.address}"
self.password = os.getenv("ROUTER_PASSWORD") self.password = os.getenv("ROUTER_PASSWORD")
if not self.password: if not self.password:
@@ -161,7 +154,7 @@ class Plugin:
# The firmware UI has no wildcard support, but normalize defensively. # The firmware UI has no wildcard support, but normalize defensively.
pass pass
ip = normalize_ipv4(slot.ip, hostname) ip = normalize_ipv4(slot.ip, hostname)
result[hostname.lower()] = ProviderRecord( result[hostname.lower()] = VerizonRecord(
hostname=hostname, hostname=hostname,
ip=ip, ip=ip,
record_type="A", record_type="A",
@@ -176,7 +169,7 @@ class Plugin:
def _write_slot(self, slot, hostname, ip): def _write_slot(self, slot, hostname, ip):
if not self.auth_token: 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: if not slot.ip_token or not slot.name_token:
raise RuntimeError(f"Slot {slot.slot} is missing dynamic field tokens") raise RuntimeError(f"Slot {slot.slot} is missing dynamic field tokens")
+33 -33
View File
@@ -12,7 +12,7 @@ from dns_sync.adapters import (
PlanOperation, PlanOperation,
) )
from dns_sync.config import AppConfig from dns_sync.config import AppConfig
from dns_sync.models import ProviderRecord from external_plugins.verizon.client import VerizonRecord
from dns_sync.registry import ( from dns_sync.registry import (
configured_adapter_endpoints, configured_adapter_endpoints,
load_adapter, load_adapter,
@@ -34,7 +34,7 @@ class VerizonAdapterTests(unittest.TestCase):
): ):
return AppConfig.from_env() return AppConfig.from_env()
def adapter(self, provider): def adapter(self, client):
config = self.config() config = self.config()
import dns_sync.registry as registry import dns_sync.registry as registry
@@ -58,8 +58,8 @@ class VerizonAdapterTests(unittest.TestCase):
with patch.object( with patch.object(
module, module,
"VerizonProvider", "VerizonClient",
return_value=provider, return_value=client,
): ):
endpoint = configured_adapter_endpoints( endpoint = configured_adapter_endpoints(
"verizon", "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( with patch.dict(
os.environ, os.environ,
{ {
@@ -169,33 +169,33 @@ class VerizonAdapterTests(unittest.TestCase):
"verizon://192.168.1.1:8443", "verizon://192.168.1.1:8443",
) )
self.assertEqual( self.assertEqual(
adapter.provider.router_url, adapter.client.router_url,
"https://192.168.1.1:8443", "https://192.168.1.1:8443",
) )
def test_connect_delegates_to_provider(self): def test_connect_delegates_to_client(self):
provider = Mock() client = Mock()
adapter = self.adapter(provider) adapter = self.adapter(client)
adapter.connect() adapter.connect()
provider.connect.assert_called_once_with() client.connect.assert_called_once_with()
def test_discovers_dns_records_with_slot_local_keys(self): def test_discovers_dns_records_with_slot_local_keys(self):
provider = Mock() client = Mock()
provider.list_records.return_value = { client.list_records.return_value = {
"second.example.com": ProviderRecord( "second.example.com": VerizonRecord(
hostname="second.example.com", hostname="second.example.com",
ip="192.168.1.101", ip="192.168.1.101",
metadata={"slot": 42}, metadata={"slot": 42},
), ),
"first.example.com": ProviderRecord( "first.example.com": VerizonRecord(
hostname="first.example.com", hostname="first.example.com",
ip="192.168.1.101", ip="192.168.1.101",
metadata={"slot": 41}, metadata={"slot": 41},
), ),
} }
adapter = self.adapter(provider) adapter = self.adapter(client)
discovered = adapter.discover(DNS_RECORDS) discovered = adapter.discover(DNS_RECORDS)
@@ -213,17 +213,17 @@ class VerizonAdapterTests(unittest.TestCase):
) )
def test_create_and_update_delegate_without_backend_ip(self): def test_create_and_update_delegate_without_backend_ip(self):
provider = Mock() client = Mock()
provider.add_record.return_value = "created" client.add_record.return_value = "created"
provider.update_record.return_value = "updated" client.update_record.return_value = "updated"
provider.list_records.return_value = { client.list_records.return_value = {
"app.example.com": ProviderRecord( "app.example.com": VerizonRecord(
hostname="app.example.com", hostname="app.example.com",
ip="192.168.1.101", ip="192.168.1.101",
metadata={"slot": 41}, metadata={"slot": 41},
), ),
} }
adapter = self.adapter(provider) adapter = self.adapter(client)
desired = self.dns_object(ip="192.168.1.101") desired = self.dns_object(ip="192.168.1.101")
create_result = adapter.apply( create_result = adapter.apply(
@@ -258,17 +258,17 @@ class VerizonAdapterTests(unittest.TestCase):
"updated", "updated",
) )
self.assertEqual( self.assertEqual(
provider.add_record.call_args.args[0].ip, client.add_record.call_args.args[0].ip,
"192.168.1.101", "192.168.1.101",
) )
self.assertEqual( self.assertEqual(
provider.update_record.call_args.args[0].ip, client.update_record.call_args.args[0].ip,
"192.168.1.101", "192.168.1.101",
) )
def test_noop_does_not_mutate_router(self): def test_noop_does_not_mutate_router(self):
provider = Mock() client = Mock()
adapter = self.adapter(provider) adapter = self.adapter(client)
result = adapter.apply( result = adapter.apply(
PlanAction( PlanAction(
@@ -279,24 +279,24 @@ class VerizonAdapterTests(unittest.TestCase):
) )
self.assertIsNone(result) self.assertIsNone(result)
provider.add_record.assert_not_called() client.add_record.assert_not_called()
provider.update_record.assert_not_called() client.update_record.assert_not_called()
def test_verify_delegates_expected_dns_record(self): def test_verify_delegates_expected_dns_record(self):
provider = Mock() client = Mock()
provider.verify_record.return_value = True client.verify_record.return_value = True
adapter = self.adapter(provider) adapter = self.adapter(client)
verified = adapter.verify(self.dns_object()) verified = adapter.verify(self.dns_object())
self.assertTrue(verified) 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.hostname, "app.example.com")
self.assertEqual(record.ip, "192.168.1.101") self.assertEqual(record.ip, "192.168.1.101")
def test_unsupported_objects_actions_and_wildcards_fail(self): def test_unsupported_objects_actions_and_wildcards_fail(self):
provider = Mock() client = Mock()
adapter = self.adapter(provider) adapter = self.adapter(client)
with self.assertRaisesRegex(RuntimeError, "cannot discover"): with self.assertRaisesRegex(RuntimeError, "cannot discover"):
adapter.discover("certificates") adapter.discover("certificates")