summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/integration_tests/bugs/test_lp1920939.py140
-rw-r--r--tests/integration_tests/clouds.py19
-rw-r--r--tests/integration_tests/conftest.py8
-rw-r--r--tox.ini1
4 files changed, 159 insertions, 9 deletions
diff --git a/tests/integration_tests/bugs/test_lp1920939.py b/tests/integration_tests/bugs/test_lp1920939.py
new file mode 100644
index 00000000..408792a6
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1920939.py
@@ -0,0 +1,140 @@
+"""
+Test that disk setup can run successfully on a mounted partition when
+partprobe is being used.
+
+lp-1920939
+"""
+import json
+import os
+import pytest
+from uuid import uuid4
+from pycloudlib.lxd.instance import LXDInstance
+
+from cloudinit.subp import subp
+from tests.integration_tests.instances import IntegrationInstance
+
+DISK_PATH = '/tmp/test_disk_setup_{}'.format(uuid4())
+
+
+def setup_and_mount_lxd_disk(instance: LXDInstance):
+ subp('lxc config device add {} test-disk-setup-disk disk source={}'.format(
+ instance.name, DISK_PATH).split())
+
+
+@pytest.yield_fixture
+def create_disk():
+ # 640k should be enough for anybody
+ subp('dd if=/dev/zero of={} bs=1k count=640'.format(DISK_PATH).split())
+ yield
+ os.remove(DISK_PATH)
+
+
+USERDATA = """\
+#cloud-config
+disk_setup:
+ /dev/sdb:
+ table_type: mbr
+ layout: [50, 50]
+ overwrite: True
+fs_setup:
+ - label: test
+ device: /dev/sdb1
+ filesystem: ext4
+ - label: test2
+ device: /dev/sdb2
+ filesystem: ext4
+mounts:
+- ["/dev/sdb1", "/mnt1"]
+- ["/dev/sdb2", "/mnt2"]
+"""
+
+UPDATED_USERDATA = """\
+#cloud-config
+disk_setup:
+ /dev/sdb:
+ table_type: mbr
+ layout: [100]
+ overwrite: True
+fs_setup:
+ - label: test3
+ device: /dev/sdb1
+ filesystem: ext4
+mounts:
+- ["/dev/sdb1", "/mnt3"]
+"""
+
+
+def _verify_first_disk_setup(client, log):
+ assert 'Traceback' not in log
+ assert 'WARN' not in log
+ lsblk = json.loads(client.execute('lsblk --json'))
+ sdb = [x for x in lsblk['blockdevices'] if x['name'] == 'sdb'][0]
+ assert len(sdb['children']) == 2
+ assert sdb['children'][0]['name'] == 'sdb1'
+ assert sdb['children'][0]['mountpoint'] == '/mnt1'
+ assert sdb['children'][1]['name'] == 'sdb2'
+ assert sdb['children'][1]['mountpoint'] == '/mnt2'
+
+
+@pytest.mark.user_data(USERDATA)
+@pytest.mark.lxd_setup.with_args(setup_and_mount_lxd_disk)
+@pytest.mark.ubuntu
+@pytest.mark.lxd_vm
+# Not bionic or xenial because the LXD agent gets in the way of us
+# changing the userdata
+@pytest.mark.not_bionic
+@pytest.mark.not_xenial
+def test_disk_setup_when_mounted(create_disk, client: IntegrationInstance):
+ """Test lp-1920939.
+
+ We insert an extra disk into our VM, format it to have two partitions,
+ modify our cloud config to mount devices before disk setup, and modify
+ our userdata to setup a single partition on the disk.
+
+ This allows cloud-init to attempt disk setup on a mounted partition.
+ When blockdev is in use, it will fail with
+ "blockdev: ioctl error on BLKRRPART: Device or resource busy" along
+ with a warning and a traceback. When partprobe is in use, everything
+ should work successfully.
+ """
+ log = client.read_from_file('/var/log/cloud-init.log')
+ _verify_first_disk_setup(client, log)
+
+ # Update our userdata and cloud.cfg to mount then perform new disk setup
+ client.write_to_file(
+ '/var/lib/cloud/seed/nocloud-net/user-data',
+ UPDATED_USERDATA
+ )
+ client.execute("sed -i 's/write-files/write-files\\n - mounts/' "
+ "/etc/cloud/cloud.cfg")
+
+ client.execute('cloud-init clean --logs')
+ client.restart()
+
+ # Assert new setup works as expected
+ assert 'Traceback' not in log
+ assert 'WARN' not in log
+
+ lsblk = json.loads(client.execute('lsblk --json'))
+ sdb = [x for x in lsblk['blockdevices'] if x['name'] == 'sdb'][0]
+ assert len(sdb['children']) == 1
+ assert sdb['children'][0]['name'] == 'sdb1'
+ assert sdb['children'][0]['mountpoint'] == '/mnt3'
+
+
+@pytest.mark.user_data(USERDATA)
+@pytest.mark.lxd_setup.with_args(setup_and_mount_lxd_disk)
+@pytest.mark.ubuntu
+@pytest.mark.lxd_vm
+def test_disk_setup_no_partprobe(create_disk, client: IntegrationInstance):
+ """Ensure disk setup still works as expected without partprobe."""
+ # We can't do this part in a bootcmd because the path has already
+ # been found by the time we get to the bootcmd
+ client.execute('rm $(which partprobe)')
+ client.execute('cloud-init clean --logs')
+ client.restart()
+
+ log = client.read_from_file('/var/log/cloud-init.log')
+ _verify_first_disk_setup(client, log)
+
+ assert 'partprobe' not in log
diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py
index 3bbccb44..1378b215 100644
--- a/tests/integration_tests/clouds.py
+++ b/tests/integration_tests/clouds.py
@@ -142,12 +142,12 @@ class IntegrationCloud(ABC):
except (ValueError, IndexError):
return image.image_id
- def _perform_launch(self, launch_kwargs):
+ def _perform_launch(self, launch_kwargs, **kwargs):
pycloudlib_instance = self.cloud_instance.launch(**launch_kwargs)
return pycloudlib_instance
def launch(self, user_data=None, launch_kwargs=None,
- settings=integration_settings):
+ settings=integration_settings, **kwargs):
if launch_kwargs is None:
launch_kwargs = {}
if self.settings.EXISTING_INSTANCE_ID:
@@ -158,21 +158,21 @@ class IntegrationCloud(ABC):
self.settings.EXISTING_INSTANCE_ID
)
return
- kwargs = {
+ default_launch_kwargs = {
'image_id': self.image_id,
'user_data': user_data,
}
- kwargs.update(launch_kwargs)
+ launch_kwargs = {**default_launch_kwargs, **launch_kwargs}
log.info(
"Launching instance with launch_kwargs:\n%s",
- "\n".join("{}={}".format(*item) for item in kwargs.items())
+ "\n".join("{}={}".format(*item) for item in launch_kwargs.items())
)
with emit_dots_on_travis():
- pycloudlib_instance = self._perform_launch(kwargs)
+ pycloudlib_instance = self._perform_launch(launch_kwargs, **kwargs)
log.info('Launched instance: %s', pycloudlib_instance)
instance = self.get_instance(pycloudlib_instance, settings)
- if kwargs.get('wait', True):
+ if launch_kwargs.get('wait', True):
# If we aren't waiting, we can't rely on command execution here
log.info(
'cloud-init version: %s',
@@ -292,7 +292,7 @@ class _LxdIntegrationCloud(IntegrationCloud):
).format(**format_variables)
subp(command.split())
- def _perform_launch(self, launch_kwargs):
+ def _perform_launch(self, launch_kwargs, **kwargs):
launch_kwargs['inst_type'] = launch_kwargs.pop('instance_type', None)
wait = launch_kwargs.pop('wait', True)
release = launch_kwargs.pop('image_id')
@@ -310,6 +310,9 @@ class _LxdIntegrationCloud(IntegrationCloud):
)
if self.settings.CLOUD_INIT_SOURCE == 'IN_PLACE':
self._mount_source(pycloudlib_instance)
+ if 'lxd_setup' in kwargs:
+ log.info("Running callback specified by 'lxd_setup' mark")
+ kwargs['lxd_setup'](pycloudlib_instance)
pycloudlib_instance.start(wait=wait)
return pycloudlib_instance
diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py
index 6f4ce8d3..5a543e39 100644
--- a/tests/integration_tests/conftest.py
+++ b/tests/integration_tests/conftest.py
@@ -213,6 +213,7 @@ def _client(request, fixture_utils, session_cloud: IntegrationCloud):
user_data = getter('user_data')
name = getter('instance_name')
lxd_config_dict = getter('lxd_config_dict')
+ lxd_setup = getter('lxd_setup')
lxd_use_exec = fixture_utils.closest_marker_args_or(
request, 'lxd_use_exec', None
)
@@ -238,9 +239,14 @@ def _client(request, fixture_utils, session_cloud: IntegrationCloud):
# run anywhere else. A failure flags up this discrepancy.
pytest.fail(XENIAL_LXD_VM_EXEC_MSG)
launch_kwargs["execute_via_ssh"] = False
+ local_launch_kwargs = {}
+ if lxd_setup is not None:
+ if not isinstance(session_cloud, _LxdIntegrationCloud):
+ pytest.skip('lxd_setup requres LXD')
+ local_launch_kwargs['lxd_setup'] = lxd_setup
with session_cloud.launch(
- user_data=user_data, launch_kwargs=launch_kwargs
+ user_data=user_data, launch_kwargs=launch_kwargs, **local_launch_kwargs
) as instance:
if lxd_use_exec is not None:
# Existing instances are not affected by the launch kwargs, so
diff --git a/tox.ini b/tox.ini
index a2981b98..9374a1cb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -177,6 +177,7 @@ markers =
openstack: test will only run on openstack platform
lxd_config_dict: set the config_dict passed on LXD instance creation
lxd_container: test will only run in LXD container
+ lxd_setup: specify callable to be called between init and start
lxd_use_exec: `execute` will use `lxc exec` instead of SSH
lxd_vm: test will only run in LXD VM
not_xenial: test cannot run on the xenial release