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,
)
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)
)
@@ -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")
+33 -33
View File
@@ -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")