Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions doc/rtd/reference/datasources/lxd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.<any-custom-key>

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<boot-Detect>` when either ``/dev/lxd/sock`` exists or
``/sys/class/dmi/id/board_name`` matches "LXD".
:ref:`detect stage<boot-Detect>` when either :file:`/dev/lxd/sock` exists
or an LXD serial device is present in :file:`/sys/class/virtio-ports`.
Comment thread
holmanb marked this conversation as resolved.

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
Expand Down
81 changes: 65 additions & 16 deletions tests/integration_tests/datasources/test_lxd_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,46 @@
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
from tests.integration_tests.util import (
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:
Expand All @@ -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(
Comment thread
holmanb marked this conversation as resolved.
PLATFORM not in ["lxd_container", "lxd_vm"],
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import time


def retry(*, tries: int = 30, delay: int = 1):
def retry(*, tries: int = 30, delay: float = 1.0):
Comment thread
blackboxsw marked this conversation as resolved.
"""Decorator for retries.

Retry a function until code no longer raises an exception or
Expand Down
106 changes: 89 additions & 17 deletions tests/unittests/test_ds_identify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"),
Comment thread
holmanb marked this conversation as resolved.
# Don't detect incorrect config when invalid datasource_list
# provided
#
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -1677,38 +1700,38 @@ 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
},
"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],
Comment thread
holmanb marked this conversation as resolved.
# /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],
Expand Down Expand Up @@ -1835,35 +1858,36 @@ def _print_run_output(rc, out, err, cfg, files):
"LXD-kvm-not-azure": {
"ds": "Azure",
"files": {
P_BOARD_NAME: "LXD\n",
Comment thread
holmanb marked this conversation as resolved.
"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
},
# 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",
},
# /dev/lxd/sock does not exist and KVM virt-type
Comment thread
holmanb marked this conversation as resolved.
# /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": [
Expand Down Expand Up @@ -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"],
},
}
33 changes: 24 additions & 9 deletions tools/ds-identify
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
holmanb marked this conversation as resolved.
[ "${DI_DMI_BOARD_NAME}" = "LXD" ] && return ${DS_FOUND}
Comment thread
blackboxsw marked this conversation as resolved.

# 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}
}

Expand Down
Loading