diff --git a/tobiko/shell/sh/__init__.py b/tobiko/shell/sh/__init__.py index 711f30bec..b32543469 100644 --- a/tobiko/shell/sh/__init__.py +++ b/tobiko/shell/sh/__init__.py @@ -24,6 +24,7 @@ from tobiko.shell.sh import _hostname from tobiko.shell.sh import _io from tobiko.shell.sh import _local from tobiko.shell.sh import _nameservers +from tobiko.shell.sh import _nmcli from tobiko.shell.sh import _path from tobiko.shell.sh import _process from tobiko.shell.sh import _ps @@ -135,3 +136,6 @@ get_file_size = _wc.get_file_size CommandNotFound = _which.CommandNotFound SkipOnCommandNotFound = _which.SkipOnCommandNotFound find_command = _which.find_command + +get_nm_connection_ids = _nmcli.get_nm_connection_ids +get_nm_connection_values = _nmcli.get_nm_connection_values diff --git a/tobiko/shell/sh/_nameservers.py b/tobiko/shell/sh/_nameservers.py index 841f5d9b3..285837f4d 100644 --- a/tobiko/shell/sh/_nameservers.py +++ b/tobiko/shell/sh/_nameservers.py @@ -59,10 +59,23 @@ def list_nameservers(ssh_client: typing.Optional[ssh.SSHClientFixture] = None, filenames = ['/etc/resolv.conf'] nameservers: tobiko.Selection[netaddr.IPAddress] = tobiko.Selection() + # obtain nameservers from the resolv.conf file for filename in filenames: nameservers.extend(parse_resolv_conf_file(ssh_client=ssh_client, filename=filename, **execute_params)) + + # obtain nameservers from nmcli, if available + try: + sh.find_command('nmcli', ssh_client=ssh_client) + except sh.CommandNotFound: + msg = 'nmcli command not available' + if ssh_client: + msg += f' on {ssh_client.host}' + LOG.debug(msg) + else: + nameservers.extend(parse_dns_nmcli(ssh_client=ssh_client)) + if ip_version: nameservers = nameservers.with_attributes(version=ip_version) return nameservers @@ -71,8 +84,7 @@ def list_nameservers(ssh_client: typing.Optional[ssh.SSHClientFixture] = None, def parse_resolv_conf_file( filename: str, ssh_client: typing.Optional[ssh.SSHClientFixture] = None, - **execute_params) -> \ - typing.Generator[netaddr.IPAddress, None, None]: + **execute_params) -> typing.Generator[netaddr.IPAddress, None, None]: lines: typing.List[str] = \ sh.execute(f"cat '{filename}'", ssh_client=ssh_client, @@ -94,3 +106,18 @@ def parse_resolv_conf_file( yield netaddr.IPAddress(nameserver) except netaddr.AddrFormatError: LOG.exception(f"Invalid nameserver address: {nameserver}") + + +def parse_dns_nmcli( + ssh_client: typing.Optional[ssh.SSHClientFixture] = None) -> \ + typing.Generator[netaddr.IPAddress, None, None]: + connections = sh.get_nm_connection_ids(ssh_client=ssh_client) + for connection in connections: + nameservers = sh.get_nm_connection_values(connection, + 'IP4.DNS,IP6.DNS', + ssh_client=ssh_client) + for nameserver in nameservers: + try: + yield netaddr.IPAddress(nameserver) + except netaddr.AddrFormatError: + LOG.exception(f"Invalid nameserver address: {nameserver}") diff --git a/tobiko/shell/sh/_nmcli.py b/tobiko/shell/sh/_nmcli.py new file mode 100644 index 000000000..00df108f4 --- /dev/null +++ b/tobiko/shell/sh/_nmcli.py @@ -0,0 +1,45 @@ +# Copyright (c) 2025 Red Hat, Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +from oslo_log import log + +from tobiko.shell.sh import _execute +from tobiko.shell import ssh + + +LOG = log.getLogger(__name__) + + +def get_nm_connection_ids(ssh_client: ssh.SSHClientType = None) -> list: + result = _execute.execute('nmcli -g UUID con', + ssh_client=ssh_client) + return result.stdout.splitlines() + + +def get_nm_connection_values(connection: str, + values: str, + ssh_client: ssh.SSHClientType = None) -> list: + result = _execute.execute(f'nmcli -g {values} con show "{connection}"', + ssh_client=ssh_client) + return_values = [] + for line in result.stdout.splitlines(): + if line: + for value in line.split('|'): + # nmcli adds escape char before ":" and we need to remove it + return_values.append(value.strip().replace('\\', '')) + + return return_values diff --git a/tobiko/tests/functional/openstack/stacks/test_advanced_vm.py b/tobiko/tests/functional/openstack/stacks/test_advanced_vm.py index 83dc450b9..77590a754 100644 --- a/tobiko/tests/functional/openstack/stacks/test_advanced_vm.py +++ b/tobiko/tests/functional/openstack/stacks/test_advanced_vm.py @@ -15,6 +15,8 @@ # under the License. from __future__ import absolute_import +import typing + import tobiko from tobiko.shell import sh from tobiko.openstack import stacks @@ -27,7 +29,7 @@ class AdvancedServerStackTest(test_cirros.CirrosServerStackTest): #: Stack of resources with a server attached to a floating IP stack = tobiko.required_fixture(stacks.AdvancedServerStackFixture) - nameservers_filenames = ('/etc/resolv.conf',) + nameservers_filenames: typing.Optional[typing.Sequence[str]] = [] def test_python(self): python_version = sh.execute(['python3', '--version'],