Skip to content

Commit

Permalink
Add integration test for lp-1920939
Browse files Browse the repository at this point in the history
In canonical#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`
  • Loading branch information
TheRealFalcon committed May 5, 2021
1 parent 8034acd commit 47d9847
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 9 deletions.
140 changes: 140 additions & 0 deletions tests/integration_tests/bugs/test_lp1920939.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 11 additions & 8 deletions tests/integration_tests/clouds.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,12 @@ def _get_initial_image(self):
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:
Expand All @@ -157,20 +157,20 @@ def launch(self, user_data=None, launch_kwargs=None,
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())
)

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',
Expand Down Expand Up @@ -290,7 +290,7 @@ def _mount_source(instance: LXDInstance):
).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')
Expand All @@ -308,6 +308,9 @@ def _perform_launch(self, launch_kwargs):
)
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

Expand Down
8 changes: 7 additions & 1 deletion tests/integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ markers =
openstack: test will only run on openstack
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
Expand Down

0 comments on commit 47d9847

Please sign in to comment.