10
10
from typing import Any , Callable , ClassVar , Iterable , List , Literal , Self , cast , overload
11
11
12
12
from pydantic import AliasChoices , BaseModel , ConfigDict , Field , computed_field , field_validator
13
+ from pydantic_extra_types .mac_address import MacAddress
13
14
from typing_extensions import Unpack
14
15
15
16
from mreg_cli .api .abstracts import APIMixin , FrozenModel , FrozenModelWithTimestamps
@@ -100,7 +101,11 @@ def parse(cls, value: Any, mode: Literal["network"]) -> IP_NetworkT: ...
100
101
101
102
@overload
102
103
@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 : ...
104
109
105
110
@classmethod
106
111
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
126
131
return func (ipnet )
127
132
return ipnet .ip_or_network
128
133
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
+
129
166
@field_validator ("ip_or_network" , mode = "before" )
130
167
@classmethod
131
168
def validate_ip_or_network (cls , value : Any ) -> IP_AddressT | IP_NetworkT :
@@ -2610,6 +2647,58 @@ def endpoint(cls) -> Endpoint:
2610
2647
"""Return the endpoint for the class."""
2611
2648
return Endpoint .Hosts
2612
2649
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
+
2613
2702
@classmethod
2614
2703
def get_by_any_means_or_raise (
2615
2704
cls , identifier : str | HostT , inform_as_cname : bool = True , inform_as_ptr : bool = True
@@ -2670,56 +2759,31 @@ def get_by_any_means(
2670
2759
2671
2760
:returns: A Host object if the host was found, otherwise None.
2672
2761
"""
2673
- host = None
2674
2762
if not isinstance (identifier , HostT ):
2675
2763
if identifier .isdigit ():
2676
2764
return Host .get_by_id (int (identifier ))
2677
2765
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
2698
2769
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 )
2704
2772
2705
2773
# 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 )
2711
2775
2712
- if host :
2776
+ if host := cls . get_by_field ( "name" , identifier . hostname ) :
2713
2777
return host
2714
2778
2715
- cname = CNAME .get_by_field ("name" , name .hostname )
2779
+ cname = CNAME .get_by_field ("name" , identifier .hostname )
2716
2780
# If we found a CNAME, get the host it points to. We're not interested in the
2717
2781
# CNAME itself.
2718
2782
if cname is not None :
2719
2783
host = Host .get_by_id (cname .host )
2720
2784
2721
2785
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 } " )
2723
2787
2724
2788
return host
2725
2789
0 commit comments