From 940c697963027d7746c0778b15aec522df232232 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 1 Dec 2025 10:43:30 -0700 Subject: [PATCH 01/15] feat(lxd): add s390x virtio-ports detection for LXD Support LXD detection on IBM's s390x architecture using virtio-ports serial device detection. Add a new detection method that checks for LXD serial devices in /sys/class/virtio-ports/ for both current (com.canonical.lxd) and legacy (org.linuxcontainers.lxd) names. Update public LXD datasource documentation for this detection method. Drop the now unnecessary DMI board name check from ds-identify and DataSourceLXD because virtio-ports is sufficient in bot KVM/QEMU and non-x86_64 platforms without DMI information. The following mechinisms for LXD detection are retained: 1. /dev/lxd/sock socket file (primary method) 2. virtio-ports serial device presence if socket file isn't yet present Implements: PL073 --- cloudinit/sources/DataSourceLXD.py | 28 +++-- doc/rtd/reference/datasources/lxd.rst | 10 +- .../datasources/test_lxd_discovery.py | 41 ++++++- tests/unittests/sources/test_lxd.py | 74 +++++++++++-- tests/unittests/test_ds_identify.py | 102 +++++++++++++++--- tools/ds-identify | 30 ++++-- 6 files changed, 235 insertions(+), 50 deletions(-) diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index 0f02c85e556..36a3ab7d088 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -196,13 +196,29 @@ def _unpickle(self, ci_pkl_version: int) -> None: @staticmethod def ds_detect() -> bool: """Check platform environment to report if this datasource may run.""" - if not os.path.exists(LXD_SOCKET_PATH): - LOG.warning("%s does not exist.", LXD_SOCKET_PATH) - return False - elif not stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode): + if os.path.exists(LXD_SOCKET_PATH): + if stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode): + return True LOG.warning("%s is not a socket", LXD_SOCKET_PATH) - return False - return True + + # On LXD KVM instances /dev/lxd/sock may not be available yet. + # Check for LXD virtio serial device presence in virtio-ports. + virtio_ports_path = "/sys/class/virtio-ports" + if os.path.isdir(virtio_ports_path): + try: + for port in os.listdir(virtio_ports_path): + name_file = os.path.join(virtio_ports_path, port, "name") + if os.path.isfile(name_file): + # Check for both current and legacy LXD serial names + if util.load_text_file(name_file).strip() in ( + "com.canonical.lxd", + "org.linuxcontainers.lxd", + ): + return True + except (OSError, IOError) as e: + LOG.warning("Cannot check virtio-ports: %s", e) + + return False def _get_data(self) -> bool: """Crawl LXD socket API instance data and return True on success""" diff --git a/doc/rtd/reference/datasources/lxd.rst b/doc/rtd/reference/datasources/lxd.rst index b1b09b80471..32388333340 100644 --- a/doc/rtd/reference/datasources/lxd.rst +++ b/doc/rtd/reference/datasources/lxd.rst @@ -7,7 +7,7 @@ The LXD datasource allows the user to provide custom user-data, vendor-data, meta-data and network-config to the instance without running a network service (or even without having a network at all). This datasource performs HTTP GETs against the `LXD socket device`_ which is provided to each -running LXD container and VM as ``/dev/lxd/sock`` and represents all +running LXD container and VM as :file:`/dev/lxd/sock` and represents all instance-meta-data as versioned HTTP routes such as: - 1.0/meta-data @@ -15,8 +15,8 @@ instance-meta-data as versioned HTTP routes such as: - 1.0/config/cloud-init.user-data - 1.0/config/user. -The LXD socket device ``/dev/lxd/sock`` is only present on containers and VMs -when the instance configuration has ``security.devlxd=true`` (default). +The LXD socket device :file:`/dev/lxd/sock` is only present on containers and +VMs when the instance configuration has ``security.devlxd=true`` (default). Disabling the ``security.devlxd`` configuration setting at initial launch will ensure that ``cloud-init`` uses the :ref:`datasource_nocloud` datasource. Disabling ``security.devlxd`` over the life of the container will result in @@ -24,8 +24,8 @@ warnings from ``cloud-init``, and ``cloud-init`` will keep the originally-detected LXD datasource. The LXD datasource is detected as viable by ``ds-identify`` during the -:ref:`detect stage` when either ``/dev/lxd/sock`` exists or -``/sys/class/dmi/id/board_name`` matches "LXD". +:ref:`detect stage` when either :file:`/dev/lxd/sock` exists +or an LXD serial device is present in :file:`/sys/class/virtio-ports`. The LXD datasource provides ``cloud-init`` with the ability to react to meta-data, vendor-data, user-data and network-config changes, and to render the diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index c468eb4384e..44090862a96 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -3,6 +3,7 @@ import pytest import yaml +from tests.integration_tests.clouds import IntegrationCloud from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU @@ -20,14 +21,17 @@ def _customize_environment(client: IntegrationInstance): if client.settings.PLATFORM == "lxd_vm": # ds-identify runs at systemd generator time before /dev/lxd/sock. - # Assert we can expected artifact which indicates LXD is viable. - result = client.execute("cat /sys/class/dmi/id/board_name") + # Assert we can expected virtio-ports artifacts which indicates LXD is + # viable. + result = client.execute("cat /sys/class/virtio-ports/*/name") if not result.ok: raise AssertionError( - "Missing expected /sys/class/dmi/id/board_name" + "Missing expected /sys/class/virtio-ports/*/name" ) if "LXD" != result.stdout: - raise AssertionError(f"DMI board_name is not LXD: {result.stdout}") + raise AssertionError( + f"virtio-ports not LXD serial devices: {result.stdout}" + ) # Having multiple datasources prevents ds-identify from short-circuiting # detection logic with a log like: @@ -53,7 +57,34 @@ def _customize_environment(client: IntegrationInstance): client.restart() -@pytest.mark.skipif(not IS_UBUNTU, reason="Netplan usage") +@pytest.mark.skipif(PLATFORM != "lxd_vm", reason="Test is LXD KVM specific") +def test_lxd_kvm_datasource_discovery_without_lxd_socket( + session_cloud: IntegrationCloud, +): + """Test DataSourceLXD is detected on KVM by virtio-ports.""" + with session_cloud.launch( + wait=False, # to prevent cloud-init status --wait + launch_kwargs={ + # We detect the LXD datasource using a socket available to the + # container. This prevents the socket from being exposed in the + # container, so LXD will not be detected. + # This allows us to wait for detection in 'init' stage with + # DataSourceNoCloudNet. + "config_dict": {"security.devlxd": False}, + }, + ) as client: + _customize_environment(client) + # We know this will be an LXD instance due to our pytest mark + client.instance.execute_via_ssh = False # pyright: ignore + result = wait_for_cloud_init(client, num_retries=60) + if not result.ok: + raise AssertionError("cloud-init failed:\n%s", result.stderr) + if "DataSourceLXD" not in result.stdout: + raise AssertionError( + "cloud-init did not discover DataSourceLXD", result.stdout + ) + + @pytest.mark.skipif( PLATFORM not in ["lxd_container", "lxd_vm"], reason="Test is LXD specific", diff --git a/tests/unittests/sources/test_lxd.py b/tests/unittests/sources/test_lxd.py index 226ed842938..29638868f94 100644 --- a/tests/unittests/sources/test_lxd.py +++ b/tests/unittests/sources/test_lxd.py @@ -2,6 +2,7 @@ import copy import json +import logging import re import stat from collections import namedtuple @@ -15,6 +16,8 @@ from cloudinit.sources import DataSourceLXD as lxd from cloudinit.sources import InvalidMetaDataException from cloudinit.sources.DataSourceLXD import MetaDataKeys +from cloudinit.util import ensure_file +from tests.unittests.helpers import populate_dir DS_PATH = "cloudinit.sources.DataSourceLXD." @@ -356,28 +359,83 @@ def test_network_config_crawled_metadata_no_network_config( class TestIsPlatformViable: @pytest.mark.parametrize( - "exists,lstat_mode,expected", + "exists,lstat_mode,virtio_ports,expected", ( - (False, None, False), - (True, stat.S_IFREG, False), - (True, stat.S_IFSOCK, True), + pytest.param( + False, + None, + {}, + False, + id="not_viable_no_lxd_sock_path_no_virtio", + ), + pytest.param( + True, + stat.S_IFREG, + {}, + False, + id="not_viable_lxd_sock_path_regular_file_no_virtio", + ), + pytest.param( + True, + stat.S_IFSOCK, + {}, + True, + id="viable_when_lxd_sock_is_socket_file_no_virtio", + ), + pytest.param( + False, + None, + {"vport5p1/name": "com.redhat.spice.0"}, + False, + id="not_viable_no_lxd_sock_with_non_lxd_virtio", + ), + pytest.param( + False, + None, + { + "vport5p1/name": "com.redhat.spice.0", + "vport5p2/name": "org.linuxcontainers.lxd", + }, + True, + id="viable_no_lxd_sock_with_legacy_lxd_virtio", + ), + pytest.param( + False, + None, + { + "vport5p1/name": "com.redhat.spice.0", + "vport5p2/name": "com.canonical.lxd", + }, + True, + id="viable_no_lxd_sock_with_canonical_lxd_virtio", + ), ), ) @mock.patch(DS_PATH + "os.lstat") - @mock.patch(DS_PATH + "os.path.exists") + @pytest.mark.usefixtures("fake_filesystem") def test_expected_viable( - self, m_exists, m_lstat, exists, lstat_mode, expected + self, m_lstat, exists, lstat_mode, virtio_ports, expected ): """Return True only when LXD_SOCKET_PATH exists and is a socket.""" - m_exists.return_value = exists + if virtio_ports: + populate_dir("/sys/class/virtio-ports", virtio_ports) + if exists: + ensure_file(lxd.LXD_SOCKET_PATH) m_lstat.return_value = LStatResponse(lstat_mode) assert expected is lxd.DataSourceLXD.ds_detect() - m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) if exists: m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) else: assert 0 == m_lstat.call_count + @pytest.mark.usefixtures("fake_filesystem") + @mock.patch(DS_PATH + "util.load_text_file", side_effect=OSError("Oh-no")) + def test_warn_on_oserror(self, m_load_text_file, caplog): + populate_dir("/sys/class/virtio-ports", {"vport5p1/name": "something"}) + with caplog.at_level(logging.WARNING): + assert False is lxd.DataSourceLXD.ds_detect() + assert "Cannot check virtio-ports: Oh-no" in caplog.messages + class TestReadMetadata: @pytest.mark.parametrize( diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index b9ae4d7380e..8dbeaf0ff2f 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -226,7 +226,6 @@ RC_NOT_FOUND = 1 DS_NONE = "None" -P_BOARD_NAME = "sys/class/dmi/id/board_name" P_CHASSIS_ASSET_TAG = "sys/class/dmi/id/chassis_asset_tag" P_PRODUCT_NAME = "sys/class/dmi/id/product_name" P_PRODUCT_SERIAL = "sys/class/dmi/id/product_serial" @@ -534,7 +533,9 @@ def test_wb_print_variables(self, tmp_path): # should have stricter identifiers). Since the MAAS datasource is # at the beginning of the list, this is particularly troublesome # and more concerning than NoCloud false positives, for example. + pytest.param("LXD-kvm-not-MAAS-1", True, id="mass_not_detected_1"), pytest.param("LXD-kvm-not-MAAS-2", True, id="mass_not_detected_2"), + pytest.param("LXD-kvm-not-MAAS-3", True, id="mass_not_detected_3"), # Don't detect incorrect config when invalid datasource_list # provided # @@ -892,6 +893,26 @@ def test_wb_print_variables(self, tmp_path): pytest.param("Not-WSL", False, id="wsl_not_found_virt"), # Negative test by lack of host filesystem mount points. pytest.param("WSL-no-host-mounts", False, id="wsl_no_fs_mounts"), + # Test LXD virtio-ports discovery with legacy serial name + pytest.param( + "LXD-virtio-legacy", True, id="lxd_virtio_legacy_serial" + ), + # Test LXD virtio-ports discovery with canonical serial name + pytest.param( + "LXD-virtio-canonical", True, id="lxd_virtio_canonical_serial" + ), + # Test that wrong virtio serial name doesn't detect LXD + pytest.param( + "LXD-virtio-wrong-name", False, id="lxd_virtio_wrong_serial" + ), + # Test LXD detection with multiple virtio ports + pytest.param( + "LXD-virtio-multiple-ports", + True, + id="lxd_virtio_multiple_ports", + ), + # Test that empty virtio-ports directory doesn't detect LXD + pytest.param("LXD-virtio-empty", False, id="lxd_virtio_empty_dir"), ], ) def test_ds_found_not_found(self, config, found, tmp_path): @@ -1677,7 +1698,9 @@ def _print_run_output(rc, out, err, cfg, files): }, "LXD-kvm": { "ds": "LXD", - "files": {P_BOARD_NAME: "LXD\n"}, + "files": { + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + }, # /dev/lxd/sock does not exist and KVM virt-type "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD @@ -1685,33 +1708,29 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-kvm-not-MAAS-1": { "ds": "LXD", "files": { - P_BOARD_NAME: "LXD\n", "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ( "datasource:\n MAAS:\n metadata_urls: [ 'blah.com' ]" ), }, - # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + # /dev/lxd/sock does exist + "mocks": [{"name": "is_socket_file", "ret": 0}], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "LXD-kvm-not-MAAS-2": { "ds": "LXD", "files": { - P_BOARD_NAME: "LXD\n", "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("#MAAS: None"), }, - # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + # /dev/lxd/sock does exist + "mocks": [{"name": "is_socket_file", "ret": 0}], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "LXD-kvm-not-MAAS-3": { "ds": "LXD", "files": { - P_BOARD_NAME: "LXD\n", "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("MAAS: None\n"), + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", }, - # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "flow_sequence-control": { @@ -1835,7 +1854,6 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-kvm-not-azure": { "ds": "Azure", "files": { - P_BOARD_NAME: "LXD\n", "etc/cloud/cloud.cfg.d/92-broken-azure.cfg": ( "datasource_list:\n - Azure" ), @@ -1844,26 +1862,28 @@ def _print_run_output(rc, out, err, cfg, files): "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, - "LXD-kvm-qemu-kernel-gt-5.10": { # LXD host > 5.10 kvm launch virt==qemu + "LXD-kvm-qemu-kernel-gt-5.10": { # LXD host > 5.10 kvm "ds": "LXD", - "files": {P_BOARD_NAME: "LXD\n"}, + "files": { + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + }, # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, # LXD host > 5.10 kvm launch virt==qemu "LXD-kvm-qemu-kernel-gt-5.10-env": { "ds": "LXD", "files": { - P_BOARD_NAME: "LXD\n", # this test is systemd-specific, but may run on non-systemd systems # ensure that /run/systemd/ exists, such that this test will take # the systemd branch on those systems as well # # https://github.com/canonical/cloud-init/issues/5095 "/run/systemd/somefile": "", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", }, - # /dev/lxd/sock does not exist and KVM virt-type + # /dev/lxd/sock does not exist "mocks": [{"name": "is_socket_file", "ret": 1}], "env_vars": IS_KVM_QEMU_ENV, "no_mocks": [ @@ -2870,4 +2890,52 @@ def _print_run_output(rc, out, err, cfg, files): ], }, }, + # Test virtio-ports discovery with legacy serial name + "LXD-virtio-legacy": { + "ds": "LXD", + "files": { + "sys/class/virtio-ports/vprt0p1/name": "org.linuxcontainers.lxd\n", + }, + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "no_mocks": ["dscheck_LXD"], + }, + # Test virtio-ports discovery with canonical serial name + "LXD-virtio-canonical": { + "ds": "LXD", + "files": { + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + }, + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "no_mocks": ["dscheck_LXD"], + }, + # Test virtio-ports with wrong serial name should not detect LXD + "LXD-virtio-wrong-name": { + "ds": "LXD", + "files": { + "sys/class/virtio-ports/vport0p1/name": "some.other.serial\n", + }, + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "no_mocks": ["dscheck_LXD"], + }, + # Test virtio-ports with multiple ports, only one is LXD + "LXD-virtio-multiple-ports": { + "ds": "LXD", + "files": { + "sys/class/virtio-ports/vport0p1/name": "some.other.serial\n", + "sys/class/virtio-ports/vport0p2/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p3/name": "another.serial\n", + }, + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "no_mocks": ["dscheck_LXD"], + }, + # Test empty virtio-ports directory should not detect LXD + "LXD-virtio-empty": { + "ds": "LXD", + "files": { + # Create the directory but no ports + "sys/class/virtio-ports/.keep": "", + }, + "mocks": [{"name": "is_socket_file", "ret": 1}], + "no_mocks": ["dscheck_LXD"], + }, } diff --git a/tools/ds-identify b/tools/ds-identify index 33353c96d08..99fe1078b14 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -960,15 +960,27 @@ dscheck_LXD() { if is_socket_file /dev/lxd/sock; then return ${DS_FOUND} fi - # On LXD KVM instances, /dev/lxd/sock is not yet setup by - # lxd-agent-loader's systemd lxd-agent.service. - # Rely on DMI product information that is present on all LXD images. - # Note "qemu" is returned on kvm instances launched from a host kernel - # kernels >=5.10, due to `hv_passthrough` option. - # systemd v. 251 should properly return "kvm" in this scenario - # https://github.com/systemd/systemd/issues/22709 - if [ "${DI_VIRT}" = "kvm" -o "${DI_VIRT}" = "qemu" ]; then - [ "${DI_DMI_BOARD_NAME}" = "LXD" ] && return ${DS_FOUND} + + # On LXD KVM instances (particularly s390x), /dev/lxd/sock may not + # be available during systemd generator timeframe until systemd + # lxd-agent.service runs. Check for LXD virtio serial device. + local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" + if [ -d "${virtio_ports_path}" ]; then + # Temporarily enable globbing to walk virtio_ports_path. + set +f; set -- "${virtio_ports_path}/"*; set -f; + for port_dir in "$@"; do + [ -d "${port_dir}" ] || continue + local name_file="${port_dir}/name" + if [ -f "${name_file}" ]; then + local port_name + port_name=$(cat "${name_file}" 2>/dev/null) || continue + # Check for both current and legacy LXD serial names + if [ "${port_name}" = "com.canonical.lxd" ] || \ + [ "${port_name}" = "org.linuxcontainers.lxd" ]; then + return ${DS_FOUND} + fi + fi + done fi return ${DS_NOT_FOUND} } From 8ba2087b9228f3ae331389a6372a2fe78f802bd4 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 12 Dec 2025 10:37:03 -0700 Subject: [PATCH 02/15] review: no cat in ds-id, comment fixes, OSError synonymou with IOError --- cloudinit/sources/DataSourceLXD.py | 7 ++++--- tools/ds-identify | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index 36a3ab7d088..4de2f3dd353 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -202,7 +202,8 @@ def ds_detect() -> bool: LOG.warning("%s is not a socket", LXD_SOCKET_PATH) # On LXD KVM instances /dev/lxd/sock may not be available yet. - # Check for LXD virtio serial device presence in virtio-ports. + # Check for LXD virtio serial device presence in virtio-ports which + # is supported on platforms without DMI data exposed. virtio_ports_path = "/sys/class/virtio-ports" if os.path.isdir(virtio_ports_path): try: @@ -215,8 +216,8 @@ def ds_detect() -> bool: "org.linuxcontainers.lxd", ): return True - except (OSError, IOError) as e: - LOG.warning("Cannot check virtio-ports: %s", e) + except OSError as e: + LOG.warning("Cannot check virtual serial device: %s", e) return False diff --git a/tools/ds-identify b/tools/ds-identify index 99fe1078b14..300b182a8aa 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -961,9 +961,10 @@ dscheck_LXD() { return ${DS_FOUND} fi - # On LXD KVM instances (particularly s390x), /dev/lxd/sock may not - # be available during systemd generator timeframe until systemd - # lxd-agent.service runs. Check for LXD virtio serial device. + # On LXD KVM instances, /dev/lxd/sock may not be available during systemd + # generator timeframe until systemd lxd-agent.service runs. + # Check for LXD virtio serial device which is supported on platforms + # without DMI data exposed. local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" if [ -d "${virtio_ports_path}" ]; then # Temporarily enable globbing to walk virtio_ports_path. @@ -973,7 +974,7 @@ dscheck_LXD() { local name_file="${port_dir}/name" if [ -f "${name_file}" ]; then local port_name - port_name=$(cat "${name_file}" 2>/dev/null) || continue + read port_name < "${name_file}" || continue # Check for both current and legacy LXD serial names if [ "${port_name}" = "com.canonical.lxd" ] || \ [ "${port_name}" = "org.linuxcontainers.lxd" ]; then From cff1ba6f774c9611d3d7343140acdf7849fd8ee2 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 12 Dec 2025 10:41:55 -0700 Subject: [PATCH 03/15] test: missing import --- tests/integration_tests/datasources/test_lxd_discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index 44090862a96..4a86219790d 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -11,6 +11,7 @@ lxd_has_nocloud, verify_clean_boot, verify_clean_log, + wait_for_cloud_init, ) From 4feb5e7f81eb77ade65bd53b6c3c5bb73fddd110 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 12 Dec 2025 10:47:31 -0700 Subject: [PATCH 04/15] ruff --- tests/integration_tests/datasources/test_lxd_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index 4a86219790d..3bc324105a2 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -6,7 +6,7 @@ from tests.integration_tests.clouds import IntegrationCloud from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM -from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU +from tests.integration_tests.releases import CURRENT_RELEASE from tests.integration_tests.util import ( lxd_has_nocloud, verify_clean_boot, From 5cde6c58446f6a91eae8f093f44c63e9d308fe31 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 17 Dec 2025 08:29:56 -0700 Subject: [PATCH 05/15] review comments: retain kvm/qemu check gating virtio-port traversal --- cloudinit/sources/DataSourceLXD.py | 68 +++++++++++++++-------------- tests/unittests/sources/test_lxd.py | 15 +++++-- tests/unittests/test_ds_identify.py | 1 + tools/ds-identify | 34 ++++++++------- 4 files changed, 66 insertions(+), 52 deletions(-) diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index 4de2f3dd353..de7031113be 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -44,29 +44,28 @@ } -def _get_fallback_interface_name() -> str: - default_name = "eth0" +def _get_virt_type() -> Optional[str]: if subp.which("systemd-detect-virt"): try: virt_type, _ = subp.subp(["systemd-detect-virt"]) + return virt_type.strip() except subp.ProcessExecutionError as err: - LOG.warning( - "Unable to run systemd-detect-virt: %s." - " Rendering default network config.", - err, - ) - return default_name - if virt_type.strip() in ( - "kvm", - "qemu", - ): # instance.type VIRTUAL-MACHINE - arch = util.system_info()["uname"][4] - if arch == "ppc64le": - return "enp0s5" - elif arch == "s390x": - return "enc9" - else: - return "enp5s0" + LOG.warning("Unable to run systemd-detect-virt: %s.", err) + return None + + +def _get_fallback_interface_name() -> str: + default_name = "eth0" + virt_type = _get_virt_type() + if virt_type in ("kvm", "qemu"): # instance.type VIRTUAL-MACHINE + arch = util.system_info()["uname"][4] + if arch == "ppc64le": + return "enp0s5" + elif arch == "s390x": + return "enc9" + else: + return "enp5s0" + LOG.debug("Rendering default network config.") return default_name @@ -204,20 +203,23 @@ def ds_detect() -> bool: # On LXD KVM instances /dev/lxd/sock may not be available yet. # Check for LXD virtio serial device presence in virtio-ports which # is supported on platforms without DMI data exposed. - virtio_ports_path = "/sys/class/virtio-ports" - if os.path.isdir(virtio_ports_path): - try: - for port in os.listdir(virtio_ports_path): - name_file = os.path.join(virtio_ports_path, port, "name") - if os.path.isfile(name_file): - # Check for both current and legacy LXD serial names - if util.load_text_file(name_file).strip() in ( - "com.canonical.lxd", - "org.linuxcontainers.lxd", - ): - return True - except OSError as e: - LOG.warning("Cannot check virtual serial device: %s", e) + if _get_virt_type() in ("kvm", "qemu"): + virtio_ports_path = "/sys/class/virtio-ports" + if os.path.isdir(virtio_ports_path): + try: + for port in os.listdir(virtio_ports_path): + name_file = os.path.join( + virtio_ports_path, port, "name" + ) + if os.path.isfile(name_file): + # Check for both current and legacy LXD serial names + if util.load_text_file(name_file).strip() in ( + "com.canonical.lxd", + "org.linuxcontainers.lxd", + ): + return True + except OSError as e: + LOG.warning("Cannot check virtual serial device: %s", e) return False diff --git a/tests/unittests/sources/test_lxd.py b/tests/unittests/sources/test_lxd.py index 29638868f94..c0d6b76a567 100644 --- a/tests/unittests/sources/test_lxd.py +++ b/tests/unittests/sources/test_lxd.py @@ -414,11 +414,12 @@ class TestIsPlatformViable: @mock.patch(DS_PATH + "os.lstat") @pytest.mark.usefixtures("fake_filesystem") def test_expected_viable( - self, m_lstat, exists, lstat_mode, virtio_ports, expected + self, m_lstat, exists, lstat_mode, virtio_ports, expected, mocker ): """Return True only when LXD_SOCKET_PATH exists and is a socket.""" if virtio_ports: populate_dir("/sys/class/virtio-ports", virtio_ports) + mocker.patch(DS_PATH + "_get_virt_type", return_value="kvm") if exists: ensure_file(lxd.LXD_SOCKET_PATH) m_lstat.return_value = LStatResponse(lstat_mode) @@ -428,13 +429,21 @@ def test_expected_viable( else: assert 0 == m_lstat.call_count + @pytest.mark.parametrize("systemd_detect_virt", ("kvm\n", "qemu\n")) @pytest.mark.usefixtures("fake_filesystem") + @mock.patch(DS_PATH + "subp.subp") + @mock.patch(DS_PATH + "subp.which", return_value=True) @mock.patch(DS_PATH + "util.load_text_file", side_effect=OSError("Oh-no")) - def test_warn_on_oserror(self, m_load_text_file, caplog): + def test_warn_on_oserror( + self, m_load_text_file, m_which, m_subp, systemd_detect_virt, caplog + ): + m_subp.return_value = (systemd_detect_virt, "") populate_dir("/sys/class/virtio-ports", {"vport5p1/name": "something"}) with caplog.at_level(logging.WARNING): assert False is lxd.DataSourceLXD.ds_detect() - assert "Cannot check virtio-ports: Oh-no" in caplog.messages + m_which.assert_called_once_with("systemd-detect-virt") + m_subp.assert_called_once_with(["systemd-detect-virt"]) + assert "Cannot check virtual serial device: Oh-no" in caplog.messages class TestReadMetadata: diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 8dbeaf0ff2f..2217743fac1 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -1731,6 +1731,7 @@ def _print_run_output(rc, out, err, cfg, files): "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("MAAS: None\n"), "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", }, + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "flow_sequence-control": { diff --git a/tools/ds-identify b/tools/ds-identify index 300b182a8aa..ce664deccea 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -965,23 +965,25 @@ dscheck_LXD() { # generator timeframe until systemd lxd-agent.service runs. # Check for LXD virtio serial device which is supported on platforms # without DMI data exposed. - local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" - if [ -d "${virtio_ports_path}" ]; then - # Temporarily enable globbing to walk virtio_ports_path. - set +f; set -- "${virtio_ports_path}/"*; set -f; - for port_dir in "$@"; do - [ -d "${port_dir}" ] || continue - local name_file="${port_dir}/name" - if [ -f "${name_file}" ]; then - local port_name - read port_name < "${name_file}" || continue - # Check for both current and legacy LXD serial names - if [ "${port_name}" = "com.canonical.lxd" ] || \ - [ "${port_name}" = "org.linuxcontainers.lxd" ]; then - return ${DS_FOUND} + if [ "${DI_VIRT}" = "kvm" -o "${DI_VIRT}" = "qemu" ]; then + local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" + if [ -d "${virtio_ports_path}" ]; then + # Temporarily enable globbing to walk virtio_ports_path. + set +f; set -- "${virtio_ports_path}/"*; set -f; + for port_dir in "$@"; do + [ -d "${port_dir}" ] || continue + local name_file="${port_dir}/name" + if [ -f "${name_file}" ]; then + local port_name + read port_name < "${name_file}" || continue + # Check for both current and legacy LXD serial names + if [ "${port_name}" = "com.canonical.lxd" ] || \ + [ "${port_name}" = "org.linuxcontainers.lxd" ]; then + return ${DS_FOUND} + fi fi - fi - done + done + fi fi return ${DS_NOT_FOUND} } From 44d8143c3e9bb0c7fe12da36a28e09e7fe02e9b1 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 19 Dec 2025 10:33:11 -0700 Subject: [PATCH 06/15] review comments: reduce nesting with early exits, ruff and revert test --- cloudinit/sources/DataSourceLXD.py | 35 +++++++++++++------------ tests/unittests/test_ds_identify.py | 2 +- tools/ds-identify | 40 +++++++++++++++-------------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index de7031113be..99f6f5732fa 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -203,24 +203,25 @@ def ds_detect() -> bool: # On LXD KVM instances /dev/lxd/sock may not be available yet. # Check for LXD virtio serial device presence in virtio-ports which # is supported on platforms without DMI data exposed. - if _get_virt_type() in ("kvm", "qemu"): - virtio_ports_path = "/sys/class/virtio-ports" - if os.path.isdir(virtio_ports_path): - try: - for port in os.listdir(virtio_ports_path): - name_file = os.path.join( - virtio_ports_path, port, "name" - ) - if os.path.isfile(name_file): - # Check for both current and legacy LXD serial names - if util.load_text_file(name_file).strip() in ( - "com.canonical.lxd", - "org.linuxcontainers.lxd", - ): - return True - except OSError as e: - LOG.warning("Cannot check virtual serial device: %s", e) + if _get_virt_type() not in ("kvm", "qemu"): + return False + virtio_ports_path = "/sys/class/virtio-ports" + if not os.path.isdir(virtio_ports_path): + return False + + try: + for port in os.listdir(virtio_ports_path): + name_file = os.path.join(virtio_ports_path, port, "name") + if os.path.isfile(name_file): + # Check both current and legacy LXD serial names + if util.load_text_file(name_file).strip() in ( + "com.canonical.lxd", + "org.linuxcontainers.lxd", + ): + return True + except OSError as e: + LOG.warning("Cannot check virtual serial device: %s", e) return False def _get_data(self) -> bool: diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 2217743fac1..579b011328a 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -1848,7 +1848,7 @@ def _print_run_output(rc, out, err, cfg, files): "flow_sequence-9": { "ds": "None", # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD "files": {"etc/cloud/cloud.cfg": dedent("datasource_list: [None]")}, }, diff --git a/tools/ds-identify b/tools/ds-identify index ce664deccea..2d47c72f104 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -965,26 +965,28 @@ dscheck_LXD() { # generator timeframe until systemd lxd-agent.service runs. # Check for LXD virtio serial device which is supported on platforms # without DMI data exposed. - if [ "${DI_VIRT}" = "kvm" -o "${DI_VIRT}" = "qemu" ]; then - local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" - if [ -d "${virtio_ports_path}" ]; then - # Temporarily enable globbing to walk virtio_ports_path. - set +f; set -- "${virtio_ports_path}/"*; set -f; - for port_dir in "$@"; do - [ -d "${port_dir}" ] || continue - local name_file="${port_dir}/name" - if [ -f "${name_file}" ]; then - local port_name - read port_name < "${name_file}" || continue - # Check for both current and legacy LXD serial names - if [ "${port_name}" = "com.canonical.lxd" ] || \ - [ "${port_name}" = "org.linuxcontainers.lxd" ]; then - return ${DS_FOUND} - fi - fi - done - fi + if [ "${DI_VIRT}" != "kvm" -a "${DI_VIRT}" != "qemu" ]; then + return ${DS_NOT_FOUND} + fi + local virtio_ports_path="${PATH_ROOT}/sys/class/virtio-ports" + if [ ! -d "${virtio_ports_path}" ]; then + return ${DS_NOT_FOUND} fi + # Temporarily enable globbing to walk virtio_ports_path. + set +f; set -- "${virtio_ports_path}/"*; set -f; + for port_dir in "$@"; do + local name_file="${port_dir}/name" + if [ -f "${name_file}" ]; then + local port_name + read port_name < "${name_file}" || \ + warn "unable to read file: $name_file" + # Check for both current and legacy LXD serial names + if [ "${port_name}" = "com.canonical.lxd" ] || \ + [ "${port_name}" = "org.linuxcontainers.lxd" ]; then + return ${DS_FOUND} + fi + fi + done return ${DS_NOT_FOUND} } From 61029f43d538141152d66553329ee3cd747b1dba Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 19 Dec 2025 12:53:33 -0700 Subject: [PATCH 07/15] chore: drop python DSLXD changes since we depend on presence of a socket --- cloudinit/sources/DataSourceLXD.py | 68 +++++++++-------------- tests/unittests/sources/test_lxd.py | 83 +++-------------------------- 2 files changed, 32 insertions(+), 119 deletions(-) diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index 99f6f5732fa..0f02c85e556 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -44,28 +44,29 @@ } -def _get_virt_type() -> Optional[str]: +def _get_fallback_interface_name() -> str: + default_name = "eth0" if subp.which("systemd-detect-virt"): try: virt_type, _ = subp.subp(["systemd-detect-virt"]) - return virt_type.strip() except subp.ProcessExecutionError as err: - LOG.warning("Unable to run systemd-detect-virt: %s.", err) - return None - - -def _get_fallback_interface_name() -> str: - default_name = "eth0" - virt_type = _get_virt_type() - if virt_type in ("kvm", "qemu"): # instance.type VIRTUAL-MACHINE - arch = util.system_info()["uname"][4] - if arch == "ppc64le": - return "enp0s5" - elif arch == "s390x": - return "enc9" - else: - return "enp5s0" - LOG.debug("Rendering default network config.") + LOG.warning( + "Unable to run systemd-detect-virt: %s." + " Rendering default network config.", + err, + ) + return default_name + if virt_type.strip() in ( + "kvm", + "qemu", + ): # instance.type VIRTUAL-MACHINE + arch = util.system_info()["uname"][4] + if arch == "ppc64le": + return "enp0s5" + elif arch == "s390x": + return "enc9" + else: + return "enp5s0" return default_name @@ -195,34 +196,13 @@ def _unpickle(self, ci_pkl_version: int) -> None: @staticmethod def ds_detect() -> bool: """Check platform environment to report if this datasource may run.""" - if os.path.exists(LXD_SOCKET_PATH): - if stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode): - return True - LOG.warning("%s is not a socket", LXD_SOCKET_PATH) - - # On LXD KVM instances /dev/lxd/sock may not be available yet. - # Check for LXD virtio serial device presence in virtio-ports which - # is supported on platforms without DMI data exposed. - if _get_virt_type() not in ("kvm", "qemu"): + if not os.path.exists(LXD_SOCKET_PATH): + LOG.warning("%s does not exist.", LXD_SOCKET_PATH) return False - - virtio_ports_path = "/sys/class/virtio-ports" - if not os.path.isdir(virtio_ports_path): + elif not stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode): + LOG.warning("%s is not a socket", LXD_SOCKET_PATH) return False - - try: - for port in os.listdir(virtio_ports_path): - name_file = os.path.join(virtio_ports_path, port, "name") - if os.path.isfile(name_file): - # Check both current and legacy LXD serial names - if util.load_text_file(name_file).strip() in ( - "com.canonical.lxd", - "org.linuxcontainers.lxd", - ): - return True - except OSError as e: - LOG.warning("Cannot check virtual serial device: %s", e) - return False + return True def _get_data(self) -> bool: """Crawl LXD socket API instance data and return True on success""" diff --git a/tests/unittests/sources/test_lxd.py b/tests/unittests/sources/test_lxd.py index c0d6b76a567..226ed842938 100644 --- a/tests/unittests/sources/test_lxd.py +++ b/tests/unittests/sources/test_lxd.py @@ -2,7 +2,6 @@ import copy import json -import logging import re import stat from collections import namedtuple @@ -16,8 +15,6 @@ from cloudinit.sources import DataSourceLXD as lxd from cloudinit.sources import InvalidMetaDataException from cloudinit.sources.DataSourceLXD import MetaDataKeys -from cloudinit.util import ensure_file -from tests.unittests.helpers import populate_dir DS_PATH = "cloudinit.sources.DataSourceLXD." @@ -359,92 +356,28 @@ def test_network_config_crawled_metadata_no_network_config( class TestIsPlatformViable: @pytest.mark.parametrize( - "exists,lstat_mode,virtio_ports,expected", + "exists,lstat_mode,expected", ( - pytest.param( - False, - None, - {}, - False, - id="not_viable_no_lxd_sock_path_no_virtio", - ), - pytest.param( - True, - stat.S_IFREG, - {}, - False, - id="not_viable_lxd_sock_path_regular_file_no_virtio", - ), - pytest.param( - True, - stat.S_IFSOCK, - {}, - True, - id="viable_when_lxd_sock_is_socket_file_no_virtio", - ), - pytest.param( - False, - None, - {"vport5p1/name": "com.redhat.spice.0"}, - False, - id="not_viable_no_lxd_sock_with_non_lxd_virtio", - ), - pytest.param( - False, - None, - { - "vport5p1/name": "com.redhat.spice.0", - "vport5p2/name": "org.linuxcontainers.lxd", - }, - True, - id="viable_no_lxd_sock_with_legacy_lxd_virtio", - ), - pytest.param( - False, - None, - { - "vport5p1/name": "com.redhat.spice.0", - "vport5p2/name": "com.canonical.lxd", - }, - True, - id="viable_no_lxd_sock_with_canonical_lxd_virtio", - ), + (False, None, False), + (True, stat.S_IFREG, False), + (True, stat.S_IFSOCK, True), ), ) @mock.patch(DS_PATH + "os.lstat") - @pytest.mark.usefixtures("fake_filesystem") + @mock.patch(DS_PATH + "os.path.exists") def test_expected_viable( - self, m_lstat, exists, lstat_mode, virtio_ports, expected, mocker + self, m_exists, m_lstat, exists, lstat_mode, expected ): """Return True only when LXD_SOCKET_PATH exists and is a socket.""" - if virtio_ports: - populate_dir("/sys/class/virtio-ports", virtio_ports) - mocker.patch(DS_PATH + "_get_virt_type", return_value="kvm") - if exists: - ensure_file(lxd.LXD_SOCKET_PATH) + m_exists.return_value = exists m_lstat.return_value = LStatResponse(lstat_mode) assert expected is lxd.DataSourceLXD.ds_detect() + m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) if exists: m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) else: assert 0 == m_lstat.call_count - @pytest.mark.parametrize("systemd_detect_virt", ("kvm\n", "qemu\n")) - @pytest.mark.usefixtures("fake_filesystem") - @mock.patch(DS_PATH + "subp.subp") - @mock.patch(DS_PATH + "subp.which", return_value=True) - @mock.patch(DS_PATH + "util.load_text_file", side_effect=OSError("Oh-no")) - def test_warn_on_oserror( - self, m_load_text_file, m_which, m_subp, systemd_detect_virt, caplog - ): - m_subp.return_value = (systemd_detect_virt, "") - populate_dir("/sys/class/virtio-ports", {"vport5p1/name": "something"}) - with caplog.at_level(logging.WARNING): - assert False is lxd.DataSourceLXD.ds_detect() - m_which.assert_called_once_with("systemd-detect-virt") - m_subp.assert_called_once_with(["systemd-detect-virt"]) - assert "Cannot check virtual serial device: Oh-no" in caplog.messages - class TestReadMetadata: @pytest.mark.parametrize( From a993ebaedeb950f608fc960816d388604608424e Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 5 Jan 2026 15:10:57 -0700 Subject: [PATCH 08/15] review comments: ds-identify integration test fix, comment updates - comment fixes in tests and code paths regarding lxd socket file. - fix integration test behavior to validate ds-identify discovery of lxd despite security.devlxd set False - fix unit tests to drop trailing newline --- .../datasources/test_lxd_discovery.py | 55 ++++++++++++------- tests/integration_tests/releases.py | 1 + tests/unittests/test_ds_identify.py | 48 ++++++++-------- tools/ds-identify | 4 +- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index 3bc324105a2..6b2c9c349f9 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -4,6 +4,7 @@ import yaml from tests.integration_tests.clouds import IntegrationCloud +from tests.integration_tests.decorators import retry from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM from tests.integration_tests.releases import CURRENT_RELEASE @@ -15,21 +16,30 @@ ) +@retry(tries=30, delay=0.5) +def _retry_get_ds_identify_log(client: IntegrationInstance): + """LXD VM agent will not be up immediately, so retry the initial cat.""" + return client.execute("cat /run/cloud-init/ds-identify.log") + + def _customize_environment(client: IntegrationInstance): # Assert our platform can detect LXD during systemd generator timeframe. - ds_id_log = client.execute("cat /run/cloud-init/ds-identify.log").stdout + ds_id_log = _retry_get_ds_identify_log(client) assert "check for 'LXD' returned found" in ds_id_log if client.settings.PLATFORM == "lxd_vm": # ds-identify runs at systemd generator time before /dev/lxd/sock. - # Assert we can expected virtio-ports artifacts which indicates LXD is + # Assert we can expect virtio-ports artifacts which indicates LXD is # viable. result = client.execute("cat /sys/class/virtio-ports/*/name") if not result.ok: raise AssertionError( "Missing expected /sys/class/virtio-ports/*/name" ) - if "LXD" != result.stdout: + if ( + "com.canonical.lxd" not in result.stdout + and "org.linuxcontainers.lxd" not in result.stdout + ): raise AssertionError( f"virtio-ports not LXD serial devices: {result.stdout}" ) @@ -45,16 +55,13 @@ def _customize_environment(client: IntegrationInstance): "/etc/cloud/cloud.cfg.d/99-detect-lxd-first.cfg", "datasource_list: [LXD, NoCloud]\n", ) - # This is also to ensure that NoCloud can be detected - if CURRENT_RELEASE.series == "jammy": - # Add nocloud-net seed files because Jammy no longer delivers NoCloud - # (LP: #1958460). - client.execute("mkdir -p /var/lib/cloud/seed/nocloud-net") - client.write_to_file("/var/lib/cloud/seed/nocloud-net/meta-data", "") - client.write_to_file( - "/var/lib/cloud/seed/nocloud-net/user-data", "#cloud-config\n{}" - ) - client.execute("cloud-init clean --logs") + # Ensure a valid NoCloud datasource will be detected if LXD fails. + client.execute("mkdir -p /var/lib/cloud/seed/nocloud-net") + client.write_to_file("/var/lib/cloud/seed/nocloud-net/meta-data", "") + client.write_to_file( + "/var/lib/cloud/seed/nocloud-net/user-data", "#cloud-config\n{}" + ) + client.execute("cloud-init clean --logs --machine-id -c all") client.restart() @@ -62,28 +69,34 @@ def _customize_environment(client: IntegrationInstance): def test_lxd_kvm_datasource_discovery_without_lxd_socket( session_cloud: IntegrationCloud, ): - """Test DataSourceLXD is detected on KVM by virtio-ports.""" + """Test DataSourceLXD on KVM detected by ds-identify using virtio-ports.""" with session_cloud.launch( wait=False, # to prevent cloud-init status --wait launch_kwargs={ # We detect the LXD datasource using a socket available to the # container. This prevents the socket from being exposed in the - # container, so LXD will not be detected. - # This allows us to wait for detection in 'init' stage with - # DataSourceNoCloudNet. + # container, so LXD will not be detected by python DataSourceLXD. "config_dict": {"security.devlxd": False}, }, ) as client: _customize_environment(client) - # We know this will be an LXD instance due to our pytest mark client.instance.execute_via_ssh = False # pyright: ignore result = wait_for_cloud_init(client, num_retries=60) - if not result.ok: + # Expect warnings and exit 2 concerning missing /dev/lxd/sock + if not result.ok and result.return_code != 2: raise AssertionError("cloud-init failed:\n%s", result.stderr) - if "DataSourceLXD" not in result.stdout: + # Expect fallback to NoCloud because python DataSourceLXD cannot + # get any information from /dev/lxd/sock due to security.devlxd above. + cloud_id = client.execute("cloud-id").stdout + if "nocloud" != cloud_id: raise AssertionError( - "cloud-init did not discover DataSourceLXD", result.stdout + "cloud-init did not discover 'nocloud' datasource." + f" Found '{cloud_id}'" ) + # Assert ds-idetify detected both LXD and NoCloud as viable during + # systemd generator time. + ds_config = client.execute("cat /run/cloud-init/cloud.cfg").stdout + assert "datasource_list: [ LXD, NoCloud, None ]" == ds_config @pytest.mark.skipif( diff --git a/tests/integration_tests/releases.py b/tests/integration_tests/releases.py index 3c3c1fd3df0..0bca05a7ef3 100644 --- a/tests/integration_tests/releases.py +++ b/tests/integration_tests/releases.py @@ -66,6 +66,7 @@ def __lt__(self, other: "Release"): raise ValueError(f"{self.os} cannot be compared to {other.os}!") return version.parse(self.version) < version.parse(other.version) + @classmethod def from_os_image( cls, diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 579b011328a..120d03921c0 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -893,9 +893,11 @@ def test_wb_print_variables(self, tmp_path): pytest.param("Not-WSL", False, id="wsl_not_found_virt"), # Negative test by lack of host filesystem mount points. pytest.param("WSL-no-host-mounts", False, id="wsl_no_fs_mounts"), - # Test LXD virtio-ports discovery with legacy serial name + # Test LXD virtio-ports discovery with linuxcontainers serial name pytest.param( - "LXD-virtio-legacy", True, id="lxd_virtio_legacy_serial" + "LXD-virtio-linuxcontainers", + True, + id="lxd_virtio_linuxcontainers_serial", ), # Test LXD virtio-ports discovery with canonical serial name pytest.param( @@ -1699,7 +1701,7 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-kvm": { "ds": "LXD", "files": { - "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, # /dev/lxd/sock does not exist and KVM virt-type "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], @@ -1712,8 +1714,8 @@ def _print_run_output(rc, out, err, cfg, files): "datasource:\n MAAS:\n metadata_urls: [ 'blah.com' ]" ), }, - # /dev/lxd/sock does exist - "mocks": [{"name": "is_socket_file", "ret": 0}], + # /dev/lxd/sock does exist and KVM virt-type + "mocks": [{"name": "is_socket_file", "ret": 0}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "LXD-kvm-not-MAAS-2": { @@ -1721,15 +1723,15 @@ def _print_run_output(rc, out, err, cfg, files): "files": { "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("#MAAS: None"), }, - # /dev/lxd/sock does exist - "mocks": [{"name": "is_socket_file", "ret": 0}], + # /dev/lxd/sock does exist and KVM virt-type + "mocks": [{"name": "is_socket_file", "ret": 0}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "LXD-kvm-not-MAAS-3": { "ds": "LXD", "files": { "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("MAAS: None\n"), - "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD @@ -1847,7 +1849,7 @@ def _print_run_output(rc, out, err, cfg, files): # no quotes, no whitespace "flow_sequence-9": { "ds": "None", - # /dev/lxd/sock does not exist and KVM virt-type + # /dev/lxd/sock does not exist and QEMU virt-type "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD "files": {"etc/cloud/cloud.cfg": dedent("datasource_list: [None]")}, @@ -1866,10 +1868,10 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-kvm-qemu-kernel-gt-5.10": { # LXD host > 5.10 kvm "ds": "LXD", "files": { - "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, - # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + # /dev/lxd/sock does not exist and QEMU virt-type + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, # LXD host > 5.10 kvm launch virt==qemu @@ -1882,9 +1884,9 @@ def _print_run_output(rc, out, err, cfg, files): # # https://github.com/canonical/cloud-init/issues/5095 "/run/systemd/somefile": "", - "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, - # /dev/lxd/sock does not exist + # /dev/lxd/sock does not exist and QEMU env virt-type "mocks": [{"name": "is_socket_file", "ret": 1}], "env_vars": IS_KVM_QEMU_ENV, "no_mocks": [ @@ -2891,20 +2893,20 @@ def _print_run_output(rc, out, err, cfg, files): ], }, }, - # Test virtio-ports discovery with legacy serial name - "LXD-virtio-legacy": { + # Test virtio-ports discovery with linuxcontainers serial name + "LXD-virtio-linuxcontainers": { "ds": "LXD", "files": { - "sys/class/virtio-ports/vprt0p1/name": "org.linuxcontainers.lxd\n", + "sys/class/virtio-ports/vprt0p1/name": "org.linuxcontainers.lxd", }, - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], "no_mocks": ["dscheck_LXD"], }, # Test virtio-ports discovery with canonical serial name "LXD-virtio-canonical": { "ds": "LXD", "files": { - "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd\n", + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], @@ -2913,7 +2915,7 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-virtio-wrong-name": { "ds": "LXD", "files": { - "sys/class/virtio-ports/vport0p1/name": "some.other.serial\n", + "sys/class/virtio-ports/vport0p1/name": "some.other.serial", }, "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], @@ -2922,9 +2924,9 @@ def _print_run_output(rc, out, err, cfg, files): "LXD-virtio-multiple-ports": { "ds": "LXD", "files": { - "sys/class/virtio-ports/vport0p1/name": "some.other.serial\n", - "sys/class/virtio-ports/vport0p2/name": "com.canonical.lxd\n", - "sys/class/virtio-ports/vport0p3/name": "another.serial\n", + "sys/class/virtio-ports/vport0p1/name": "some.other.serial", + "sys/class/virtio-ports/vport0p2/name": "com.canonical.lxd", + "sys/class/virtio-ports/vport0p3/name": "another.serial", }, "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], diff --git a/tools/ds-identify b/tools/ds-identify index 2d47c72f104..ec9ad79837f 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -961,10 +961,10 @@ dscheck_LXD() { return ${DS_FOUND} fi - # On LXD KVM instances, /dev/lxd/sock may not be available during systemd + # On LXD KVM instances, /dev/lxd/sock will not be available during systemd # generator timeframe until systemd lxd-agent.service runs. # Check for LXD virtio serial device which is supported on platforms - # without DMI data exposed. + # without DMI data. if [ "${DI_VIRT}" != "kvm" -a "${DI_VIRT}" != "qemu" ]; then return ${DS_NOT_FOUND} fi From 042e234567a8a02ba7f0fd2b5d4e53e901f5e7f2 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 5 Jan 2026 15:29:13 -0700 Subject: [PATCH 09/15] test: adjust remaining mocks for non-MAAS and azure platform tests --- tests/integration_tests/datasources/test_lxd_discovery.py | 1 + tests/unittests/test_ds_identify.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index 6b2c9c349f9..d114487e7a9 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -99,6 +99,7 @@ def test_lxd_kvm_datasource_discovery_without_lxd_socket( assert "datasource_list: [ LXD, NoCloud, None ]" == ds_config +@pytest.mark.skipif(not IS_UBUNTU, reason="Netplan usage") @pytest.mark.skipif( PLATFORM not in ["lxd_container", "lxd_vm"], reason="Test is LXD specific", diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 120d03921c0..7dce0e4afd8 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -1849,8 +1849,8 @@ def _print_run_output(rc, out, err, cfg, files): # no quotes, no whitespace "flow_sequence-9": { "ds": "None", - # /dev/lxd/sock does not exist and QEMU virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], + # /dev/lxd/sock does not exist and KVM virt-type + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD "files": {"etc/cloud/cloud.cfg": dedent("datasource_list: [None]")}, }, @@ -1862,7 +1862,7 @@ def _print_run_output(rc, out, err, cfg, files): ), }, # /dev/lxd/sock does not exist and KVM virt-type - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], + "mocks": [{"name": "is_socket_file", "ret": 0}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "LXD-kvm-qemu-kernel-gt-5.10": { # LXD host > 5.10 kvm From 4cb6751946cae8da1513dbe68d1572ad20fe8614 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 5 Jan 2026 17:10:02 -0700 Subject: [PATCH 10/15] ruff/black --- tests/integration_tests/datasources/test_lxd_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index d114487e7a9..e2248d13ba8 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -7,7 +7,7 @@ from tests.integration_tests.decorators import retry from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM -from tests.integration_tests.releases import CURRENT_RELEASE +from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU from tests.integration_tests.util import ( lxd_has_nocloud, verify_clean_boot, From 3e869c3aec368f3e6fb74516ad802874d6cfffc5 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 6 Jan 2026 13:50:05 -0700 Subject: [PATCH 11/15] test: use yaml.safe_load for ds-identify asserts, comment updates --- .../datasources/test_lxd_discovery.py | 15 +++++++++------ tests/integration_tests/decorators.py | 2 +- tests/integration_tests/releases.py | 1 - tests/unittests/test_ds_identify.py | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index e2248d13ba8..4def834670c 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -73,9 +73,9 @@ def test_lxd_kvm_datasource_discovery_without_lxd_socket( with session_cloud.launch( wait=False, # to prevent cloud-init status --wait launch_kwargs={ - # We detect the LXD datasource using a socket available to the - # container. This prevents the socket from being exposed in the - # container, so LXD will not be detected by python DataSourceLXD. + # Setting security.devlxd to False prevents /dev/lxd/sock + # from being exposed in the VM, so DataSourceLXD will not be + # identified by the python DataSourceLXD.ds_detect. "config_dict": {"security.devlxd": False}, }, ) as client: @@ -85,8 +85,9 @@ def test_lxd_kvm_datasource_discovery_without_lxd_socket( # Expect warnings and exit 2 concerning missing /dev/lxd/sock if not result.ok and result.return_code != 2: raise AssertionError("cloud-init failed:\n%s", result.stderr) - # Expect fallback to NoCloud because python DataSourceLXD cannot - # get any information from /dev/lxd/sock due to security.devlxd above. + # Expect NoCloud because python DataSourceLXD cannot read metadata from + # /dev/lxd/sock due to security.devlxd above and falls back to + # the nocloud seed files written by _customize_environment. cloud_id = client.execute("cloud-id").stdout if "nocloud" != cloud_id: raise AssertionError( @@ -96,7 +97,9 @@ def test_lxd_kvm_datasource_discovery_without_lxd_socket( # Assert ds-idetify detected both LXD and NoCloud as viable during # systemd generator time. ds_config = client.execute("cat /run/cloud-init/cloud.cfg").stdout - assert "datasource_list: [ LXD, NoCloud, None ]" == ds_config + assert { + "datasource_list": ["LXD", "NoCloud", "None"] + } == yaml.safe_load(ds_config) @pytest.mark.skipif(not IS_UBUNTU, reason="Netplan usage") diff --git a/tests/integration_tests/decorators.py b/tests/integration_tests/decorators.py index 885b4f46aed..298c2557fa4 100644 --- a/tests/integration_tests/decorators.py +++ b/tests/integration_tests/decorators.py @@ -2,7 +2,7 @@ import time -def retry(*, tries: int = 30, delay: int = 1): +def retry(*, tries: int = 30, delay: float = 1.0): """Decorator for retries. Retry a function until code no longer raises an exception or diff --git a/tests/integration_tests/releases.py b/tests/integration_tests/releases.py index 0bca05a7ef3..3c3c1fd3df0 100644 --- a/tests/integration_tests/releases.py +++ b/tests/integration_tests/releases.py @@ -66,7 +66,6 @@ def __lt__(self, other: "Release"): raise ValueError(f"{self.os} cannot be compared to {other.os}!") return version.parse(self.version) < version.parse(other.version) - @classmethod def from_os_image( cls, diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 7dce0e4afd8..864d135a452 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -1861,7 +1861,7 @@ def _print_run_output(rc, out, err, cfg, files): "datasource_list:\n - Azure" ), }, - # /dev/lxd/sock does not exist and KVM virt-type + # /dev/lxd/sock does exist and KVM virt-type "mocks": [{"name": "is_socket_file", "ret": 0}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, From bb689926a1e7c7ca7e2859ec4b0a601171da2fe3 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 6 Jan 2026 13:54:28 -0700 Subject: [PATCH 12/15] tests: revert stray MOCK_VIRT_IS_KVM_QEMU test --- tests/unittests/test_ds_identify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 864d135a452..5b1341207a2 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -1733,7 +1733,8 @@ def _print_run_output(rc, out, err, cfg, files): "etc/cloud/cloud.cfg.d/92-broken-maas.cfg": ("MAAS: None\n"), "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", }, - "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM_QEMU], + # /dev/lxd/sock does not exist and KVM virt-type + "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], "no_mocks": ["dscheck_LXD"], # Don't default mock dscheck_LXD }, "flow_sequence-control": { From 934dd2ae6421ed1d532492a004260e8d59903629 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 8 Jan 2026 18:22:49 -0700 Subject: [PATCH 13/15] chore: avoid ds-id nesting and lxd doc updates avoiding details --- doc/rtd/reference/datasources/lxd.rst | 14 +++----------- tools/ds-identify | 18 ++++++++---------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/doc/rtd/reference/datasources/lxd.rst b/doc/rtd/reference/datasources/lxd.rst index 32388333340..e4031333b70 100644 --- a/doc/rtd/reference/datasources/lxd.rst +++ b/doc/rtd/reference/datasources/lxd.rst @@ -7,21 +7,13 @@ The LXD datasource allows the user to provide custom user-data, vendor-data, meta-data and network-config to the instance without running a network service (or even without having a network at all). This datasource performs HTTP GETs against the `LXD socket device`_ which is provided to each -running LXD container and VM as :file:`/dev/lxd/sock` and represents all -instance-meta-data as versioned HTTP routes such as: - - - 1.0/meta-data - - 1.0/config/cloud-init.vendor-data - - 1.0/config/cloud-init.user-data - - 1.0/config/user. +running LXD container and VM as :file:`/dev/lxd/sock` The LXD socket device :file:`/dev/lxd/sock` is only present on containers and VMs when the instance configuration has ``security.devlxd=true`` (default). Disabling the ``security.devlxd`` configuration setting at initial launch will -ensure that ``cloud-init`` uses the :ref:`datasource_nocloud` datasource. -Disabling ``security.devlxd`` over the life of the container will result in -warnings from ``cloud-init``, and ``cloud-init`` will keep the -originally-detected LXD datasource. +result in warnings from ``cloud-init``, and ``cloud-init`` will be unable to use +the LXD datasource The LXD datasource is detected as viable by ``ds-identify`` during the :ref:`detect stage` when either :file:`/dev/lxd/sock` exists diff --git a/tools/ds-identify b/tools/ds-identify index ec9ad79837f..68ab575b2e5 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -975,16 +975,14 @@ dscheck_LXD() { # Temporarily enable globbing to walk virtio_ports_path. set +f; set -- "${virtio_ports_path}/"*; set -f; for port_dir in "$@"; do - local name_file="${port_dir}/name" - if [ -f "${name_file}" ]; then - local port_name - read port_name < "${name_file}" || \ - warn "unable to read file: $name_file" - # Check for both current and legacy LXD serial names - if [ "${port_name}" = "com.canonical.lxd" ] || \ - [ "${port_name}" = "org.linuxcontainers.lxd" ]; then - return ${DS_FOUND} - fi + local name_file="${port_dir}/name" port_name + [ ! -f "${name_file}" ] && continue + read port_name < "${name_file}" || \ + warn "unable to read file: $name_file" + # Check for both current and legacy LXD serial names + if [ "${port_name}" = "com.canonical.lxd" ] || \ + [ "${port_name}" = "org.linuxcontainers.lxd" ]; then + return ${DS_FOUND} fi done return ${DS_NOT_FOUND} From 7a62b56ea2ed2bbe41e53eb577f6d936b17d19fc Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 12 Jan 2026 22:32:44 -0700 Subject: [PATCH 14/15] doc: reduce dev/lxd/sock content --- doc/rtd/reference/datasources/lxd.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/rtd/reference/datasources/lxd.rst b/doc/rtd/reference/datasources/lxd.rst index e4031333b70..1168463e1db 100644 --- a/doc/rtd/reference/datasources/lxd.rst +++ b/doc/rtd/reference/datasources/lxd.rst @@ -9,11 +9,9 @@ a network service (or even without having a network at all). This datasource performs HTTP GETs against the `LXD socket device`_ which is provided to each running LXD container and VM as :file:`/dev/lxd/sock` -The LXD socket device :file:`/dev/lxd/sock` is only present on containers and -VMs when the instance configuration has ``security.devlxd=true`` (default). -Disabling the ``security.devlxd`` configuration setting at initial launch will -result in warnings from ``cloud-init``, and ``cloud-init`` will be unable to use -the LXD datasource +The LXD socket device :file:`/dev/lxd/sock` is required to use the LXD +datasource. This file is present in containers and VMs when the instance +configuration sets ``security.devlxd=true``. The LXD datasource is detected as viable by ``ds-identify`` during the :ref:`detect stage` when either :file:`/dev/lxd/sock` exists From 7085cd3b0d8012f9047c346416ba1cbb63713250 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 14 Jan 2026 20:29:43 -0700 Subject: [PATCH 15/15] chore: correct ds-id whitespace alignment on multi-line --- tools/ds-identify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ds-identify b/tools/ds-identify index 68ab575b2e5..191a92d5180 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -978,7 +978,7 @@ dscheck_LXD() { local name_file="${port_dir}/name" port_name [ ! -f "${name_file}" ] && continue read port_name < "${name_file}" || \ - warn "unable to read file: $name_file" + warn "unable to read file: $name_file" # Check for both current and legacy LXD serial names if [ "${port_name}" = "com.canonical.lxd" ] || \ [ "${port_name}" = "org.linuxcontainers.lxd" ]; then