You've already forked mariadb-columnstore-engine
mirror of
https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
synced 2025-08-07 03:22:57 +03:00
fix(cmapi): MCOL-5913 cmapi write local address 127.0.1.1 instead real ip address
* feat(cmapi): NetworkManager class for some ip hostname opoerations. * fix(cmapi): Use NetworkManager class to resolve ip and hostname in node_manipulation.add_node function * fix(cmapi): Minor docstring and formatting fixes
This commit is contained in:
committed by
Alan Mologorsky
parent
59a19aaa88
commit
b6a5c1d71f
@@ -43,7 +43,14 @@ SECRET_KEY = 'MCSIsTheBestEver' # not just a random string! (base32)
|
|||||||
|
|
||||||
|
|
||||||
# network constants
|
# network constants
|
||||||
LOCALHOSTS = ('localhost', '127.0.0.1', '::1')
|
# according to https://www.ibm.com/docs/en/storage-sentinel/1.1.2?topic=installation-map-your-local-host-loopback-address
|
||||||
|
LOCALHOSTS = (
|
||||||
|
'127.0.0.1',
|
||||||
|
'localhost', 'localhost.localdomain',
|
||||||
|
'localhost4', 'localhost4.localdomain4',
|
||||||
|
'::1',
|
||||||
|
'localhost6', 'localhost6.localdomain6',
|
||||||
|
)
|
||||||
|
|
||||||
CMAPI_INSTALL_PATH = '/usr/share/columnstore/cmapi/'
|
CMAPI_INSTALL_PATH = '/usr/share/columnstore/cmapi/'
|
||||||
CMAPI_PYTHON_BIN = os.path.join(CMAPI_INSTALL_PATH, "python/bin/python3")
|
CMAPI_PYTHON_BIN = os.path.join(CMAPI_INSTALL_PATH, "python/bin/python3")
|
||||||
|
258
cmapi/cmapi_server/managers/network.py
Normal file
258
cmapi/cmapi_server/managers/network.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
from ipaddress import ip_address
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
_PSUTIL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
psutil = None
|
||||||
|
_PSUTIL_AVAILABLE = False
|
||||||
|
|
||||||
|
from cmapi_server.exceptions import CMAPIBasicError
|
||||||
|
|
||||||
|
|
||||||
|
SIOCGIFADDR = 0x8915 # SIOCGIFADDR "socket ioctl get interface address"
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkManager:
|
||||||
|
@classmethod
|
||||||
|
def get_ip_version(cls, ip_addr: str) -> int:
|
||||||
|
"""Get version of a given IP address.
|
||||||
|
|
||||||
|
:param ip_addr: IP to get version
|
||||||
|
:type ip_addr: str
|
||||||
|
:return: version of a given IP
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
return ip_address(ip_addr).version
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_ip(cls, input_str: str) -> bool:
|
||||||
|
"""Check is input a valid IP or not.
|
||||||
|
|
||||||
|
:param input_str: input string
|
||||||
|
:type input_str: str
|
||||||
|
:return: True if input is a valid IP
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ip_address(input_str)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_hostname_to_ip(
|
||||||
|
cls,
|
||||||
|
hostname: str,
|
||||||
|
only_ipv4: bool = True,
|
||||||
|
exclude_loopback: bool = False
|
||||||
|
) -> list[str]:
|
||||||
|
"""Resolve a hostname to one or more IP addresses.
|
||||||
|
|
||||||
|
:param hostname: Hostname to resolve.
|
||||||
|
:type hostname: str
|
||||||
|
:param only_ipv4: Return only IPv4 addresses (default: True).
|
||||||
|
:type only_ipv4: bool
|
||||||
|
:param exclude_loopback: Exclude loopback addresses like 127.x.x.x (default: True).
|
||||||
|
:type exclude_loopback: bool
|
||||||
|
:return: List of resolved IP addresses.
|
||||||
|
:rtype: list[str]
|
||||||
|
"""
|
||||||
|
sorted_ips: list[str] = []
|
||||||
|
try:
|
||||||
|
addr_info = socket.getaddrinfo(
|
||||||
|
hostname,
|
||||||
|
None,
|
||||||
|
socket.AF_INET if only_ipv4 else socket.AF_UNSPEC,
|
||||||
|
socket.SOCK_STREAM
|
||||||
|
)
|
||||||
|
ip_set = {
|
||||||
|
info[4][0] for info in addr_info
|
||||||
|
if not (exclude_loopback and ip_address(info[4][0]).is_loopback)
|
||||||
|
}
|
||||||
|
sorted_ips = sorted(
|
||||||
|
list(ip_set),
|
||||||
|
key=lambda ip: (
|
||||||
|
not ip_address(ip).is_loopback, # loopback first (False < True)
|
||||||
|
ip_address(ip).version != 4, # IPv4 before IPv6 (False < True)
|
||||||
|
ip_address(ip) # lexical order
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except socket.gaierror:
|
||||||
|
logging.error(
|
||||||
|
f'Standard name resolution failed for hostname: {hostname!r}',
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted_ips
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ip_address_by_nic(cls, ifname: str) -> str:
|
||||||
|
"""Get IP address of a network interface.
|
||||||
|
|
||||||
|
:param ifname: network interface name
|
||||||
|
:type ifname: str
|
||||||
|
:return: ip address
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
# doesn't work on Windows,
|
||||||
|
# OpenBSD and probably doesn't on FreeBSD/pfSense either
|
||||||
|
ip_addr: str = ''
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
ip_addr = socket.inet_ntoa(
|
||||||
|
fcntl.ioctl(
|
||||||
|
s.fileno(),
|
||||||
|
SIOCGIFADDR,
|
||||||
|
struct.pack('256s', bytes(ifname[:15], 'utf-8'))
|
||||||
|
)[20:24]
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno == errno.ENODEV:
|
||||||
|
logging.error(
|
||||||
|
f'Interface {ifname!r} doesn\'t exist.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.error(
|
||||||
|
f'Unknown OSError code while getting IP for an {ifname!r}',
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.error(
|
||||||
|
(
|
||||||
|
'Unknown exception while getting IP address of an '
|
||||||
|
f'{ifname!r} interface',
|
||||||
|
),
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
return ip_addr
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_current_node_ips(
|
||||||
|
cls, ignore_loopback: bool = False, only_ipv4: bool = True,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Get all IP addresses for all existing network interfaces.
|
||||||
|
|
||||||
|
|
||||||
|
:param ignore_loopback: Ignore loopback addresses, defaults to False
|
||||||
|
:type ignore_loopback: bool, optional
|
||||||
|
:param only_ipv4: Return only IPv4 addresses, defaults to True
|
||||||
|
:type only_ipv4: bool, optional
|
||||||
|
:return: IP addresses for all node interfaces
|
||||||
|
:rtype: list[str]
|
||||||
|
:raises CMAPIBasicError: If no IPs are found
|
||||||
|
"""
|
||||||
|
ext_ips: list[str] = []
|
||||||
|
loopback_ips: list[str] = []
|
||||||
|
|
||||||
|
if _PSUTIL_AVAILABLE:
|
||||||
|
try:
|
||||||
|
for _, addrs in psutil.net_if_addrs().items():
|
||||||
|
for addr in addrs:
|
||||||
|
if only_ipv4 and addr.family != socket.AF_INET:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ip = ip_address(addr.address)
|
||||||
|
if ip.is_loopback:
|
||||||
|
loopback_ips.append(str(ip))
|
||||||
|
else:
|
||||||
|
ext_ips.append(str(ip))
|
||||||
|
except ValueError:
|
||||||
|
continue # Not a valid IP (e.g., MAC addresses)
|
||||||
|
except Exception:
|
||||||
|
logging.warning(
|
||||||
|
'Failed to get IPs via psutil, falling back to ioctl',
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = ext_ips if ignore_loopback else [*ext_ips, *loopback_ips]
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
logging.warning(
|
||||||
|
'psutil returned no valid IPs, trying fallback method'
|
||||||
|
)
|
||||||
|
|
||||||
|
ext_ips: list[str] = []
|
||||||
|
loopback_ips: list[str] = []
|
||||||
|
# Fallback to stdlib method using fcntl/ioctl
|
||||||
|
for _, nic_name in socket.if_nameindex():
|
||||||
|
ip_addr = cls.get_ip_address_by_nic(nic_name)
|
||||||
|
if not ip_addr:
|
||||||
|
continue
|
||||||
|
if only_ipv4 and cls.get_ip_version(ip_addr) != 4:
|
||||||
|
continue
|
||||||
|
if ip_address(ip_addr).is_loopback:
|
||||||
|
loopback_ips.append(ip_addr)
|
||||||
|
else:
|
||||||
|
ext_ips.append(ip_addr)
|
||||||
|
|
||||||
|
result = ext_ips if ignore_loopback else [*ext_ips, *loopback_ips]
|
||||||
|
if not result:
|
||||||
|
raise CMAPIBasicError('No IP addresses found on this node.')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_hostname(cls, ip_addr: str) -> Optional[str]:
|
||||||
|
"""Get hostname for a given IP address.
|
||||||
|
|
||||||
|
:param ip_addr: IP address to get hostname
|
||||||
|
:type ip_addr: str
|
||||||
|
:return: Hostname if it exists, otherwise None
|
||||||
|
:rtype: Optional[str]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hostnames = socket.gethostbyaddr(ip_addr)
|
||||||
|
return hostnames[0]
|
||||||
|
except socket.herror:
|
||||||
|
logging.error(f'No hostname found for address: {ip_addr!r}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_only_loopback_hostname(cls, hostname: str) -> bool:
|
||||||
|
"""Check if all IPs resolved from the hostname are loopback.
|
||||||
|
|
||||||
|
:param hostname: Hostname to check
|
||||||
|
:type hostname: str
|
||||||
|
:return: True if all resolved IPs are loopback, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
ips = cls.resolve_hostname_to_ip(hostname)
|
||||||
|
if not ips:
|
||||||
|
return False
|
||||||
|
for ip in ips:
|
||||||
|
if not ip_address(ip).is_loopback:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_ip_and_hostname(cls, input_str: str) -> tuple[str, str]:
|
||||||
|
"""Resolve input string to an (IP, hostname) pair.
|
||||||
|
|
||||||
|
:param input_str: Input which may be an IP address or a hostname
|
||||||
|
:type input_str: str
|
||||||
|
:return: A tuple containing (ip, hostname)
|
||||||
|
:rtype: tuple[str, str]
|
||||||
|
:raises CMAPIBasicError: if hostname resolution yields no IPs
|
||||||
|
"""
|
||||||
|
ip: str = ''
|
||||||
|
hostname: str = None
|
||||||
|
|
||||||
|
if cls.is_ip(input_str):
|
||||||
|
ip = input_str
|
||||||
|
hostname = cls.get_hostname(input_str)
|
||||||
|
else:
|
||||||
|
hostname = input_str
|
||||||
|
ip_list = cls.resolve_hostname_to_ip(
|
||||||
|
input_str,
|
||||||
|
exclude_loopback=not cls.is_only_loopback_hostname(input_str)
|
||||||
|
)
|
||||||
|
if not ip_list:
|
||||||
|
raise CMAPIBasicError(f'No IPs found for {hostname!r}')
|
||||||
|
ip = ip_list[0]
|
||||||
|
return ip, hostname
|
@@ -20,6 +20,7 @@ from cmapi_server.constants import (
|
|||||||
CMAPI_CONF_PATH, CMAPI_SINGLE_NODE_XML, DEFAULT_MCS_CONF_PATH, LOCALHOSTS,
|
CMAPI_CONF_PATH, CMAPI_SINGLE_NODE_XML, DEFAULT_MCS_CONF_PATH, LOCALHOSTS,
|
||||||
MCS_DATA_PATH,
|
MCS_DATA_PATH,
|
||||||
)
|
)
|
||||||
|
from cmapi_server.managers.network import NetworkManager
|
||||||
from mcs_node_control.models.node_config import NodeConfig
|
from mcs_node_control.models.node_config import NodeConfig
|
||||||
|
|
||||||
|
|
||||||
@@ -928,7 +929,7 @@ def _remove_node_from_PMS(root, node):
|
|||||||
|
|
||||||
return pm_num
|
return pm_num
|
||||||
|
|
||||||
def _add_Module_entries(root, node):
|
def _add_Module_entries(root, node: str) -> None:
|
||||||
'''
|
'''
|
||||||
get new node id
|
get new node id
|
||||||
add ModuleIPAddr, ModuleHostName, ModuleDBRootCount (don't set ModuleDBRootID* here)
|
add ModuleIPAddr, ModuleHostName, ModuleDBRootCount (don't set ModuleDBRootID* here)
|
||||||
@@ -937,47 +938,52 @@ def _add_Module_entries(root, node):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# XXXPAT: No guarantee these are the values used in the rest of the system.
|
# XXXPAT: No guarantee these are the values used in the rest of the system.
|
||||||
# This will work best with a simple network configuration where there is 1 IP addr
|
# TODO: what should we do with complicated network configs where node has
|
||||||
# and 1 host name for a node.
|
# several ips and\or several hostnames
|
||||||
ip4 = socket.gethostbyname(node)
|
ip4, hostname = NetworkManager.resolve_ip_and_hostname(node)
|
||||||
if ip4 == node: # node is an IP addr
|
logging.info(f'Using ip address {ip4} and hostname {hostname}')
|
||||||
node_name = socket.gethostbyaddr(node)[0]
|
|
||||||
else:
|
|
||||||
node_name = node # node is a hostname
|
|
||||||
|
|
||||||
logging.info(f"_add_Module_entries(): using ip address {ip4} and hostname {node_name}")
|
smc_node = root.find('./SystemModuleConfig')
|
||||||
|
mod_count_node = smc_node.find('./ModuleCount3')
|
||||||
smc_node = root.find("./SystemModuleConfig")
|
nnid_node = root.find('./NextNodeId')
|
||||||
mod_count_node = smc_node.find("./ModuleCount3")
|
|
||||||
nnid_node = root.find("./NextNodeId")
|
|
||||||
nnid = int(nnid_node.text)
|
nnid = int(nnid_node.text)
|
||||||
current_module_count = int(mod_count_node.text)
|
current_module_count = int(mod_count_node.text)
|
||||||
|
|
||||||
# look for existing entries and fix if they exist
|
# look for existing entries and fix if they exist
|
||||||
for i in range(1, nnid):
|
for i in range(1, nnid):
|
||||||
ip_node = smc_node.find(f"./ModuleIPAddr{i}-1-3")
|
curr_ip_node = smc_node.find(f'./ModuleIPAddr{i}-1-3')
|
||||||
name_node = smc_node.find(f"./ModuleHostName{i}-1-3")
|
curr_name_node = smc_node.find(f'./ModuleHostName{i}-1-3')
|
||||||
# if we find a matching IP address, but it has a different hostname, update the addr
|
# TODO: NETWORK: seems it's useless even in very rare cases.
|
||||||
if ip_node is not None and ip_node.text == ip4:
|
# Even simplier to rewrite resolved IP an Hostname
|
||||||
logging.info(f"_add_Module_entries(): found ip address already at ModuleIPAddr{i}-1-3")
|
# if we find a matching IP address, but it has a different hostname,
|
||||||
hostname = smc_node.find(f"./ModuleHostName{i}-1-3").text
|
# update the addr
|
||||||
if hostname != node_name:
|
if curr_ip_node is not None and curr_ip_node.text == ip4:
|
||||||
new_ip_addr = socket.gethostbyname(hostname)
|
logging.info(f'Found ip address already at ModuleIPAddr{i}-1-3')
|
||||||
logging.info(f"_add_Module_entries(): hostname doesn't match, updating address to {new_ip_addr}")
|
if curr_name_node != hostname:
|
||||||
smc_node.find(f"ModuleHostName{i}-1-3").text = new_ip_addr
|
new_ip_addr = NetworkManager.resolve_hostname_to_ip(
|
||||||
|
curr_name_node
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
'Hostname doesn\'t match, updating address to '
|
||||||
|
f'{new_ip_addr!r}'
|
||||||
|
)
|
||||||
|
smc_node.find(f'ModuleHostName{i}-1-3').text = new_ip_addr
|
||||||
else:
|
else:
|
||||||
logging.info(f"_add_Module_entries(): no update is necessary")
|
logging.info('No update for ModuleIPAddr{i}-1-3 is necessary')
|
||||||
return
|
return
|
||||||
|
|
||||||
# if we find a matching hostname, update the ip addr
|
# if we find a matching hostname, update the ip addr
|
||||||
if name_node is not None and name_node.text == node_name:
|
if curr_name_node is not None and curr_name_node.text == hostname:
|
||||||
logging.info(f"_add_Module_entries(): found existing entry for {node_name}, updating its address to {ip4}")
|
logging.info(
|
||||||
ip_node.text = ip4
|
f'Found existing entry for {hostname!r}, updating its '
|
||||||
|
f'address to {ip4!r}'
|
||||||
|
)
|
||||||
|
curr_ip_node.text = ip4
|
||||||
return
|
return
|
||||||
|
|
||||||
etree.SubElement(smc_node, f"ModuleIPAddr{nnid}-1-3").text = ip4
|
etree.SubElement(smc_node, f'ModuleIPAddr{nnid}-1-3').text = ip4
|
||||||
etree.SubElement(smc_node, f"ModuleHostName{nnid}-1-3").text = node_name
|
etree.SubElement(smc_node, f'ModuleHostName{nnid}-1-3').text = hostname
|
||||||
etree.SubElement(smc_node, f"ModuleDBRootCount{nnid}-3").text = "0"
|
etree.SubElement(smc_node, f'ModuleDBRootCount{nnid}-3').text = '0'
|
||||||
mod_count_node.text = str(current_module_count + 1)
|
mod_count_node.text = str(current_module_count + 1)
|
||||||
nnid_node.text = str(nnid + 1)
|
nnid_node.text = str(nnid + 1)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user