2023-07-09 18:52:12 +02:00

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
)