Skip to content

Commit 3879271

Browse files
committed
Fix ipv6 host lookup raising ValidationError when not found
1 parent 14e54fc commit 3879271

File tree

2 files changed

+122
-35
lines changed

2 files changed

+122
-35
lines changed

mreg_cli/api/fields.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,29 @@ def validate_naive(cls, value: str | MacAddress | Self) -> Self:
4141
except ValidationError as e:
4242
raise ValueError(f"Invalid MAC address '{value}'") from e
4343

44+
@classmethod
45+
def parse(cls, obj: Any) -> MacAddress:
46+
"""Parse a MAC address from a string. Returns the MAC address as a string.
47+
48+
:param obj: The object to parse.
49+
:returns: The MAC address as a string.
50+
:raises ValueError: If the object is not a valid MAC address.
51+
"""
52+
# Match interface of NetworkOrIP.parse
53+
return cls.validate(obj).address
54+
55+
@classmethod
56+
def parse_optional(cls, obj: Any) -> MacAddress | None:
57+
"""Parse a MAC address from a string. Returns None if the MAC address is invalid.
58+
59+
:param obj: The object to parse.
60+
:returns: The MAC address as a string or None if it is invalid.
61+
"""
62+
try:
63+
return cls.parse(obj)
64+
except ValueError:
65+
return None
66+
4467
def __str__(self) -> str:
4568
"""Return the MAC address as a string."""
4669
return str(self.address)

mreg_cli/api/models.py

Lines changed: 99 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any, Callable, ClassVar, Iterable, List, Literal, Self, cast, overload
1111

1212
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator
13+
from pydantic_extra_types.mac_address import MacAddress
1314
from typing_extensions import Unpack
1415

1516
from mreg_cli.api.abstracts import APIMixin, FrozenModel, FrozenModelWithTimestamps
@@ -100,7 +101,11 @@ def parse(cls, value: Any, mode: Literal["network"]) -> IP_NetworkT: ...
100101

101102
@overload
102103
@classmethod
103-
def parse(cls, value: Any, mode: IPNetMode) -> IP_AddressT | IP_NetworkT: ...
104+
def parse(cls, value: Any, mode: Literal["networkv4"]) -> ipaddress.IPv4Network: ...
105+
106+
@overload
107+
@classmethod
108+
def parse(cls, value: Any, mode: Literal["networkv6"]) -> ipaddress.IPv6Network: ...
104109

105110
@classmethod
106111
def parse(cls, value: Any, mode: IPNetMode | None = None) -> IP_AddressT | IP_NetworkT:
@@ -126,6 +131,38 @@ def parse(cls, value: Any, mode: IPNetMode | None = None) -> IP_AddressT | IP_Ne
126131
return func(ipnet)
127132
return ipnet.ip_or_network
128133

134+
@classmethod
135+
def parse_ip_optional(
136+
cls, ip: str, version: Literal[4, 6] | None = None
137+
) -> IP_AddressT | None:
138+
"""Check if a value is a valid IP address.
139+
140+
:param ip: The IP address to parse.
141+
:param version: The IP version to parse as. Parses as any version if None.
142+
:returns: The parsed IP address or None if parsing fails.
143+
"""
144+
mode = "ipv4" if version == 4 else "ipv6" if version == 6 else "ip"
145+
try:
146+
return cls.parse(ip, mode=mode)
147+
except ValueError:
148+
return None
149+
150+
@classmethod
151+
def parse_network_optional(
152+
cls, ip: str, version: Literal[4, 6] | None = None
153+
) -> IP_NetworkT | None:
154+
"""Parse a value as an IP network. Returns None if parsing fails.
155+
156+
:param ip: The IP network to parse.
157+
:param version: The IP version to parse as. Parses as any version if None.
158+
:returns: The parsed IP network or None if parsing fails.
159+
"""
160+
mode = "networkv4" if version == 4 else "networkv6" if version == 6 else "network"
161+
try:
162+
return cls.parse(ip, mode=mode)
163+
except ValueError:
164+
return None
165+
129166
@field_validator("ip_or_network", mode="before")
130167
@classmethod
131168
def validate_ip_or_network(cls, value: Any) -> IP_AddressT | IP_NetworkT:
@@ -2610,6 +2647,58 @@ def endpoint(cls) -> Endpoint:
26102647
"""Return the endpoint for the class."""
26112648
return Endpoint.Hosts
26122649

