From 4c3c36297ad199ee9325a48f7e56a9c099ec183f Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 14 May 2021 14:38:56 -0500 Subject: Add integration test for lp-1920939 (#891) In #856 we added the ability to use partprobe instead of blockdev for reading partitions. Test that partprobe succeeds where blockdev fails. Also add a mechanism to our integration tests to allow a callable to be called between `lxc init` and `lxc start` --- tests/integration_tests/bugs/test_lp1920939.py | 140 +++++++++++++++++++++++++ tests/integration_tests/clouds.py | 19 ++-- tests/integration_tests/conftest.py | 8 +- tox.ini | 1 + 4 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 tests/integration_tests/bugs/test_lp1920939.py 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 -- cgit v1.2.3