Skip to content

Commit 50efa91

Browse files
Updated mock resolution to also include HostAddressManager DNS-related methods
1 parent 666cedd commit 50efa91

File tree

3 files changed

+140
-71
lines changed

3 files changed

+140
-71
lines changed

cmapi/cmapi_server/managers/host_identity.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -250,42 +250,85 @@ def get_local_identity(self) -> HostIdentity:
250250

251251
def _resolve_dns(self, hostname: str) -> tuple[list[IPAddress], list[str]]:
252252
"""Resolve the given hostname using DNS and return (addresses, names)."""
253-
resolver = dns.resolver.Resolver(configure=True)
254-
255-
# IPv4
256-
a_records: list[IPAddress] = []
253+
ipv4_texts: list[str] = []
254+
ipv6_texts: list[str] = []
257255
try:
258-
for a_rdata in resolver.resolve(hostname, 'A', raise_on_no_answer=False):
259-
a_records.append(ipaddress.ip_address(a_rdata.to_text()))
256+
ipv4_texts = self._dns_resolve_ipv4(hostname)
257+
except dns.resolver.NoAnswer:
258+
logger.debug('IPv4 lookup returned no records for %s', hostname)
259+
ipv4_texts = []
260260
except Exception:
261-
logger.exception('A lookup failed for %s', hostname)
262-
263-
# IPv6
264-
aaaa_records: list[IPAddress] = []
261+
logger.exception('IPv4 lookup unexpected failure for %s', hostname)
262+
raise
265263
if self._policy.allow_ipv6:
266264
try:
267-
for aaaa_rdata in resolver.resolve(hostname, 'AAAA', raise_on_no_answer=False):
268-
aaaa_records.append(ipaddress.ip_address(aaaa_rdata.to_text()))
265+
ipv6_texts = self._dns_resolve_ipv6(hostname)
266+
except dns.resolver.NoAnswer:
267+
logger.debug('IPv6 lookup returned no records for %s', hostname)
268+
ipv6_texts = []
269269
except Exception:
270-
logger.exception('AAAA lookup failed for %s', hostname)
270+
logger.exception('IPv6 lookup unexpected failure for %s', hostname)
271+
raise
272+
273+
addrs: list[IPAddress] = []
274+
for t in ipv4_texts:
275+
try:
276+
addrs.append(ipaddress.ip_address(t))
277+
except ValueError:
278+
continue
279+
for t in ipv6_texts:
280+
try:
281+
addrs.append(ipaddress.ip_address(t))
282+
except ValueError:
283+
continue
271284

272285
names = [hostname]
273-
return a_records + aaaa_records, names
286+
return addrs, names
274287

275288
def _reverse_dns_names(self, ip: IPAddress) -> list[str]:
276289
"""Fetch PTR names for an IP via DNS."""
277290
try:
278-
reverse_name = dns.reversename.from_address(str(ip))
279-
answer = dns.resolver.resolve(reverse_name, 'PTR', raise_on_no_answer=False)
280-
names: list[str] = []
281-
for ptr_rdata in answer:
291+
return self._dns_reverse(str(ip))
292+
except dns.resolver.NoAnswer:
293+
logger.debug('PTR lookup returned no records for %s', ip)
294+
return []
295+
except Exception:
296+
logger.exception('PTR lookup unexpected failure for %s', ip)
297+
raise
298+
299+
# DNS abstraction methods for easier mocking
300+
def _dns_resolve_ipv4(self, hostname: str) -> list[str]:
301+
resolver = dns.resolver.Resolver(configure=True)
302+
results: list[str] = []
303+
for rdata in resolver.resolve(hostname, 'A', raise_on_no_answer=False):
304+
try:
305+
results.append(rdata.to_text())
306+
except Exception:
307+
continue
308+
return results
309+
310+
def _dns_resolve_ipv6(self, hostname: str) -> list[str]:
311+
resolver = dns.resolver.Resolver(configure=True)
312+
results: list[str] = []
313+
for rdata in resolver.resolve(hostname, 'AAAA', raise_on_no_answer=False):
314+
try:
315+
results.append(rdata.to_text())
316+
except Exception:
317+
continue
318+
return results
319+
320+
def _dns_reverse(self, ip_text: str) -> list[str]:
321+
reverse_name = dns.reversename.from_address(ip_text)
322+
answer = dns.resolver.resolve(reverse_name, 'PTR', raise_on_no_answer=False)
323+
names: list[str] = []
324+
for ptr_rdata in answer:
325+
try:
282326
fqdn_name = str(ptr_rdata.target).rstrip('.').lower()
283327
if _is_fqdn(fqdn_name):
284328
names.append(fqdn_name)
285-
return names
286-
except Exception:
287-
logger.exception('PTR lookup failed for %s', ip)
288-
return []
329+
except Exception:
330+
continue
331+
return names
289332

290333
def _contains_private(self, addrs: list[str]) -> bool:
291334
"""Return True if any resolvable address string is a private IP."""

cmapi/cmapi_server/test/mock_resolution.py

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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

149178
def simple_resolution_mock(hostname: str, ip: str):
150179
"""Return a context manager for simple name/IP resolution mocking.

cmapi/cmapi_server/test/test_failover_agent.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import logging
22

3-
from mcs_node_control.models.node_config import NodeConfig
4-
53
from cmapi_server.failover_agent import FailoverAgent
6-
from cmapi_server.managers.network import NetworkManager
74
from cmapi_server.node_manipulation import add_node, remove_node
8-
from cmapi_server.test.mock_resolution import simple_resolution_mock, make_local_resolution_builder
5+
from cmapi_server.test.mock_resolution import make_local_resolution_builder
96
from cmapi_server.test.unittest_global import BaseNodeManipTestCase, tmp_mcs_config_filename
10-
7+
from mcs_node_control.models.node_config import NodeConfig
118

129
logging.basicConfig(level='DEBUG')
1310

0 commit comments

Comments
 (0)