From ea64876595639f721793d28ce12f2f8cbf0dbafd Mon Sep 17 00:00:00 2001 From: Guilherme Sales Date: Tue, 28 Apr 2026 17:23:40 +0100 Subject: [PATCH] Add installer teardown for LUKS and LVM layouts --- archinstall/lib/disk/lvm.py | 8 ++ archinstall/lib/installer.py | 166 +++++++++++++++++++++++++++++------ 2 files changed, 148 insertions(+), 26 deletions(-) diff --git a/archinstall/lib/disk/lvm.py b/archinstall/lib/disk/lvm.py index 34ae9a2696..5ee7b04107 100644 --- a/archinstall/lib/disk/lvm.py +++ b/archinstall/lib/disk/lvm.py @@ -118,6 +118,14 @@ def lvm_vol_change(vol: LvmVolume, activate: bool) -> None: SysCommand(cmd) +def lvm_vg_change(vg: LvmVolumeGroup, activate: bool) -> None: + active_flag = 'y' if activate else 'n' + cmd = ['vgchange', '-a', active_flag, vg.name] + + debug(f'vgchange volume group: {" ".join(cmd)}') + SysCommand(cmd) + + def lvm_export_vg(vg: LvmVolumeGroup) -> None: cmd = f'vgexport {vg.name}' diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 7bbfbae57b..5e29286ded 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -6,6 +6,7 @@ import textwrap import time from collections.abc import Callable +from contextlib import suppress from pathlib import Path from subprocess import CalledProcessError from types import TracebackType @@ -16,7 +17,7 @@ from archinstall.lib.command import SysCommand, run from archinstall.lib.disk.fido import Fido2 from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev -from archinstall.lib.disk.lvm import lvm_import_vg, lvm_pvseg_info, lvm_vol_change +from archinstall.lib.disk.lvm import lvm_import_vg, lvm_pvseg_info, lvm_vg_change, lvm_vol_change from archinstall.lib.disk.utils import ( get_lsblk_by_mountpoint, get_lsblk_info, @@ -24,6 +25,8 @@ get_unique_path_for_device, mount, swapon, + udev_sync, + umount, ) from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError, RequirementError, ServiceException, SysCallError from archinstall.lib.hardware import SysInfo @@ -131,45 +134,51 @@ def __init__( self._zram_enabled = False self._disable_fstrim = False - + self._layout_teardown_required = False self.pacman = Pacman(self.target, silent) def __enter__(self) -> Self: return self def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: - if exc_type is not None: - error(str(exc_value)) + try: + if exc_type is not None: + error(str(exc_value)) - self.sync_log_to_install_medium() + self.sync_log_to_install_medium() - # We avoid printing /mnt/ because that might confuse people if they note it down - # and then reboot, and an identical log file will be found in the ISO medium anyway. - print(tr('[!] A log file has been created here: {}').format(logger.path)) - print(tr('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')) + # We avoid printing /mnt/ because that might confuse people if they note it down + # and then reboot, and an identical log file will be found in the ISO medium anyway. + print(tr('[!] A log file has been created here: {}').format(logger.path)) + print(tr('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')) - # Return None to propagate the exception - return None + # Return None to propagate the exception + return None - info(tr('Syncing the system...')) - os.sync() + info(tr('Syncing the system...')) + os.sync() - if not (missing_steps := self.post_install_check()): - msg = f'Installation completed without any errors.\nLog files temporarily available at {logger.directory}.\nYou may reboot when ready.\n' - log(msg, fg='green') - self.sync_log_to_install_medium() - return True - else: - warn('Some required steps were not successfully installed/configured before leaving the installer:') + if not (missing_steps := self.post_install_check()): + msg = f'Installation completed without any errors.\nLog files temporarily available at {logger.directory}.\nYou may reboot when ready.\n' + log(msg, fg='green') + self.sync_log_to_install_medium() + return True + else: + warn('Some required steps were not successfully installed/configured before leaving the installer:') - for step in missing_steps: - warn(f' - {step}') + for step in missing_steps: + warn(f' - {step}') - warn(f'Detailed error logs can be found at: {logger.directory}') - warn('Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues') + warn(f'Detailed error logs can be found at: {logger.directory}') + warn('Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues') - self.sync_log_to_install_medium() - return False + self.sync_log_to_install_medium() + return False + finally: + try: + self.teardown() + except Exception as err: + warn(f'Failed to teardown installation target: {err}') def remove_mod(self, mod: str) -> None: if mod in self._modules: @@ -257,6 +266,7 @@ def sanity_check( def mount_ordered_layout(self) -> None: debug('Mounting ordered layout') + self._layout_teardown_required = True luks_handlers: dict[Any, Luks2] = {} @@ -438,6 +448,110 @@ def _mount_btrfs_subvol( options = mount_options + [f'subvol={subvol.name}'] mount(dev_path, mountpoint, options=options) + def teardown(self) -> None: + if not self._layout_teardown_required: + debug('No mounted layout registered for teardown') + return + + info('Tearing down installation target') + + with suppress(Exception): + os.sync() + + self._swapoff_layout() + + with suppress(SysCallError, DiskError): + umount(self.target, recursive=True) + + match self._disk_encryption.encryption_type: + case EncryptionType.LUKS: + self._teardown_luks_partitions() + + case EncryptionType.LUKS_ON_LVM: + self._teardown_luks_lvm() + self._teardown_lvm() + + case EncryptionType.LVM_ON_LUKS: + self._teardown_lvm() + self._teardown_luks_partitions() + + case EncryptionType.NO_ENCRYPTION: + self._teardown_lvm() + + with suppress(SysCallError): + udev_sync() + + self._layout_teardown_required = False + + def _swapoff_path(self, path: Path | None) -> None: + if path is None: + return + + with suppress(SysCallError, DiskError): + SysCommand(['swapoff', str(path)]) + + def _swapoff_layout(self) -> None: + for mod in self._disk_config.device_modifications: + for part in mod.partitions: + if part.is_swap(): + self._swapoff_path(part.dev_path) + + if part.mapper_name: + self._swapoff_path(Path('/dev/mapper') / part.mapper_name) + + if not self._disk_config.lvm_config: + return + + for vol in self._disk_config.lvm_config.get_all_volumes(): + if vol.fs_type == FilesystemType.LINUX_SWAP: + self._swapoff_path(vol.dev_path) + + if vol.mapper_name: + self._swapoff_path(Path('/dev/mapper') / vol.mapper_name) + + def _teardown_lvm(self) -> None: + lvm_config = self._disk_config.lvm_config + + if not lvm_config: + return + + for vg in reversed(lvm_config.vol_groups): + for vol in reversed(vg.volumes): + if not vol.dev_path: + continue + + with suppress(SysCallError, DiskError): + lvm_vol_change(vol, False) + + with suppress(SysCallError, DiskError): + lvm_vg_change(vg, False) + + def _teardown_luks_partitions(self) -> None: + for part_mod in reversed(self._disk_encryption.partitions): + if not part_mod.dev_path or not part_mod.mapper_name: + continue + + luks_handler = Luks2( + part_mod.dev_path, + mapper_name=part_mod.mapper_name, + ) + + with suppress(SysCallError, DiskError): + luks_handler.lock() + + def _teardown_luks_lvm(self) -> None: + for vol in reversed(self._disk_encryption.lvm_volumes): + if not vol.dev_path or not vol.mapper_name: + continue + + luks_handler = Luks2( + vol.dev_path, + mapper_name=vol.mapper_name, + ) + + with suppress(SysCallError, DiskError): + luks_handler.lock() + def generate_key_files(self) -> None: match self._disk_encryption.encryption_type: case EncryptionType.LUKS: