@@ -73,6 +73,79 @@ def set_default(self, ip: str, hostname: str):
7373 self ._default_hostname = hostname
7474 return self
7575
76+ def build (self ):
77+
78+ @contextmanager
79+ def _ctx ():
80+ patches = [
81+ # Patch socket-level resolvers (NetworkManager uses these under the hood)
82+ patch ('socket.getaddrinfo' , side_effect = self ._fake_getaddrinfo ),
83+ patch ('socket.gethostbyname' , side_effect = self ._fake_gethostbyname ),
84+ patch ('socket.gethostbyaddr' , side_effect = self ._fake_gethostbyaddr ),
85+ # Patch local identity to be synthetic; avoid real system calls
86+ patch ('socket.gethostname' , return_value = CUR_HOST_HOSTNAME ),
87+ patch ('socket.getfqdn' , return_value = CUR_HOST_HOSTNAME ),
88+ # Patch NetworkManager local IP discovery (it uses psutil or system libs,
89+ # proper mocking would be too complex)
90+ patch ('cmapi_server.managers.network.NetworkManager.get_current_node_ips' , return_value = [CUR_HOST_IP , DEFAULT_LOCALHOST_IP ]),
91+ # Patch HostAddressManager DNS abstraction methods
92+ patch ('cmapi_server.managers.host_identity.HostAddressManager._dns_resolve_ipv4' ,
93+ side_effect = self ._fake_dns_resolve_ipv4 ),
94+ patch ('cmapi_server.managers.host_identity.HostAddressManager._dns_resolve_ipv6' ,
95+ side_effect = self ._fake_dns_resolve_ipv6 ),
96+ patch ('cmapi_server.managers.host_identity.HostAddressManager._dns_reverse' ,
97+ side_effect = self ._fake_dns_reverse ),
98+ ]
99+ with ExitStack () as stack :
100+ for p in patches :
101+ stack .enter_context (p )
102+ yield
103+
104+ return _ctx ()
105+
106+ def _fake_getaddrinfo (self , host , port , family = socket .AF_UNSPEC , type = 0 , proto = 0 , flags = 0 ):
107+ # Only handle AF_INET calls; otherwise, simulate failure
108+ if family not in (socket .AF_UNSPEC , socket .AF_INET ):
109+ raise socket .gaierror
110+ # For localhost, return loopback first and include CUR_HOST_IP as secondary
111+ if host == DEFAULT_LOCALHOST_HOSTNAME :
112+ return [
113+ (socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (DEFAULT_LOCALHOST_IP , port )),
114+ (socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (CUR_HOST_IP , port )),
115+ ]
116+ ip , _ = self ._resolve_forward (host )
117+ return [(socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (ip , port ))]
118+
119+ def _fake_gethostbyname (self , name : str ) -> str :
120+ ip , _ = self ._resolve_forward (name )
121+ return ip
122+
123+ def _fake_gethostbyaddr (self , addr : str ):
124+ # If no reverse record was set, simulate reverse lookup failure
125+ if addr not in self ._reverse :
126+ raise socket .herror
127+ primary , aliases = self ._reverse [addr ]
128+ return (primary , aliases , [addr ])
129+
130+ # HostIdentityManager DNS abstraction mocks
131+ def _fake_dns_resolve_ipv4 (self , hostname : str ) -> List [str ]:
132+ # Return mapped IPv4 for provided hostname, or empty list if unknown
133+ ip = self ._forward .get (hostname )
134+ return [ip ] if ip else []
135+
136+ def _fake_dns_resolve_ipv6 (self , hostname : str ) -> List [str ]:
137+ # Keep IPv6 disabled by default in tests for determinism
138+ return []
139+
140+ def _fake_dns_reverse (self , ip_text : str ) -> List [str ]:
141+ # Return PTR names (primary + aliases) from reverse map, lowercase
142+ rec = self ._reverse .get (ip_text )
143+ if not rec :
144+ return []
145+ primary , aliases = rec
146+ names = [primary , * aliases ]
147+ return [n .rstrip ('.' ).lower () for n in names if n ]
148+
76149 def _resolve_forward (self , host : str ) -> Tuple [str , str ]:
77150 """Resolve hostname or IP to (ip, hostname) using mappings/defaults."""
78151 # If input looks like an IP, return it with reverse or default hostname
@@ -101,50 +174,6 @@ def _resolve_forward(self, host: str) -> Tuple[str, str]:
101174 # As a last resort, echo back (host, host)
102175 return host , host
103176
104- def build (self ):
105-
106- def _fake_getaddrinfo (host , port , family = socket .AF_UNSPEC , type = 0 , proto = 0 , flags = 0 ):
107- # Only handle AF_INET calls; otherwise, simulate failure
108- if family not in (socket .AF_UNSPEC , socket .AF_INET ):
109- raise socket .gaierror
110- # For localhost, return loopback first and include CUR_HOST_IP as secondary
111- if host == DEFAULT_LOCALHOST_HOSTNAME :
112- return [
113- (socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (DEFAULT_LOCALHOST_IP , port )),
114- (socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (CUR_HOST_IP , port )),
115- ]
116- ip , _ = self ._resolve_forward (host )
117- return [(socket .AF_INET , socket .SOCK_STREAM , 6 , '' , (ip , port ))]
118-
119- def _fake_gethostbyname (name : str ) -> str :
120- ip , _ = self ._resolve_forward (name )
121- return ip
122-
123- def _fake_gethostbyaddr (addr : str ):
124- # If no reverse record was set, simulate reverse lookup failure
125- if addr not in self ._reverse :
126- raise socket .herror
127- primary , aliases = self ._reverse [addr ]
128- return (primary , aliases , [addr ])
129-
130- @contextmanager
131- def _ctx ():
132- patches = [
133- # Patch socket-level resolvers (NetworkManager uses these under the hood)
134- patch ('socket.getaddrinfo' , side_effect = _fake_getaddrinfo ),
135- patch ('socket.gethostbyname' , side_effect = _fake_gethostbyname ),
136- patch ('socket.gethostbyaddr' , side_effect = _fake_gethostbyaddr ),
137- # Patch local identity to be synthetic; avoid real system calls
138- patch ('socket.gethostname' , return_value = CUR_HOST_HOSTNAME ),
139- patch ('socket.getfqdn' , return_value = CUR_HOST_HOSTNAME ),
140- ]
141- with ExitStack () as stack :
142- for p in patches :
143- stack .enter_context (p )
144- yield
145-
146- return _ctx ()
147-
148177
149178def simple_resolution_mock (hostname : str , ip : str ):
150179 """Return a context manager for simple name/IP resolution mocking.
0 commit comments