diff --git a/changelog/69036.added.md b/changelog/69036.added.md new file mode 100644 index 000000000000..aca967b55c77 --- /dev/null +++ b/changelog/69036.added.md @@ -0,0 +1 @@ +Added UUID detection for Xen paravirtualized guests diff --git a/salt/grains/core.py b/salt/grains/core.py index 5a4ae1ae671e..2e5291d5f1cb 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -3259,6 +3259,26 @@ def _hw_data(osdata): return {} grains = {} + + # For Xen para-virtualized guests read UUID from /sys/hypervisor/uuid. + # This file also exists on Xen Dom0 but contains all-zeros; skip that + # sentinel value so the real DMI/smbios UUID is used on Dom0 hosts. + if osdata["kernel"] == "Linux" and os.path.exists("/sys/hypervisor/uuid"): + try: + with salt.utils.files.fopen("/sys/hypervisor/uuid", "rb") as ifile: + hypervisor_uuid = salt.utils.stringutils.to_unicode( + ifile.read().strip(), errors="replace" + ).lower() + # All-zero UUID is the Dom0 sentinel; ignore it. + if hypervisor_uuid and hypervisor_uuid.strip("0-"): + grains["uuid"] = hypervisor_uuid + log.debug( + "Read UUID from /sys/hypervisor/uuid for para-virtualized guest: %s", + grains["uuid"], + ) + except OSError as err: + log.debug("Unable to read /sys/hypervisor/uuid: %s", err) + if osdata["kernel"] == "Linux" and os.path.exists("/sys/class/dmi/id"): # On many Linux distributions basic firmware information is available via sysfs # requires CONFIG_DMIID to be enabled in the Linux kernel configuration @@ -3273,6 +3293,9 @@ def _hw_data(osdata): "serialnumber": "product_serial", } for key, fw_file in sysfs_firmware_info.items(): + # Skip UUID if already read from /sys/hypervisor/uuid (Xen PV guests) + if key == "uuid" and "uuid" in grains: + continue contents_file = os.path.join("/sys/class/dmi/id", fw_file) if os.path.exists(contents_file): try: @@ -3303,18 +3326,20 @@ def _hw_data(osdata): ): # On SmartOS (possibly SunOS also) smbios only works in the global zone # smbios is also not compatible with linux's smbios (smbios -s = print summarized) + uuid = __salt__["smbios.get"]("system-uuid") + if uuid is not None: + uuid = uuid.lower() + else: + uuid = grains.get("uuid") grains = { "biosversion": __salt__["smbios.get"]("bios-version"), "biosvendor": __salt__["smbios.get"]("bios-vendor"), "productname": __salt__["smbios.get"]("system-product-name"), "manufacturer": __salt__["smbios.get"]("system-manufacturer"), "biosreleasedate": __salt__["smbios.get"]("bios-release-date"), - "uuid": __salt__["smbios.get"]("system-uuid"), + "uuid": uuid, } grains = {key: val for key, val in grains.items() if val is not None} - uuid = __salt__["smbios.get"]("system-uuid") - if uuid is not None: - grains["uuid"] = uuid.lower() for serial in ( "system-serial-number", "chassis-serial-number", diff --git a/tests/pytests/unit/grains/test_core.py b/tests/pytests/unit/grains/test_core.py index 1a1bb7632c27..c92c28d1309a 100644 --- a/tests/pytests/unit/grains/test_core.py +++ b/tests/pytests/unit/grains/test_core.py @@ -3560,6 +3560,50 @@ def read(self): assert core._hw_data({"kernel": "Linux"}) == {} +@pytest.mark.skip_unless_on_linux +def test__hw_data_xen_pv_uuid(): + hypervisor_uuid = b"12345678-1234-1234-1234-123456789ABC" + expected_uuid = "12345678-1234-1234-1234-123456789abc" + + def _exists_side_effect(path): + if path == "/sys/hypervisor/uuid": + return True + return False + + with patch("os.path.exists", side_effect=_exists_side_effect), patch( + "salt.utils.platform.is_proxy", return_value=False + ), patch("salt.utils.path.which_bin", return_value=None), patch( + "salt.utils.files.fopen", + mock_open(read_data=hypervisor_uuid), + ): + result = core._hw_data({"kernel": "Linux"}) + assert result.get("uuid") == expected_uuid + + +@pytest.mark.skip_unless_on_linux +def test__hw_data_xen_dom0_uuid_ignored(): + """ + On Xen Dom0, /sys/hypervisor/uuid contains an all-zero sentinel value. + The grain should not be set from that value so the real DMI UUID can + be used instead. + """ + dom0_uuid = b"00000000-0000-0000-0000-000000000000" + + def _exists_side_effect(path): + if path == "/sys/hypervisor/uuid": + return True + return False + + with patch("os.path.exists", side_effect=_exists_side_effect), patch( + "salt.utils.platform.is_proxy", return_value=False + ), patch("salt.utils.path.which_bin", return_value=None), patch( + "salt.utils.files.fopen", + mock_open(read_data=dom0_uuid), + ): + result = core._hw_data({"kernel": "Linux"}) + assert result.get("uuid") is None + + @pytest.mark.skip_unless_on_windows def test_kernelparams_return_windows(): """