Skip to content

Commit 413e95b

Browse files
committed
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 s390x. The following mechinisms for LXD detection are retained: 1. /dev/lxd/sock socket file (primary method) 2. virtio-ports serial device (KVM/QEMU and new for s390x) Implements: PL073
1 parent 8907560 commit 413e95b

6 files changed

Lines changed: 236 additions & 50 deletions

File tree

cloudinit/sources/DataSourceLXD.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,30 @@ def _unpickle(self, ci_pkl_version: int) -> None:
196196
@staticmethod
197197
def ds_detect() -> bool:
198198
"""Check platform environment to report if this datasource may run."""
199-
if not os.path.exists(LXD_SOCKET_PATH):
200-
LOG.warning("%s does not exist.", LXD_SOCKET_PATH)
201-
return False
202-
elif not stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode):
199+
if os.path.exists(LXD_SOCKET_PATH):
200+
if stat.S_ISSOCK(os.lstat(LXD_SOCKET_PATH).st_mode):
201+
return True
203202
LOG.warning("%s is not a socket", LXD_SOCKET_PATH)
204-
return False
205-
return True
203+
204+
# On LXD KVM instances (particularly s390x), /dev/lxd/sock may not
205+
# be available yet. Check for LXD virtio serial device presence
206+
# in virtio-ports.
207+
virtio_ports_path = "/sys/class/virtio-ports"
208+
if os.path.isdir(virtio_ports_path):
209+
try:
210+
for port in os.listdir(virtio_ports_path):
211+
name_file = os.path.join(virtio_ports_path, port, "name")
212+
if os.path.isfile(name_file):
213+
# Check for both current and legacy LXD serial names
214+
if util.load_text_file(name_file).strip() in (
215+
"com.canonical.lxd",
216+
"org.linuxcontainers.lxd",
217+
):
218+
return True
219+
except (OSError, IOError) as e:
220+
LOG.warning("Cannot check virtio-ports: %s", e)
221+
222+
return False
206223

207224
def _get_data(self) -> bool:
208225
"""Crawl LXD socket API instance data and return True on success"""

doc/rtd/reference/datasources/lxd.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,25 @@ The LXD datasource allows the user to provide custom user-data,
77
vendor-data, meta-data and network-config to the instance without running
88
a network service (or even without having a network at all). This datasource
99
performs HTTP GETs against the `LXD socket device`_ which is provided to each
10-
running LXD container and VM as ``/dev/lxd/sock`` and represents all
10+
running LXD container and VM as :file:`/dev/lxd/sock` and represents all
1111
instance-meta-data as versioned HTTP routes such as:
1212

1313
- 1.0/meta-data
1414
- 1.0/config/cloud-init.vendor-data
1515
- 1.0/config/cloud-init.user-data
1616
- 1.0/config/user.<any-custom-key>
1717

18-
The LXD socket device ``/dev/lxd/sock`` is only present on containers and VMs
19-
when the instance configuration has ``security.devlxd=true`` (default).
18+
The LXD socket device :file:`/dev/lxd/sock` is only present on containers and
19+
VMs when the instance configuration has ``security.devlxd=true`` (default).
2020
Disabling the ``security.devlxd`` configuration setting at initial launch will
2121
ensure that ``cloud-init`` uses the :ref:`datasource_nocloud` datasource.
2222
Disabling ``security.devlxd`` over the life of the container will result in
2323
warnings from ``cloud-init``, and ``cloud-init`` will keep the
2424
originally-detected LXD datasource.
2525

2626
The LXD datasource is detected as viable by ``ds-identify`` during the
27-
:ref:`detect stage<boot-Detect>` when either ``/dev/lxd/sock`` exists or
28-
``/sys/class/dmi/id/board_name`` matches "LXD".
27+
:ref:`detect stage<boot-Detect>` when either :file:`/dev/lxd/sock` exists
28+
or an LXD serial device is present in :file:`/sys/class/virtio-ports`.
2929

3030
The LXD datasource provides ``cloud-init`` with the ability to react to
3131
meta-data, vendor-data, user-data and network-config changes, and to render the

tests/integration_tests/datasources/test_lxd_discovery.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
import yaml
55

6+
from tests.integration_tests.clouds import IntegrationCloud
67
from tests.integration_tests.instances import IntegrationInstance
78
from tests.integration_tests.integration_settings import PLATFORM
89
from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU
@@ -20,14 +21,17 @@ def _customize_environment(client: IntegrationInstance):
2021

2122
if client.settings.PLATFORM == "lxd_vm":
2223
# ds-identify runs at systemd generator time before /dev/lxd/sock.
23-
# Assert we can expected artifact which indicates LXD is viable.
24-
result = client.execute("cat /sys/class/dmi/id/board_name")
24+
# Assert we can expected virtio-ports artifacts which indicates LXD is
25+
# viable.
26+
result = client.execute("cat /sys/class/virtio-ports/*/name")
2527
if not result.ok:
2628
raise AssertionError(
27-
"Missing expected /sys/class/dmi/id/board_name"
29+
"Missing expected /sys/class/virtio-ports/*/name"
2830
)
2931
if "LXD" != result.stdout:
30-
raise AssertionError(f"DMI board_name is not LXD: {result.stdout}")
32+
raise AssertionError(
33+
f"virtio-ports not LXD serial devices: {result.stdout}"
34+
)
3135

3236
# Having multiple datasources prevents ds-identify from short-circuiting
3337
# detection logic with a log like:
@@ -53,7 +57,34 @@ def _customize_environment(client: IntegrationInstance):
5357
client.restart()
5458