2650+
@classmethod
2651+
def get_by_ip(cls, ip: IP_AddressT, inform_as_ptr: bool = True) -> Host | None:
2652+
"""Get a host by IP address.
2653+
2654+
:param ip: The IP address to search for.
2655+
:param check_ptr: If True, check for PTR overrides as well.
2656+
:returns: The Host object if found, None otherwise.
2657+
"""
2658+
try:
2659+
host = cls.get_by_field("ipaddresses__ipaddress", str(ip))
2660+
if not host:
2661+
host = cls.get_by_field("ptr_overrides__ipaddress", str(ip))
2662+
if host and inform_as_ptr:
2663+
OutputManager().add_line(f"{ip} is a PTR override for {host.name}")
2664+
return host
2665+
except MultipleEntitiesFound as e:
2666+
raise MultipleEntitiesFound(f"Multiple hosts found with IP address {ip}.") from e
2667+
2668+
@classmethod
2669+
def get_by_ip_or_raise(cls, ip: IP_AddressT, inform_as_ptr: bool = True) -> Host:
2670+
"""Get a host by IP address or raise EntityNotFound.
2671+
2672+
:param ip: The IP address to search for.
2673+
:returns: The Host object if found.
2674+
:param check_ptr: If True, check for PTR overrides as well.
2675+
"""
2676+
host = cls.get_by_ip(ip, inform_as_ptr=inform_as_ptr)
2677+
if not host:
2678+
raise EntityNotFound(f"Host with IP address {ip} not found.")
2679+
return host
2680+
2681+
@classmethod
2682+
def get_by_mac(cls, mac: MacAddress) -> Host | None:
2683+
"""Get a host by MAC address.
2684+
2685+
:param ip: The MAC address to search for.
2686+
:returns: The Host object if found, None otherwise.
2687+
"""
2688+
return cls.get_by_field("ipaddresses__macaddress", str(mac))
2689+
2690+
@classmethod
2691+
def get_by_mac_or_raise(cls, mac: MacAddress) -> Host:
2692+
"""Get a host by MAC address or raise EntityNotFound.
2693+
2694+
:param ip: The MAC address to search for.
2695+
:returns: The Host object if found.
2696+
"""
2697+
host = cls.get_by_mac(mac)
2698+
if not host:
2699+
raise EntityNotFound(f"Host with MAC address {mac} not found.")
2700+
return host
2701+
26132702
@classmethod
26142703
def get_by_any_means_or_raise(
26152704
cls, identifier: str | HostT, inform_as_cname: bool = True, inform_as_ptr: bool = True
@@ -2670,56 +2759,31 @@ def get_by_any_means(
26702759
26712760
:returns: A Host object if the host was found, otherwise None.
26722761
"""
2673-
host = None
26742762
if not isinstance(identifier, HostT):
26752763
if identifier.isdigit():
26762764
return Host.get_by_id(int(identifier))
26772765

2678-
try:
2679-
ptr = False
2680-
ipaddress.ip_address(identifier)
2681-
NetworkOrIP.parse(identifier, mode="ip")
2682-
2683-
host = Host.get_by_field("ipaddresses__ipaddress", identifier)
2684-
if not host:
2685-
host = Host.get_by_field("ptr_overrides__ipaddress", identifier)
2686-
ptr = True
2687-
2688-
if host:
2689-
if ptr and inform_as_ptr:
2690-
OutputManager().add_line(f"{identifier} is a PTR override for {host.name}")
2691-
return host
2692-
except MultipleEntitiesFound as e:
2693-
raise MultipleEntitiesFound(
2694-
f"Multiple hosts found with IP address or PTR {identifier}."
2695-
) from e
2696-
except ValueError: # invalid IP
2697-
pass
2766+
if ip := NetworkOrIP.parse_ip_optional(identifier):
2767+
host = cls.get_by_ip_or_raise(ip, inform_as_ptr=inform_as_ptr)
2768+
return host
26982769

2699-
try:
2700-
mac = MACAddressField.validate_naive(identifier)
2701-
return Host.get_by_field("ipaddresses__macaddress", mac.address)
2702-
except ValueError:
2703-
pass
2770+
if mac := MACAddressField.parse_optional(identifier):
2771+
return cls.get_by_mac_or_raise(mac)
27042772

27052773
# Let us try to find the host by name...
2706-
name = HostT(hostname=identifier)
2707-
else:
2708-
name = identifier
2709-
2710-
host = Host.get_by_field("name", name.hostname)
2774+
identifier = HostT(hostname=identifier)
27112775

2712-
if host:
2776+
if host := cls.get_by_field("name", identifier.hostname):
27132777
return host
27142778

2715-
cname = CNAME.get_by_field("name", name.hostname)
2779+
cname = CNAME.get_by_field("name", identifier.hostname)
27162780
# If we found a CNAME, get the host it points to. We're not interested in the
27172781
# CNAME itself.
27182782
if cname is not None:
27192783
host = Host.get_by_id(cname.host)
27202784

27212785
if host and inform_as_cname:
2722-
OutputManager().add_line(f"{name} is a CNAME for {host.name}")
2786+
OutputManager().add_line(f"{identifier.hostname} is a CNAME for {host.name}")
27232787

27242788
return host
27252789

0 commit comments

Comments
 (0)