From 591cdb865befff8035d53b861d9ff95b5704ed64 Mon Sep 17 00:00:00 2001 From: Petr Stodulka Date: Fri, 14 Jul 2023 17:32:46 +0200 Subject: [PATCH 41/42] upgradeinitramfsgenerator: Check the free space prior the initeramfs generation Under rare conditions it's possible the last piece free space is consumed when the upgrade initramfs is generated. It's hard to hit this problems right now without additional customisations that consume more space than we expect. However, when it happens, it not good situation. From this point, check the remaining free space on the FS hosting the container. In case we have less than 500MB, do not even try. Possibly we will increase the value in future, but consider it good enough for now. --- .../libraries/upgradeinitramfsgenerator.py | 73 +++++++++++++++++++ .../unit_test_upgradeinitramfsgenerator.py | 14 ++++ 2 files changed, 87 insertions(+) diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py index f141d9e3..5a686a47 100644 --- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py +++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py @@ -20,6 +20,7 @@ from leapp.utils.deprecation import suppress_deprecation INITRAM_GEN_SCRIPT_NAME = 'generate-initram.sh' DRACUT_DIR = '/dracut' +DEDICATED_LEAPP_PART_URL = 'https://access.redhat.com/solutions/7011704' def _get_target_kernel_version(context): @@ -231,6 +232,77 @@ def prepare_userspace_for_initram(context): _copy_files(context, files) +def _get_fspace(path, convert_to_mibs=False, coefficient=1): + """ + Return the free disk space on given path. + + The default is in bytes, but if convert_to_mibs is True, return MiBs instead. + + Raises OSError if nothing exists on the given `path`. + + :param path: Path to an existing file or directory + :type path: str + :param convert_to_mibs: If True, convert the value to MiBs + :type convert_to_mibs: bool + :param coefficient: Coefficient to multiply the free space (e.g. 0.9 to have it 10% lower). Max: 1 + :type coefficient: float + :rtype: int + """ + # TODO(pstodulk): discuss the function params + # NOTE(pstodulk): This func is copied from the overlaygen.py lib + # probably it would make sense to make it public in the utils.py lib, + # but for now, let's keep it private + stat = os.statvfs(path) + + coefficient = min(coefficient, 1) + fspace_bytes = int(stat.f_frsize * stat.f_bavail * coefficient) + if convert_to_mibs: + return int(fspace_bytes / 1024 / 1024) # noqa: W1619; pylint: disable=old-division + return fspace_bytes + + +def _check_free_space(context): + """ + Raise StopActorExecutionError if there is less than 500MB of free space available. + + If there is not enough free space in the context, the initramfs will not be + generated successfully and it's hard to discover what was the issue. Also + the missing space is able to kill the leapp itself - trying to write to the + leapp.db when the FS hosting /var/lib/leapp is full, kills the framework + and the actor execution too - so there is no gentle way to handle such + exceptions when it happens. From this point, let's rather check the available + space in advance and stop the execution when it happens. + + It is not expected to hit this issue, but I was successful and I know + it's still possible even with all other changes (just it's much harder + now to hit it). So adding this seatbelt, that is not 100% bulletproof, + but I call it good enough. + + Currently protecting last 500MB. In case of problems, we can increase + the value. + """ + message = 'There is not enough space on the file system hosting /var/lib/leapp.' + hint = ( + 'Increase the free space on the filesystem hosting' + ' /var/lib/leapp by 500MB at minimum (suggested 1500MB).\n\n' + 'It is also a good practice to create dedicated partition' + ' for /var/lib/leapp when more space is needed, which can be' + ' dropped after the system upgrade is fully completed.' + ' For more info, see: {}' + .format(DEDICATED_LEAPP_PART_URL) + ) + detail = ( + 'Remaining free space is lower than 500MB which is not enough to' + ' be able to generate the upgrade initramfs. ' + ) + + if _get_fspace(context.base_dir, convert_to_mibs=True) < 500: + raise StopActorExecutionError( + message=message, + details={'hint': hint, 'detail': detail} + ) + + def generate_initram_disk(context): """ Function to actually execute the init ramdisk creation. @@ -238,6 +310,7 @@ def generate_initram_disk(context): Includes handling of specified dracut and kernel modules from the host when needed. The check for the 'conflicting' modules is in a separate actor. """ + _check_free_space(context) env = {} if get_target_major_version() == '9': env = {'SYSTEMD_SECCOMP': '0'} diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py index a2f1c837..8068e177 100644 --- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py +++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py @@ -10,6 +10,7 @@ from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, from leapp.utils.deprecation import suppress_deprecation from leapp.models import ( # isort:skip + FIPSInfo, RequiredUpgradeInitramPackages, # deprecated UpgradeDracutModule, # deprecated BootContent, @@ -250,6 +251,16 @@ def test_prepare_userspace_for_initram(monkeypatch, adjust_cwd, input_msgs, pkgs assert _sort_files(upgradeinitramfsgenerator._copy_files.args[1]) == _files +class MockedGetFspace(object): + def __init__(self, space): + self.space = space + + def __call__(self, dummy_path, convert_to_mibs=False): + if not convert_to_mibs: + return self.space + return int(self.space / 1024 / 1024) # noqa: W1619; pylint: disable=old-division + + @pytest.mark.parametrize('input_msgs,dracut_modules,kernel_modules', [ # test dracut modules with UpgradeDracutModule(s) - orig functionality (gen_UDM_list(MODULES[0]), MODULES[0], []), @@ -275,8 +286,11 @@ def test_generate_initram_disk(monkeypatch, input_msgs, dracut_modules, kernel_m monkeypatch.setattr(upgradeinitramfsgenerator, '_get_target_kernel_version', lambda _: '') monkeypatch.setattr(upgradeinitramfsgenerator, 'copy_kernel_modules', MockedCopyArgs()) monkeypatch.setattr(upgradeinitramfsgenerator, 'copy_boot_files', lambda dummy: None) + monkeypatch.setattr(upgradeinitramfsgenerator, '_get_fspace', MockedGetFspace(2*2**30)) upgradeinitramfsgenerator.generate_initram_disk(context) + # TODO(pstodulk): add tests for the check of the free space (sep. from this func) + # test now just that all modules have been passed for copying - so we know # all modules have been consumed detected_dracut_modules = set() -- 2.41.0