Files
zaza-openstack-tests/unit_tests/charm_tests/test_utils.py
Martin Kalcok 3d8df93b63 test_utils: Ensure generic kernel
Some LXD images may come with KVM kernel that does not contain
drivers for VFIO. Replace it with generic kernel before we try
to enable VFIO.

Signed-off-by: Martin Kalcok <martin.kalcok@canonical.com>
2025-05-21 13:33:35 +02:00

367 lines
15 KiB
Python

# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest import mock
import zaza.openstack.charm_tests.test_utils as test_utils
import unit_tests.utils as ut_utils
class TestBaseCharmTest(ut_utils.BaseTestCase):
def setUp(self):
super(TestBaseCharmTest, self).setUp()
self.target = test_utils.BaseCharmTest()
def patch_target(self, attr, return_value=None):
mocked = mock.patch.object(self.target, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
self._patches_start[attr] = started
setattr(self, attr, started)
def test_get_my_tests_options(self):
class FakeTest(test_utils.BaseCharmTest):
def method(self, test_config):
self.test_config = test_config
return self.get_my_tests_options('aKey', 'aDefault')
f = FakeTest()
self.assertEqual(f.method({}), 'aDefault')
self.assertEqual(f.method({
'tests_options': {
'unit_tests.charm_tests.test_utils.'
'FakeTest.method.aKey': 'aValue',
},
}), 'aValue')
def test_config_change(self):
default_config = {'fakeKey': 'testProvidedDefault'}
alterna_config = {'fakeKey': 'testProvidedAlterna'}
self.target.model_name = 'aModel'
self.target.test_config = {}
self.patch_target('config_current')
self.config_current.return_value = default_config
self.patch_object(test_utils.model, 'set_application_config')
self.patch_object(test_utils.model, 'wait_for_agent_status')
self.patch_object(test_utils.model, 'wait_for_application_states')
self.patch_object(test_utils.model, 'block_until_all_units_idle')
with self.target.config_change(
default_config, alterna_config, application_name='anApp'):
self.set_application_config.assert_called_once_with(
'anApp', alterna_config, model_name='aModel')
self.wait_for_agent_status.assert_called_once_with(
model_name='aModel')
self.wait_for_application_states.assert_called_once_with(
model_name='aModel', states={})
self.block_until_all_units_idle.assert_called_once_with()
# after yield we will have different calls than the above, measure both
self.set_application_config.assert_has_calls([
mock.call('anApp', alterna_config, model_name='aModel'),
mock.call('anApp', default_config, model_name='aModel'),
])
self.wait_for_application_states.assert_has_calls([
mock.call(model_name='aModel', states={}),
mock.call(model_name='aModel', states={}),
])
self.block_until_all_units_idle.assert_has_calls([
mock.call(),
mock.call(),
])
# confirm operation with `reset_to_charm_default`
self.set_application_config.reset_mock()
self.wait_for_agent_status.reset_mock()
self.wait_for_application_states.reset_mock()
self.patch_object(test_utils.model, 'reset_application_config')
with self.target.config_change(
default_config, alterna_config, application_name='anApp',
reset_to_charm_default=True):
self.set_application_config.assert_called_once_with(
'anApp', alterna_config, model_name='aModel')
# we want to assert this not to be called after yield
self.set_application_config.reset_mock()
self.assertFalse(self.set_application_config.called)
self.reset_application_config.assert_called_once_with(
'anApp', list(alterna_config.keys()), model_name='aModel')
self.wait_for_application_states.assert_has_calls([
mock.call(model_name='aModel', states={}),
mock.call(model_name='aModel', states={}),
])
self.block_until_all_units_idle.assert_has_calls([
mock.call(),
mock.call(),
])
# confirm operation where both default and alternate config passed in
# are the same. This is used to set config and not change it back.
self.set_application_config.reset_mock()
self.wait_for_agent_status.reset_mock()
self.wait_for_application_states.reset_mock()
self.reset_application_config.reset_mock()
with self.target.config_change(
alterna_config, alterna_config, application_name='anApp'):
self.set_application_config.assert_called_once_with(
'anApp', alterna_config, model_name='aModel')
# we want to assert these not to be called after yield
self.set_application_config.reset_mock()
self.wait_for_agent_status.reset_mock()
self.wait_for_application_states.reset_mock()
self.assertFalse(self.set_application_config.called)
self.assertFalse(self.reset_application_config.called)
self.assertFalse(self.wait_for_agent_status.called)
self.assertFalse(self.wait_for_application_states.called)
def test_separate_non_string_config(self):
intended_cfg_keys = ['foo2', 'foo3', 'foo4', 'foo5']
current_config_mock = {
'foo2': None,
'foo3': 'old_bar3',
'foo4': None,
'foo5': 'old_bar5',
}
self.patch_target('config_current')
self.config_current.return_value = current_config_mock
non_string_type_keys = ['foo2', 'foo3', 'foo4']
expected_result_filtered = {
'foo3': 'old_bar3',
'foo5': 'old_bar5',
}
expected_result_special = {
'foo2': None,
'foo4': None,
}
current, non_string = (
self.target.config_current_separate_non_string_type_keys(
non_string_type_keys, intended_cfg_keys, 'application_name')
)
self.assertEqual(expected_result_filtered, current)
self.assertEqual(expected_result_special, non_string)
self.config_current.assert_called_once_with(
'application_name', intended_cfg_keys)
def test_separate_special_config_None_params(self):
current_config_mock = {
'foo1': 'old_bar1',
'foo2': None,
'foo3': 'old_bar3',
'foo4': None,
'foo5': 'old_bar5',
}
self.patch_target('config_current')
self.config_current.return_value = current_config_mock
non_string_type_keys = ['foo2', 'foo3', 'foo4']
expected_result_filtered = {
'foo1': 'old_bar1',
'foo3': 'old_bar3',
'foo5': 'old_bar5',
}
expected_result_special = {
'foo2': None,
'foo4': None,
}
current, non_string = (
self.target.config_current_separate_non_string_type_keys(
non_string_type_keys)
)
self.assertEqual(expected_result_filtered, current)
self.assertEqual(expected_result_special, non_string)
self.config_current.assert_called_once_with(None, None)
@mock.patch('zaza.openstack.utilities.generic.get_pkg_version')
def test_package_version_matches(self, get_pkg_version):
versions = ['4.3.0', '4.0.0']
def _check_should_not_run():
package_version = test_utils.package_version_matches(
'hacluster', 'crmsh', versions=versions, op='eq')
if package_version and package_version != '4.4.1':
return
raise Exception('should not run')
for version in versions:
get_pkg_version.return_value = version
_check_should_not_run()
get_pkg_version.reset_mock()
get_pkg_version.return_value = '4.4.1'
self.assertRaises(Exception, _check_should_not_run)
def test_enable_hugepages_vfio_on_hvs_in_vms(self):
"""Test basic happy path for enabling huge pages and VFIO."""
self.patch_object(test_utils.model, 'get_units')
self.patch_object(test_utils.zaza.utilities.machine_os,
'get_hv_application')
self.patch_object(test_utils.zaza.utilities.machine_os, 'is_vm')
self.patch_object(test_utils.zaza.utilities.juju, 'remote_run')
self.patch_target('assert_unit_cpu_topology')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_hugepages')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_vfio_unsafe_noiommu_mode')
self.patch_object(test_utils.model, 'wait_for_application_states')
nr_hugepages = 4
unit = mock.MagicMock()
unit.name = 'unitA'
model_name = 'zaza-123'
self.target.model_name = model_name
self.target.test_config = {}
self.get_units.return_value = [unit]
self.is_vm.return_value = True
self.remote_run.return_value = '5.15.0-1080'
self.target.enable_hugepages_vfio_on_hvs_in_vms(nr_hugepages)
self.remote_run.assert_called_once_with(
unit.name,
'uname -r',
model_name=self.target.model_name,
fatal=True)
self.enable_hugepages.assert_called_once_with(
unit,
nr_hugepages,
model_name=self.target.model_name)
self.enable_vfio_unsafe_noiommu_mode.assert_called_once_with(
unit,
model_name=self.target.model_name)
def test_enable_hugepages_vfio_on_hvs_in_vms_kvm_kernel(self):
"""Test enabling huge pages and VFIO if machine has KVM kernel."""
self.patch_object(test_utils.model, 'get_units')
self.patch_object(test_utils.zaza.utilities.machine_os,
'get_hv_application')
self.patch_object(test_utils.zaza.utilities.machine_os, 'is_vm')
self.patch_object(test_utils.zaza.utilities.juju, 'remote_run')
self.patch_target('assert_unit_cpu_topology')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_hugepages')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_vfio_unsafe_noiommu_mode')
self.patch_object(test_utils.model, 'wait_for_application_states')
nr_hugepages = 4
unit = mock.MagicMock()
unit.name = 'unitA'
model_name = 'zaza-123'
self.target.model_name = model_name
self.target.test_config = {}
self.get_units.return_value = [unit]
self.is_vm.return_value = True
self.remote_run.return_value = '5.15.0-1080-kvm'
replace_kernel_cmd = ('export DEBIAN_FRONTEND=noninteractive && '
'apt-get update && '
'apt remove -yqq linux-*-kvm && '
'apt install -yqq linux-generic')
remote_calls = [
mock.call(unit.name, 'uname -r', model_name=self.target.model_name,
fatal=True),
mock.call(unit.name, replace_kernel_cmd,
model_name=self.target.model_name, fatal=True),
]
self.target.enable_hugepages_vfio_on_hvs_in_vms(nr_hugepages)
self.remote_run.assert_has_calls(remote_calls)
self.enable_hugepages.assert_called_once_with(
unit,
nr_hugepages,
model_name=self.target.model_name)
self.enable_vfio_unsafe_noiommu_mode.assert_called_once_with(
unit,
model_name=self.target.model_name)
def test_enable_hugepages_vfio_on_hvs_in_vms_recover_unit_error(self):
"""Test recovering from UnitError when enabling huge pages and VFIO.
Unit can go into Error state during reboot due to
https://bugs.launchpad.net/juju/+bug/2077936. This can be detected
during enabling of hugepages or when enabling VFIO and it's a
recoverable error.
"""
self.patch_object(test_utils.model, 'get_units')
self.patch_object(test_utils.zaza.utilities.machine_os,
'get_hv_application')
self.patch_object(test_utils.zaza.utilities.machine_os, 'is_vm')
self.patch_object(test_utils.zaza.utilities.juju, 'remote_run')
self.patch_target('assert_unit_cpu_topology')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_hugepages')
self.patch_object(test_utils.zaza.utilities.machine_os,
'enable_vfio_unsafe_noiommu_mode')
self.patch_object(test_utils.model, 'wait_for_application_states')
self.patch_object(test_utils.model, 'resolve_units')
nr_hugepages = 4
unit = mock.MagicMock()
unit.name = 'unitA'
model_name = 'zaza-123'
self.target.model_name = model_name
self.target.test_config = {}
self.get_units.return_value = [unit]
self.is_vm.return_value = True
self.remote_run.return_value = '5.15.0-1080'
# Both huge pages and wait for application state after VFIO
# can detect unit in Error state
self.enable_hugepages.side_effect = \
test_utils.zaza.model.UnitError(unit)
self.wait_for_application_states.side_effect = \
[test_utils.zaza.model.UnitError(unit), None]
self.target.enable_hugepages_vfio_on_hvs_in_vms(nr_hugepages)
self.remote_run.assert_called_once_with(
unit.name,
'uname -r',
model_name=self.target.model_name,
fatal=True)
self.enable_hugepages.assert_called_once_with(
unit,
nr_hugepages,
model_name=self.target.model_name)
self.enable_vfio_unsafe_noiommu_mode.assert_called_once_with(
unit,
model_name=self.target.model_name)
self.resolve_units.assert_has_calls([mock.call(), mock.call()])
# application state is awaited second time after recovering from error
self.wait_for_application_states.assert_has_calls(
[
mock.call(model_name=self.target.model_name, states={}),
mock.call(model_name=self.target.model_name, states={})
]
)
class TestOpenStackBaseTest(ut_utils.BaseTestCase):
def test_setUpClass(self):
self.patch_object(test_utils.openstack_utils, 'get_cacert')
self.patch_object(test_utils.openstack_utils,
'get_overcloud_keystone_session')
self.patch_object(test_utils.BaseCharmTest, 'setUpClass')
class MyTestClass(test_utils.OpenStackBaseTest):
model_name = 'deadbeef'
MyTestClass.setUpClass('foo', 'bar')
self.setUpClass.assert_called_with('foo', 'bar')