# -*- coding: utf-8 -*-
"""
AS3 Ninja types.
"""
import ipaddress
from typing import Any, Optional
from pydantic import BaseModel
__all__ = [
"F5IP",
"F5IPv4",
"F5IPv6",
]
[docs]class BaseF5IP(str):
"""
F5IP base class.
Accepts IPv4 and IPv6 IP addresses in F5 notation.
"""
[docs] @staticmethod
def _get_addr(ipaddr: str) -> str:
addr = ipaddr.split("%")[0]
if addr != ipaddr:
return addr
return ipaddr.split("/")[0]
[docs] @staticmethod
def _get_mask(ipaddr: str) -> str:
try:
return ipaddr.split("/")[1]
except IndexError:
return ""
[docs] @staticmethod
def _get_rdid(ipaddr: str) -> str:
try:
ipaddr = ipaddr.split("/")[0]
return ipaddr.split("%")[1]
except IndexError:
return ""
[docs] @staticmethod
def _validate_ipv4(value: str) -> None:
try:
ipaddress.IPv4Network(value)
except ValueError:
raise ValueError(f"invalid address: '{value}'") from None
if ipaddress.IPv4Network(value).is_loopback is True:
raise ValueError(f"invalid address: '{value}': loopback not allowed")
[docs] @staticmethod
def _validate_ipv6(value: str) -> None:
try:
ipaddress.IPv6Network(value)
except ValueError:
raise ValueError(f"invalid address: '{value}'") from None
if ipaddress.IPv6Network(value).is_loopback is True:
raise ValueError(f"invalid address: '{value}': loopback not allowed")
[docs] @classmethod
def _validate_ipany(cls, value: str) -> None:
try:
cls._validate_ipv4(value)
except ValueError:
cls._validate_ipv6(value)
[docs] @staticmethod
def _validate_rdid(rdid: str) -> None:
if rdid:
try:
_rdid = int(rdid)
except ValueError:
raise ValueError(f"invalid route domain: '{rdid}'") from None
if _rdid < 0 or _rdid > 65534:
raise ValueError(f"invalid route domain: '{rdid}'")
[docs] @classmethod
def _validate_ip(cls, value: str) -> None:
cls._validate_ipany(value)
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
examples=[
"192.0.2.1",
"192.0.2.0/24",
"192.0.2.1%12345",
"192.0.2.0%12345/24",
"2001:db8::1",
"2001:db8::/32",
"2001:db8::1%12345",
"2001:db8::%12345/32",
]
)
[docs] @classmethod
def validate(cls, value):
"""
Validate method is automatically called pydantic.
"""
addr = cls._get_addr(value)
mask = cls._get_mask(value)
rdid = cls._get_rdid(value)
cls._validate_rdid(rdid)
if mask:
cls._validate_ip(f"{addr}/{mask}")
else:
cls._validate_ip(addr)
return cls(f"{value}")
[docs]class BaseF5IPv4(BaseF5IP):
"""
F5IPv4 base class.
Accepts IPv4 addresses in F5 notation.
"""
[docs] @classmethod
def _validate_ip(cls, value: str) -> None:
cls._validate_ipv4(value)
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
examples=[
"192.0.2.1",
"192.0.2.0/24",
"192.0.2.1%12345",
"192.0.2.0%12345/24",
]
)
[docs]class BaseF5IPv6(BaseF5IP):
"""
F5IPv6 base class.
Accepts IPv6 addresses in F5 notation.
"""
[docs] @classmethod
def _validate_ip(cls, value: str) -> None:
cls._validate_ipv6(value)
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
examples=[
"2001:db8::1",
"2001:db8::/32",
"2001:db8::1%12345",
"2001:db8::%12345/32",
]
)
[docs]class F5IP(BaseModel):
"""
Accepts and validates IPv6 and IPv4 addresses in F5 notation.
"""
f5ip: BaseF5IP
addr: str
mask: Optional[Any]
rdid: Optional[Any]
def __init__(self, f5ip):
if not isinstance(f5ip, str):
raise TypeError("string required")
super().__init__(
f5ip=f5ip,
addr=BaseF5IP._get_addr(f5ip),
mask=BaseF5IP._get_mask(f5ip),
rdid=BaseF5IP._get_rdid(f5ip),
)
def __repr__(self):
return f"{self.__class__.__name__}({self.f5ip.__repr__()})"
def __str__(self):
return f"{self.__class__.__name__}({self.f5ip.__repr__()})"
[docs]class F5IPv4(F5IP):
"""
Accepts and validates IPv4 addresses in F5 notation.
"""
f5ip: BaseF5IPv4
[docs]class F5IPv6(F5IP):
"""
Accepts and validates IPv6 addresses in F5 notation.
"""
f5ip: BaseF5IPv6