diff --git a/lib/ansible/module_utils/facts/system/local.py b/lib/ansible/module_utils/facts/system/local.py index 3d656f5a345..ac887b3599f 100644 --- a/lib/ansible/module_utils/facts/system/local.py +++ b/lib/ansible/module_utils/facts/system/local.py @@ -1,17 +1,5 @@ -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations diff --git a/lib/ansible/module_utils/facts/system/platform.py b/lib/ansible/module_utils/facts/system/platform.py index 94819861b4b..c3c131b1c90 100644 --- a/lib/ansible/module_utils/facts/system/platform.py +++ b/lib/ansible/module_utils/facts/system/platform.py @@ -1,17 +1,5 @@ -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations @@ -84,9 +72,10 @@ class PlatformFactCollector(BaseFactCollector): platform_facts['architecture'] = data[0] else: bootinfo_bin = module.get_bin_path('bootinfo') - rc, out, err = module.run_command([bootinfo_bin, '-p']) - data = out.splitlines() - platform_facts['architecture'] = data[0] + if bootinfo_bin is not None: + rc, out, err = module.run_command([bootinfo_bin, '-p']) + data = out.splitlines() + platform_facts['architecture'] = data[0] elif platform_facts['system'] == 'OpenBSD': platform_facts['architecture'] = platform.uname()[5] diff --git a/test/units/module_utils/facts/system/test_chroot.py b/test/units/module_utils/facts/system/test_chroot.py index 0baa78fd249..37d9a06ce5f 100644 --- a/test/units/module_utils/facts/system/test_chroot.py +++ b/test/units/module_utils/facts/system/test_chroot.py @@ -3,16 +3,12 @@ from __future__ import annotations -import stat -from collections import namedtuple +from os import stat_result import pytest - from ansible.module_utils.facts.system.chroot import ChrootFactCollector -MOCKSTAT = namedtuple("mock_stat", ["st_dev", "st_ino", "st_size", "st_mtime"]) - class TestChrootFacts: def test_debian_chroot(self, monkeypatch): @@ -24,22 +20,19 @@ class TestChrootFacts: mocker.patch( "os.stat", side_effect=[ - MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0), - MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=3, st_size=1, st_mtime=0), + stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]), + stat_result([0, 3, 0, 0, 0, 0, 0, 0, 0, 0]), ], ) chroot_facts = ChrootFactCollector().collect() assert chroot_facts["is_chroot"] is True - def _mock_os_stat_exception(self): - raise Exception("fake os.stat exception") - def test_detect_chroot_no_btrfs_no_xfs(self, mocker): mocker.patch( "os.stat", side_effect=[ - MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0), - self._mock_os_stat_exception, + stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]), + Exception("fake os.stat exception"), ], ) chroot_facts = ChrootFactCollector().collect() @@ -49,8 +42,8 @@ class TestChrootFacts: mocker.patch( "os.stat", side_effect=[ - MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0), - self._mock_os_stat_exception, + stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]), + Exception("fake os.stat exception"), ], ) @@ -75,8 +68,8 @@ class TestChrootFacts: mocker.patch( "os.stat", side_effect=[ - MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0), - self._mock_os_stat_exception, + stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]), + Exception("fake os.stat exception"), ], ) mocker.patch.object(module, "get_bin_path", return_value="/usr/bin/stat") diff --git a/test/units/module_utils/facts/system/test_local.py b/test/units/module_utils/facts/system/test_local.py new file mode 100644 index 00000000000..7732fe0a531 --- /dev/null +++ b/test/units/module_utils/facts/system/test_local.py @@ -0,0 +1,77 @@ +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + +from os import stat_result + +from ansible.module_utils.facts.system.local import LocalFactCollector + + +class TestLocalFacts: + def test_local_no_module(self): + local_facts = LocalFactCollector().collect() + assert local_facts == {"local": {}} + + def test_local_no_fact_path_exists(self, mocker): + module = mocker.Mock() + mocker.patch("os.path.exists", return_value=False) + local_facts = LocalFactCollector().collect(module=module) + assert local_facts == {"local": {}} + + def test_local_facts(self, mocker): + module = mocker.MagicMock() + module.params = {"fact_path": "/usr/local/facts"} + mocker.patch("os.path.exists", return_value=True) + mocker.patch("glob.glob", return_value=["/usr/local/facts/sample.fact"]) + local_facts = LocalFactCollector().collect(module=module) + assert "Could not stat fact" in local_facts["local"]["sample"] + + mock_stat = stat_result([mocker.MagicMock()] * 10) + mocker.patch("os.stat", return_value=mock_stat) + mocker.patch.object(module, "run_command", return_value=(1, "", "failed")) + local_facts = LocalFactCollector().collect(module=module) + assert "Failure executing" in local_facts["local"]["sample"] + + mock_output = """{"defaults": {"foo": "bar"}}""" + mocker.patch.object(module, "run_command", return_value=(0, mock_output, "")) + local_facts = LocalFactCollector().collect(module=module) + assert local_facts["local"]["sample"]["defaults"]["foo"] == "bar" + + mock_config_output = "foo=bar\n" + mocker.patch.object( + module, "run_command", return_value=(0, mock_config_output, "") + ) + local_facts = LocalFactCollector().collect(module=module) + assert "error loading facts as JSON or ini" in local_facts["local"]["sample"] + + mock_config_output = "[defaults]\nfoo=bar\n" + mocker.patch.object( + module, "run_command", return_value=(0, mock_config_output, "") + ) + local_facts = LocalFactCollector().collect(module=module) + assert local_facts["local"]["sample"]["defaults"]["foo"] == "bar" + + mock_config_output = "[defaults]\n" + mocker.patch.object( + module, "run_command", return_value=(0, mock_config_output, "") + ) + local_facts = LocalFactCollector().collect(module=module) + assert local_facts["local"]["sample"]["defaults"] == {} + + mock_config_output = "[defaults]\n" + mocker.patch.object( + module, "run_command", return_value=(0, mock_config_output, "") + ) + local_facts = LocalFactCollector().collect(module=module) + assert local_facts["local"]["sample"]["defaults"] == {} + + mocker.patch.object(module, "run_command", return_value=(0, "", "")) + mocker.patch( + "json.loads", side_effect=Exception("fake _mock_json_load exception") + ) + local_facts = LocalFactCollector().collect(module=module) + assert ( + "Failed to convert (/usr/local/facts/sample.fact)" + in local_facts["local"]["sample"] + ) diff --git a/test/units/module_utils/facts/system/test_platform.py b/test/units/module_utils/facts/system/test_platform.py new file mode 100644 index 00000000000..5f8df6dfb25 --- /dev/null +++ b/test/units/module_utils/facts/system/test_platform.py @@ -0,0 +1,122 @@ +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + +import re + +import pytest + +from ansible.module_utils.facts.system.platform import PlatformFactCollector + +SOLARIS_I86_RE_PATTERN = re.compile(r"i([3456]86|86pc)") + + +class TestPlatformFacts: + def test_platform_system(self, mocker): + mocker.patch("platform.system", return_value="Darwin") + mocker.patch("platform.release", return_value="23.5.0") + mocker.patch("platform.python_version", return_value="3.11.4") + mocker.patch("socket.getfqdn", return_value="localhost.localdomain") + mocker.patch("platform.node", return_value="localhost.localdomain") + mac_kernel_ver = "Darwin Kernel Version 23.5.0: Wed May 1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000" + mocker.patch("platform.version", return_value=mac_kernel_ver) + mocker.patch("platform.machine", return_value="arm64") + mock_machine_id = "fe31eab802474047fd1e9f8ca050234b" + mocker.patch( + "ansible.module_utils.facts.system.platform.get_file_content", + return_value=mock_machine_id, + ) + platform_facts = PlatformFactCollector().collect() + + assert "system" in platform_facts + assert "kernel" in platform_facts + assert "kernel_version" in platform_facts + assert "python_version" in platform_facts + assert "fqdn" in platform_facts + assert "hostname" in platform_facts + assert "nodename" in platform_facts + assert "domain" in platform_facts + assert "machine" in platform_facts + assert "architecture" in platform_facts + + assert platform_facts["system"] == "Darwin" + assert platform_facts["kernel"] == "23.5.0" + assert "Darwin" in platform_facts["kernel_version"] + assert platform_facts["python_version"] == "3.11.4" + assert platform_facts["fqdn"] == "localhost.localdomain" + assert platform_facts["hostname"] == "localhost" + assert platform_facts["nodename"] == "localhost.localdomain" + assert platform_facts["domain"] == "localdomain" + assert platform_facts["machine"] == "arm64" + assert platform_facts["architecture"] == "arm64" + assert platform_facts["machine_id"] == mock_machine_id + + @pytest.mark.parametrize( + "platform_machine", + [ + pytest.param("AMD64", id="amd64"), + pytest.param("aarch64", id="arm64"), + pytest.param("aarch64", id="armhf"), + pytest.param("armv7l", id="armhf"), + pytest.param("ppc", id="powerpc"), + pytest.param("ppc64le", id="ppc64el"), + pytest.param("x86_64", id="amd64"), + pytest.param("x86_64", id="i386"), + pytest.param("s390x", id="s390x"), + pytest.param("riscv64", id="riscv64"), + pytest.param("unknownarch", id="unknown-arch"), + pytest.param("i386", id="solaris-i386"), + pytest.param("i386", id="solaris-i386-64"), + ], + ) + def test_platform_machine(self, mocker, platform_machine): + platform_facts = PlatformFactCollector().collect() + mocker.patch("platform.machine", return_value=platform_machine) + assert "machine" in platform_facts + assert "userspace_bits" in platform_facts + assert "architecture" in platform_facts + + if platform_facts["machine"] == "x86_64": + assert platform_facts["architecture"] == platform_facts["machine"] + assert "userspace_architecture" in platform_facts + if platform_facts["userspace_bits"] == "64": + assert platform_facts["userspace_architecture"] == "x86_64" + elif platform_facts["userspace_bits"] == "32": + assert platform_facts["userspace_architecture"] == "i386" + elif SOLARIS_I86_RE_PATTERN.search(platform_facts["machine"]): + assert platform_facts["architecture"] == "i386" + if platform_facts["userspace_bits"] == "64": + assert platform_facts["userspace_architecture"] == "x86_64" + elif platform_facts["userspace_bits"] == "32": + assert platform_facts["userspace_architecture"] == "i386" + else: + assert platform_facts["architecture"] == platform_facts["machine"] + + def test_platform_aix(self, mocker): + module = mocker.MagicMock() + mocker.patch("platform.system", return_value="AIX") + mocker.patch.object(module, "get_bin_path", return_value="/usr/bin/getconf") + mocker.patch.object(module, "run_command", return_value=(0, "chrp\n", "")) + platform_facts = PlatformFactCollector().collect(module=module) + assert platform_facts["architecture"] == "chrp" + + mocker.patch.object(module, "get_bin_path", side_effect=[None, "fake/bootinfo"]) + platform_facts = PlatformFactCollector().collect(module=module) + assert platform_facts["architecture"] == "chrp" + + def test_platform_openbsd(self, mocker): + module = mocker.MagicMock() + mocker.patch("platform.system", return_value="OpenBSD") + mocker.patch("platform.release", return_value="7.4") + mocker.patch("platform.version", return_value="7.4") + mocker.patch("socket.getfqdn", return_value="localhost.localdomain") + mocker.patch("platform.node", return_value="localhost.localdomain") + mocker.patch("platform.architecture", return_value=("64bit", "ELF")) + mocker.patch("platform.machine", return_value="amd64") + mocker.patch( + "platform.uname", + return_value=["OpenBSD", "openbsd", "7.4", "7.4", "amd64", "amd64"], + ) + platform_facts = PlatformFactCollector().collect(module=module) + assert platform_facts["architecture"] == "amd64"