diff --git a/doc/rtd/reference/datasources/lxd.rst b/doc/rtd/reference/datasources/lxd.rst index b1b09b80471..1168463e1db 100644 --- a/doc/rtd/reference/datasources/lxd.rst +++ b/doc/rtd/reference/datasources/lxd.rst @@ -7,25 +7,15 @@ 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 -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. - -The LXD socket device ``/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. +running LXD container and VM as :file:`/dev/lxd/sock` + +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 ``/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..4def834670c 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -3,6 +3,8 @@ import pytest 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, IS_UBUNTU @@ -10,24 +12,37 @@ lxd_has_nocloud, verify_clean_boot, verify_clean_log, + wait_for_cloud_init, ) +@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 artifact which indicates LXD is viable. - result = client.execute("cat /sys/class/dmi/id/board_name") + # 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/dmi/id/board_name" + "Missing expected /sys/class/virtio-ports/*/name" + ) + 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}" ) - if "LXD" != result.stdout: - raise AssertionError(f"DMI board_name is not LXD: {result.stdout}") # Having multiple datasources prevents ds-identify from short-circuiting # detection logic with a log like: @@ -40,19 +55,53 @@ 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() +@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 on KVM detected by ds-identify using virtio-ports.""" + with session_cloud.launch( + wait=False, # to prevent cloud-init status --wait + launch_kwargs={ + # 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: + _customize_environment(client) + client.instance.execute_via_ssh = False # pyright: ignore + result = wait_for_cloud_init(client, num_retries=60) + # 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 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( + "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"] + } == yaml.safe_load(ds_config) + + @pytest.mark.skipif(not IS_UBUNTU, reason="Netplan usage") @pytest.mark.skipif( PLATFORM not in ["lxd_container", "lxd_vm"], 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/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index b9ae4d7380e..5b1341207a2 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,28 @@ 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 linuxcontainers serial name + pytest.param( + "LXD-virtio-linuxcontainers", + True, + id="lxd_virtio_linuxcontainers_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 +1700,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", + }, # /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,30 +1710,28 @@ 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 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": { "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 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": { - 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", }, # /dev/lxd/sock does not exist and KVM virt-type "mocks": [{"name": "is_socket_file", "ret": 1}, MOCK_VIRT_IS_KVM], @@ -1835,19 +1858,20 @@ 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" ), }, - # /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 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-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"}, - # /dev/lxd/sock does not exist and KVM virt-type + "files": { + "sys/class/virtio-ports/vport0p1/name": "com.canonical.lxd", + }, + # /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 }, @@ -1855,15 +1879,15 @@ def _print_run_output(rc, out, err, cfg, files): "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", }, - # /dev/lxd/sock does not exist and KVM virt-type + # /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": [ @@ -2870,4 +2894,52 @@ def _print_run_output(rc, out, err, cfg, files): ], }, }, + # Test virtio-ports discovery with linuxcontainers serial name + "LXD-virtio-linuxcontainers": { + "ds": "LXD", + "files": { + "sys/class/virtio-ports/vprt0p1/name": "org.linuxcontainers.lxd", + }, + "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", + }, + "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", + }, + "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", + "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"], + }, + # 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..191a92d5180 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -960,16 +960,31 @@ 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, /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. + 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" 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} }