Skip to content

Commit 7bdcd35

Browse files
Introduced ResolvingSource abstraction
1 parent a80d2fd commit 7bdcd35

File tree

2 files changed

+115
-34
lines changed

2 files changed

+115
-34
lines changed

cmapi/cmapi_server/managers/host_identity.py

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
"""
2+
This module implements hostname <-> IP addr resolution logic (it is not as simple as it may look).
3+
4+
Most of CMAPI/Columnstore code assumes that each node has 1 IP address and 1 hostname.
5+
But in reality it can have N IP addresses and M hostnames, and we need some way to choose which IP/hostname will be primary.
6+
Also some of these names will not be visible from the other hosts (like local aliases from systemd).
7+
And some sources are more reliable than the others (DNS > /etc/hosts), so we must order them by reliablity and choose the most reliable.
8+
9+
So:
10+
1. We must choose 1 IP address and 0/1 hostnames as primary (of many)
11+
2. We need to filter out unreliable names
12+
3. We must order IPs/hostnames by source reliability
13+
4. There can be very many resolving sources, so we cannot resolve everything ourselves, and must rely on OS resolving
14+
(see /etc/nsswitch.conf, there are local /etc/hosts, DNS, mDNS, systemd-resolved, LDAP, myhostname, etc)
15+
5. But most important sources (DNS and /etc/hosts, recommended by our manual...) must be checked and ordered by our policy
16+
"""
117
import hashlib
218
import ipaddress
319
import logging
@@ -12,6 +28,7 @@
1228

1329
from cmapi_server.exceptions import CMAPIBasicError, ResolutionError, ResolutionPolicyViolationError
1430
from cmapi_server.managers.network import NetworkManager
31+
from cmapi_server.managers.resolving_sources import get_resolving_source
1532

1633
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
1734

@@ -310,44 +327,14 @@ def _get_identity_from_non_fqdn(self, hostname: str) -> HostIdentity:
310327

311328
def _resolve_dns(self, hostname: str) -> list[IPAddress]:
312329
"""Resolve the given hostname using DNS and return addresses."""
313-
ipv4_texts: list[str] = []
314-
ipv6_texts: list[str] = []
315-
try:
316-
ipv4_texts = self._dns_resolve_ipv4(hostname)
317-
except dns.resolver.NoAnswer:
318-
logger.warning('IPv4 lookup returned no records for %s', hostname)
319-
ipv4_texts = []
320-
except Exception:
321-
logger.exception('IPv4 lookup unexpected failure for %s', hostname)
322-
raise
323-
324-
if self._policy.allow_ipv6:
325-
try:
326-
ipv6_texts = self._dns_resolve_ipv6(hostname)
327-
except dns.resolver.NoAnswer:
328-
logger.warning('IPv6 lookup returned no records for %s', hostname)
329-
ipv6_texts = []
330-
except Exception:
331-
logger.exception('IPv6 lookup unexpected failure for %s', hostname)
332-
raise
333-
334-
addrs: list[IPAddress] = []
335-
for ip_text in ipv4_texts + ipv6_texts:
336-
ip = _ip_or_none(ip_text)
337-
if ip is None:
338-
logger.error('DNS returned invalid IP address %s for host name %s, skipping', ip_text, hostname)
339-
continue
340-
addrs.append(ip)
341-
342-
return addrs
330+
resolver = get_resolving_source('dns')
331+
return resolver.resolve(hostname)
343332

344333
def _get_names_of_ip(self, ip: IPAddress) -> list[str]:
345334
"""Fetch PTR names for an IP via DNS."""
346335
try:
347-
return self._dns_reverse(str(ip))
348-
except dns.resolver.NoAnswer:
349-
logger.warning('ip-to-name lookup returned no records for %s', ip)
350-
return []
336+
resolver = get_resolving_source('dns')
337+
return resolver.reverse(ip)
351338
except Exception:
352339
logger.exception('ip-to-name lookup unexpected failure for %s', ip)
353340
raise
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import ipaddress
2+
import logging
3+
from functools import cache
4+
from typing import Union
5+
6+
import dns.resolver
7+
import dns.reversename
8+
9+
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class ResolvingSource:
15+
"""Base class for name/IP resolution sources"""
16+
17+
def resolve(self, hostname: str) -> list[IPAddress]:
18+
"""Forward lookup: hostname -> list of IPAddress objects."""
19+
raise NotImplementedError
20+
21+
def reverse(self, ip: IPAddress) -> list[str]:
22+
"""Reverse lookup: IPAddress -> list of normalized hostnames."""
23+
raise NotImplementedError
24+
25+
26+
class DNSResolvingSource(ResolvingSource):
27+
"""DNS-based resolving source"""
28+
29+
def resolve(self, hostname: str) -> list[IPAddress]:
30+
resolver = dns.resolver.Resolver(configure=True)
31+
results: list[IPAddress] = []
32+
seen: set[str] = set()
33+
34+
# A records
35+
try:
36+
answer = resolver.resolve(hostname, 'A', raise_on_no_answer=False)
37+
for rdata in answer:
38+
try:
39+
ip_text = rdata.to_text()
40+
if ip_text in seen:
41+
continue
42+
ip = ipaddress.ip_address(ip_text)
43+
except Exception:
44+
continue
45+
seen.add(ip_text)
46+
results.append(ip)
47+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
48+
pass
49+
50+
# AAAA records
51+
try:
52+
answer = resolver.resolve(hostname, 'AAAA', raise_on_no_answer=False)
53+
for rdata in answer:
54+
try:
55+
ip_text = rdata.to_text()
56+
if ip_text in seen:
57+
continue
58+
ip = ipaddress.ip_address(ip_text)
59+
except Exception:
60+
continue
61+
seen.add(ip_text)
62+
results.append(ip)
63+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
64+
pass
65+
66+
if not results:
67+
logger.warning('DNS lookups (A/AAAA) did not return any addresses for %s', hostname)
68+
69+
return results
70+
71+
def reverse(self, ip: IPAddress) -> list[str]:
72+
ip_text = str(ip)
73+
reverse_name = dns.reversename.from_address(ip_text)
74+
try:
75+
answer = dns.resolver.resolve(reverse_name, 'PTR', raise_on_no_answer=False)
76+
except dns.resolver.NoAnswer:
77+
return []
78+
79+
names: list[str] = []
80+
for ptr_rdata in answer:
81+
try:
82+
name = str(ptr_rdata.target).rstrip('.').lower()
83+
except Exception:
84+
continue
85+
if name:
86+
names.append(name)
87+
return names
88+
89+
90+
@cache
91+
def get_resolving_source(name: str) -> ResolvingSource:
92+
if name == 'dns':
93+
return DNSResolvingSource()
94+
raise ValueError(f'Unknown resolving source: {name}')

0 commit comments

Comments
 (0)