From b3a11e966d58cbd1e3cd91407f32be633260c766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yeray=20Guti=C3=A9rrez=20Cedr=C3=A9s?= Date: Thu, 30 Apr 2026 08:11:25 +0100 Subject: [PATCH 1/4] Calculate UUID grain for Xen PV guests --- changelog/69036.added.md | 1 + salt/grains/core.py | 20 ++++++++++++++++++++ tests/pytests/unit/grains/test_core.py | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 changelog/69036.added.md 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..01a1fe8be2d9 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -3259,6 +3259,23 @@ def _hw_data(osdata): return {} grains = {} + + # For Xen para-virtualized guests read UUID from /sys/hypervisor/uuid + if osdata["kernel"] == "Linux" and os.path.exists("/sys/hypervisor/uuid"): + try: + with salt.utils.files.fopen("/sys/hypervisor/uuid", "r") as ifile: + hypervisor_uuid = salt.utils.stringutils.to_unicode( + ifile.read().strip(), errors="replace" + ) + # Normalize to lowercase to match DMI format + grains["uuid"] = hypervisor_uuid.lower() + 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 +3290,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: diff --git a/tests/pytests/unit/grains/test_core.py b/tests/pytests/unit/grains/test_core.py index 1a1bb7632c27..3e0805007769 100644 --- a/tests/pytests/unit/grains/test_core.py +++ b/tests/pytests/unit/grains/test_core.py @@ -3560,6 +3560,26 @@ def read(self): assert core._hw_data({"kernel": "Linux"}) == {} +@pytest.mark.skip_unless_on_linux +def test__hw_data_xen_pv_uuid(): + hypervisor_uuid = "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_windows def test_kernelparams_return_windows(): """ From fec65ef6415095271e8550dd14cb5015055f1f6b Mon Sep 17 00:00:00 2001 From: vzhestkov Date: Tue, 12 May 2026 09:27:12 +0200 Subject: [PATCH 2/4] Fix the case when either dmidecode or smbios is available but /sys/class/dmi/id is missing --- salt/grains/core.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index 01a1fe8be2d9..a030a93a134b 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -3323,18 +3323,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", From 2b8306ec0f5bc6759ede2b25ee5810a8cd37508e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yeray=20Guti=C3=A9rrez=20Cedr=C3=A9s?= Date: Wed, 27 May 2026 09:10:11 +0100 Subject: [PATCH 3/4] Refine Xen PV UUID detection The unit test `test__hw_data_linux_empty` was failing because of existing mocks for fopen in rb mode. Switching to rb does no harm because of the use of `to_unicode` of the `/sys/hypervisor/uuid` file contents. --- salt/grains/core.py | 14 +++++++------- tests/pytests/unit/grains/test_core.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index a030a93a134b..d7f84406d4d4 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -3263,16 +3263,16 @@ def _hw_data(osdata): # For Xen para-virtualized guests read UUID from /sys/hypervisor/uuid if osdata["kernel"] == "Linux" and os.path.exists("/sys/hypervisor/uuid"): try: - with salt.utils.files.fopen("/sys/hypervisor/uuid", "r") as ifile: + with salt.utils.files.fopen("/sys/hypervisor/uuid", "rb") as ifile: hypervisor_uuid = salt.utils.stringutils.to_unicode( ifile.read().strip(), errors="replace" ) - # Normalize to lowercase to match DMI format - grains["uuid"] = hypervisor_uuid.lower() - log.debug( - "Read UUID from /sys/hypervisor/uuid for para-virtualized guest: %s", - grains["uuid"], - ) + if hypervisor_uuid: + grains["uuid"] = hypervisor_uuid.lower() + 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) diff --git a/tests/pytests/unit/grains/test_core.py b/tests/pytests/unit/grains/test_core.py index 3e0805007769..432532baeced 100644 --- a/tests/pytests/unit/grains/test_core.py +++ b/tests/pytests/unit/grains/test_core.py @@ -3562,7 +3562,7 @@ def read(self): @pytest.mark.skip_unless_on_linux def test__hw_data_xen_pv_uuid(): - hypervisor_uuid = "12345678-1234-1234-1234-123456789ABC" + hypervisor_uuid = b"12345678-1234-1234-1234-123456789ABC" expected_uuid = "12345678-1234-1234-1234-123456789abc" def _exists_side_effect(path): From ebd9ca7535d8fbf800369461c2f88446a883e247 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Fri, 19 Jun 2026 02:37:26 -0700 Subject: [PATCH 4/4] Skip all-zero UUID from /sys/hypervisor/uuid on Xen Dom0 /sys/hypervisor/uuid exists on Xen Dom0 as well as DomU, but Dom0 exposes the sentinel value 00000000-0000-0000-0000-000000000000. Reading that value would overwrite the real DMI uuid grain on Dom0 hosts. Skip the value when it consists only of zeros and dashes so that Dom0 systems fall through to the normal DMI/smbios UUID path. Add a regression test covering the Dom0 case. --- salt/grains/core.py | 11 +++++++---- tests/pytests/unit/grains/test_core.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index d7f84406d4d4..2e5291d5f1cb 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -3260,15 +3260,18 @@ def _hw_data(osdata): grains = {} - # For Xen para-virtualized guests read UUID from /sys/hypervisor/uuid + # 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" - ) - if hypervisor_uuid: - grains["uuid"] = hypervisor_uuid.lower() + ).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"], diff --git a/tests/pytests/unit/grains/test_core.py b/tests/pytests/unit/grains/test_core.py index 432532baeced..c92c28d1309a 100644 --- a/tests/pytests/unit/grains/test_core.py +++ b/tests/pytests/unit/grains/test_core.py @@ -3580,6 +3580,30 @@ def _exists_side_effect(path): 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(): """