5559

56-
@pytest.mark.skipif(not IS_UBUNTU, reason="Netplan usage")
60+
@pytest.mark.skipif(PLATFORM != "lxd_vm", reason="Test is LXD KVM specific")
61+
def test_lxd_kvm_datasource_discovery_without_lxd_socket(
62+
session_cloud: IntegrationCloud,
63+
):
64+
"""Test DataSourceLXD is detected on KVM by virtio-ports."""
65+
with session_cloud.launch(
66+
wait=False, # to prevent cloud-init status --wait
67+
launch_kwargs={
68+
# We detect the LXD datasource using a socket available to the
69+
# container. This prevents the socket from being exposed in the
70+
# container, so LXD will not be detected.
71+
# This allows us to wait for detection in 'init' stage with
72+
# DataSourceNoCloudNet.
73+
"config_dict": {"security.devlxd": False},
74+
},
75+
) as client:
76+
_customize_environment(client)
77+
# We know this will be an LXD instance due to our pytest mark
78+
client.instance.execute_via_ssh = False # pyright: ignore
79+
result = wait_for_cloud_init(client, num_retries=60)
80+
if not result.ok:
81+
raise AssertionError("cloud-init failed:\n%s", result.stderr)
82+
if "DataSourceLXD" not in result.stdout:
83+
raise AssertionError(
84+
"cloud-init did not discover DataSourceLXD", result.stdout
85+
)
86+
87+
5788
@pytest.mark.skipif(
5889
PLATFORM not in ["lxd_container", "lxd_vm"],
5990
reason="Test is LXD specific",

tests/unittests/sources/test_lxd.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import json
5+
import logging
56
import re
67
import stat
78
from collections import namedtuple
@@ -15,6 +16,8 @@
1516
from cloudinit.sources import DataSourceLXD as lxd
1617
from cloudinit.sources import InvalidMetaDataException
1718
from cloudinit.sources.DataSourceLXD import MetaDataKeys
19+
from cloudinit.util import ensure_file
20+
from tests.unittests.helpers import populate_dir
1821

1922
DS_PATH = "cloudinit.sources.DataSourceLXD."
2023

@@ -356,28 +359,83 @@ def test_network_config_crawled_metadata_no_network_config(
356359

357360
class TestIsPlatformViable:
358361
@pytest.mark.parametrize(
359-
"exists,lstat_mode,expected",
362+
"exists,lstat_mode,virtio_ports,expected",
360363
(
361-
(False, None, False),
362-
(True, stat.S_IFREG, False),
363-
(True, stat.S_IFSOCK, True),
364+
pytest.param(
365+
False,
366+
None,
367+
{},
368+
False,
369+
id="not_viable_no_lxd_sock_path_no_virtio",
370+
),
371+
pytest.param(
372+
True,
373+
stat.S_IFREG,
374+
{},
375+
False,
376+
id="not_viable_lxd_sock_path_regular_file_no_virtio",
377+
),
378+
pytest.param(
379+
True,
380+
stat.S_IFSOCK,
381+
{},
382+
True,
383+
id="viable_when_lxd_sock_is_socket_file_no_virtio",
384+
),
385+
pytest.param(
386+
False,
387+
None,
388+
{"vport5p1/name": "com.redhat.spice.0"},
389+
False,
390+
id="not_viable_no_lxd_sock_with_non_lxd_virtio",
391+
),
392+
pytest.param(
393+
False,
394+
None,
395+
{
396+
"vport5p1/name": "com.redhat.spice.0",
397+
"vport5p2/name": "org.linuxcontainers.lxd",
398+
},
399+
True,
400+
id="viable_no_lxd_sock_with_legacy_lxd_virtio",
401+
),
402+
pytest.param(
403+
False,
404+
None,
405+
{
406+
"vport5p1/name": "com.redhat.spice.0",
407+
"vport5p2/name": "com.canonical.lxd",
408+
},
409+
True,
410+
id="viable_no_lxd_sock_with_canonical_lxd_virtio",
411+
),
364412
),
365413
)
366414
@mock.patch(DS_PATH + "os.lstat")
367-
@mock.patch(DS_PATH + "os.path.exists")
415+
@pytest.mark.usefixtures("fake_filesystem")
368416
def test_expected_viable(
369-
self, m_exists, m_lstat, exists, lstat_mode, expected
417+
self, m_lstat, exists, lstat_mode, virtio_ports, expected
370418
):
371419
"""Return True only when LXD_SOCKET_PATH exists and is a socket."""
372-
m_exists.return_value = exists
420+
if virtio_ports:
421+
populate_dir("/sys/class/virtio-ports", virtio_ports)
422+
if exists:
423+
ensure_file(lxd.LXD_SOCKET_PATH)
373424
m_lstat.return_value = LStatResponse(lstat_mode)
374425
assert expected is lxd.DataSourceLXD.ds_detect()
375-
m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)])
376426
if exists:
377427
m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)])
378428
else:
379429
assert 0 == m_lstat.call_count
380430

431+
@pytest.mark.usefixtures("fake_filesystem")
432+
@mock.patch(DS_PATH + "util.load_text_file", side_effect=OSError("Oh-no"))
433+
def test_warn_on_oserror(self, m_load_text_file, caplog):
434+
populate_dir("/sys/class/virtio-ports", {"vport5p1/name": "something"})
435+
with caplog.at_level(logging.WARNING):
436+
assert False is lxd.DataSourceLXD.ds_detect()
437+
assert "Cannot check virtio-ports: Oh-no" in caplog.messages
438+
381439

382440
class TestReadMetadata:
383441
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)