196 lines
5.9 KiB
Python
196 lines
5.9 KiB
Python
import httpx
|
|
|
|
from abc import ABC, abstractmethod
|
|
from ipaddress import IPv4Address, IPv6Address
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
from typing import ClassVar, Union, get_origin, get_args
|
|
from ..logging import LOGGER
|
|
from ..exceptions import VendorError
|
|
|
|
|
|
class VendorSettings(BaseModel):
|
|
vendor_name: ClassVar[str] = None
|
|
enabled: bool = Field(False, description="if this vendor is enabled and its settings are required")
|
|
create_zone_if_missing: bool = False
|
|
default_zone_ttl: int = 86400
|
|
"""
|
|
used for creating zones - is not applied to existing zones
|
|
"""
|
|
default_record_ttl: int = 600
|
|
"""
|
|
used for creating records - is not applied to existing records
|
|
|
|
NOTE: may be overridden by individual FQDN settings
|
|
"""
|
|
|
|
@model_validator(mode='after')
|
|
def validate_required_fields_if_enabled(cls, data):
|
|
"""
|
|
All fields not allowing `None` via type annotation will be required, if `enabled` is `True`
|
|
|
|
This validator enforces it after individual field validation
|
|
"""
|
|
if not data.enabled:
|
|
# disabled - no need to validate
|
|
return data
|
|
|
|
for name, info in cls.model_fields.items():
|
|
if getattr(data, name, None) is not None:
|
|
# value is set
|
|
continue
|
|
if get_origin(info.annotation) is Union:
|
|
# maybe None is allowed?
|
|
if type(None) in get_args(info.annotation):
|
|
# None is allowed
|
|
continue
|
|
raise ValueError(f'{cls.vendor_name} setting `{name}` is required')
|
|
return data
|
|
|
|
|
|
class Vendor(BaseModel, ABC):
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
settings: VendorSettings | None = None
|
|
httpx_client: httpx.AsyncClient | None = None
|
|
httpx_auth: httpx.Auth | None = None
|
|
"""
|
|
Can be used to supply authentication data such as auth headers
|
|
|
|
See https://www.python-httpx.org/advanced/#customizing-authentication
|
|
"""
|
|
|
|
@abstractmethod
|
|
async def ensure_fqdn(self, fqdn: str, zone: str, ip_addresses: list[IPv4Address | IPv6Address], ttl: int = None):
|
|
raise NotImplementedError()
|
|
|
|
def create_httpx_client(self):
|
|
"""
|
|
Create a new httpx.AsyncClient to be used via e.g. `async with self.httpx_client:`
|
|
"""
|
|
self.httpx_client = httpx.AsyncClient(
|
|
auth=self.httpx_auth
|
|
)
|
|
|
|
@classmethod
|
|
def get_record_type_for_ip(cls, ip_address: IPv4Address | IPv6Address):
|
|
if ip_address.version == 4:
|
|
return 'A'
|
|
elif ip_address.version == 6:
|
|
return 'AAAA'
|
|
else:
|
|
raise NotImplementedError(f'Unsupported IP version: {ip_address.version}')
|
|
|
|
@property
|
|
def logger(self):
|
|
return LOGGER
|
|
|
|
async def _api_request(self, coroutine, raise_for_status: bool = True) -> httpx.Response:
|
|
response = await coroutine
|
|
if raise_for_status and response.status_code >= 400:
|
|
raise VendorError(f'Received HTTP {response.status_code} from Vendor API: {response.text}')
|
|
return response
|
|
|
|
async def api_get(
|
|
self,
|
|
url: str,
|
|
query_parameters: dict = None,
|
|
headers: dict = None,
|
|
timeout: int = 30,
|
|
raise_for_status: bool = True
|
|
) -> httpx.Response:
|
|
return await self._api_request(
|
|
coroutine=self.httpx_client.get(
|
|
auth=self.httpx_auth,
|
|
url=url,
|
|
params=query_parameters,
|
|
headers=headers,
|
|
timeout=timeout
|
|
),
|
|
raise_for_status=raise_for_status
|
|
)
|
|
|
|
async def api_post(
|
|
self,
|
|
url: str,
|
|
query_parameters: dict = None,
|
|
headers: dict = None,
|
|
data=None,
|
|
json: dict = None,
|
|
timeout: int = 30,
|
|
raise_for_status: bool = True
|
|
) -> httpx.Response:
|
|
return await self._api_request(
|
|
coroutine=self.httpx_client.post(
|
|
url=url,
|
|
params=query_parameters,
|
|
headers=headers,
|
|
data=data,
|
|
json=json,
|
|
timeout=timeout
|
|
),
|
|
raise_for_status=raise_for_status
|
|
)
|
|
|
|
async def api_put(
|
|
self,
|
|
url: str,
|
|
query_parameters: dict = None,
|
|
headers: dict = None,
|
|
data=None,
|
|
json: dict = None,
|
|
timeout: int = 30,
|
|
raise_for_status: bool = True
|
|
) -> httpx.Response:
|
|
return await self._api_request(
|
|
coroutine=self.httpx_client.put(
|
|
url=url,
|
|
params=query_parameters,
|
|
headers=headers,
|
|
data=data,
|
|
json=json,
|
|
timeout=timeout
|
|
),
|
|
raise_for_status=raise_for_status
|
|
)
|
|
|
|
async def api_patch(
|
|
self,
|
|
url: str,
|
|
query_parameters: dict = None,
|
|
headers: dict = None,
|
|
data=None,
|
|
json: dict = None,
|
|
timeout: int = 30,
|
|
raise_for_status: bool = True
|
|
) -> httpx.Response:
|
|
return await self._api_request(
|
|
coroutine=self.httpx_client.patch(
|
|
url=url,
|
|
params=query_parameters,
|
|
headers=headers,
|
|
data=data,
|
|
json=json,
|
|
timeout=timeout
|
|
),
|
|
raise_for_status=raise_for_status
|
|
)
|
|
|
|
async def api_delete(
|
|
self,
|
|
url: str,
|
|
query_parameters: dict = None,
|
|
headers: dict = None,
|
|
timeout: int = 30,
|
|
raise_for_status: bool = True
|
|
) -> httpx.Response:
|
|
return await self._api_request(
|
|
coroutine=self.httpx_client.delete(
|
|
auth=self.httpx_auth,
|
|
url=url,
|
|
params=query_parameters,
|
|
headers=headers,
|
|
timeout=timeout
|
|
),
|
|
raise_for_status=raise_for_status
|
|
)
|