diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index be6290bb76..7e4a32b570 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -148,12 +148,24 @@ def _determine_fs_type( lsblk_info: LsblkInfo | None = None, ) -> FilesystemType | None: try: + if lsblk_info is not None: + if crypt_child := lsblk_info.find_child_by_type('crypt'): + if crypt_child.fstype: + try: + return FilesystemType(crypt_child.fstype) + except ValueError: + pass + if lsblk_info.fstype == FilesystemType.CRYPTO_LUKS: + return FilesystemType.CRYPTO_LUKS + if partition.fileSystem: if partition.fileSystem.type == FilesystemType.LINUX_SWAP.parted_value: return FilesystemType.LINUX_SWAP return FilesystemType(partition.fileSystem.type) - if lsblk_info is not None: - return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None + + if lsblk_info is not None and lsblk_info.fstype: + return FilesystemType(lsblk_info.fstype) + return None except ValueError: debug(f'Could not determine the filesystem: {partition.fileSystem}') @@ -191,8 +203,13 @@ def get_btrfs_info( subvol_infos: list[_BtrfsSubvolumeInfo] = [] - if not lsblk_info.mountpoint: - mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + target_lsblk = lsblk_info + if crypt_child := lsblk_info.find_child_by_type('crypt'): + if crypt_child.fstype == FilesystemType.BTRFS: + target_lsblk = crypt_child + + if not target_lsblk.mountpoint and not target_lsblk.mountpoints: + mount(target_lsblk.path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) mountpoint = self._TMP_BTRFS_MOUNT else: # when multiple subvolumes are mounted then the lsblk output may look like @@ -200,7 +217,7 @@ def get_btrfs_info( # "mountpoints": ["/mnt/archinstall/var/log", "/mnt/archinstall/home", ..] # so we'll determine the minimum common path and assume that's the root try: - common_path = os.path.commonpath(lsblk_info.mountpoints) + common_path = os.path.commonpath(target_lsblk.mountpoints) except ValueError: return subvol_infos @@ -217,7 +234,7 @@ def get_btrfs_info( # "fsroots": ["/@log", "/@home", "/@"...] # we'll thereby map the fsroot, which are the mounted filesystem roots # to the corresponding mountpoints - btrfs_subvol_info = dict(zip(lsblk_info.fsroots, lsblk_info.mountpoints, strict=True)) + btrfs_subvol_info = dict(zip(target_lsblk.fsroots, target_lsblk.mountpoints, strict=True)) # ID 256 gen 16 top level 5 path @ for line in result.splitlines(): @@ -227,8 +244,8 @@ def get_btrfs_info( sub_vol_mountpoint = btrfs_subvol_info.get('/' / name, None) subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) - if not lsblk_info.mountpoint: - umount(dev_path) + if not target_lsblk.mountpoint and not target_lsblk.mountpoints: + umount(self._TMP_BTRFS_MOUNT) return subvol_infos diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 11b89a9cad..68d4cb9a43 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -914,7 +914,7 @@ def minimal_installation( self._prepare_fs_type(part.fs_type) - if part in self._disk_encryption.partitions: + if part in self._disk_encryption.partitions or part.is_encrypted: self._prepare_encrypt() if ucode := self._get_microcode(): @@ -1105,26 +1105,27 @@ def _get_kernel_params_partition( ) -> list[str]: kernel_parameters = [] - if root_partition in self._disk_encryption.partitions: + if root_partition in self._disk_encryption.partitions or root_partition.is_encrypted: # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) + mapper_name = root_partition.mapper_name or 'root' if self._disk_encryption.hsm_device: debug(f'Root partition is an encrypted device, identifying by UUID: {root_partition.uuid}') # Note: UUID must be used, not PARTUUID for sd-encrypt to work - kernel_parameters.append(f'rd.luks.name={root_partition.uuid}=root') + kernel_parameters.append(f'rd.luks.name={root_partition.uuid}={mapper_name}') # Note: tpm2-device and fido2-device don't play along very well: # https://github.com/archlinux/archinstall/pull/1196#issuecomment-1129715645 kernel_parameters.append('rd.luks.options=fido2-device=auto,password-echo=no') - elif partuuid: + elif partuuid and root_partition.partuuid: debug(f'Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}') - kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:root') + kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:{mapper_name}') else: debug(f'Root partition is an encrypted device, identifying by UUID: {root_partition.uuid}') - kernel_parameters.append(f'cryptdevice=UUID={root_partition.uuid}:root') + kernel_parameters.append(f'cryptdevice=UUID={root_partition.uuid}:{mapper_name}') if id_root: - kernel_parameters.append('root=/dev/mapper/root') + kernel_parameters.append(f'root=/dev/mapper/{mapper_name}') elif id_root: if partuuid: debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index b4c069e659..f154d03e7a 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -551,6 +551,8 @@ class _PartitionInfo: disk: Disk mountpoints: list[Path] btrfs_subvol_infos: list[_BtrfsSubvolumeInfo] = field(default_factory=list) + is_encrypted: bool = False + mapper_name: str | None = None @property def sector_size(self) -> SectorSize: @@ -604,6 +606,12 @@ def from_partition( if btrfs_subvol_infos is None: btrfs_subvol_infos = [] + crypt_child = lsblk_info.find_child_by_type('crypt') + is_encrypted = (lsblk_info.fstype == FilesystemType.CRYPTO_LUKS) or (crypt_child is not None) + mapper_name = crypt_child.name if crypt_child else None + + mountpoints = lsblk_info.all_mountpoints if not lsblk_info.mountpoints else lsblk_info.mountpoints + return cls( partition=partition, name=partition.name, @@ -617,8 +625,10 @@ def from_partition( partuuid=lsblk_info.partuuid, uuid=lsblk_info.uuid, disk=partition.disk, - mountpoints=lsblk_info.mountpoints, + mountpoints=mountpoints, btrfs_subvol_infos=btrfs_subvol_infos, + is_encrypted=is_encrypted, + mapper_name=mapper_name, ) @@ -914,6 +924,8 @@ class PartitionModification(SubConfig): partn: int | None = None partuuid: str | None = None uuid: str | None = None + is_encrypted: bool = False + _mapper_name: str | None = None _obj_id: UUID | str = field(init=False) @@ -982,6 +994,8 @@ def from_existing_partition(cls, partition_info: _PartitionInfo) -> Self: flags=partition_info.flags, mountpoint=mountpoint, btrfs_subvols=subvol_mods, + is_encrypted=partition_info.is_encrypted, + _mapper_name=partition_info.mapper_name, ) @property @@ -1040,6 +1054,8 @@ def is_create_or_modify(self) -> bool: @property def mapper_name(self) -> str | None: + if self._mapper_name: + return self._mapper_name if self.is_root(): return 'root' if self.is_home(): @@ -1048,6 +1064,10 @@ def mapper_name(self) -> str | None: return f'{ENC_IDENTIFIER}{self.dev_path.name}' return None + @mapper_name.setter + def mapper_name(self, value: str | None) -> None: + self._mapper_name = value + def set_flag(self, flag: PartitionFlag) -> None: if flag not in self.flags: self.flags.append(flag) @@ -1807,6 +1827,23 @@ def remove_none(cls, value: Any) -> Any: def serialize_size(self, size: Size) -> str: return size.format_size(Unit.MiB) + @property + def all_mountpoints(self) -> list[Path]: + mountpoints = list(self.mountpoints) + for child in self.children: + for mp in child.all_mountpoints: + if mp not in mountpoints: + mountpoints.append(mp) + return mountpoints + + def find_child_by_type(self, device_type: str) -> LsblkInfo | None: + for child in self.children: + if child.type == device_type: + return child + if found := child.find_child_by_type(device_type): + return found + return None + @classmethod def fields(cls) -> list[str]: return [field.alias or name for name, field in cls.model_fields.items() if name != 'children'] diff --git a/tests/test_pre_mount.py b/tests/test_pre_mount.py new file mode 100644 index 0000000000..348bf72568 --- /dev/null +++ b/tests/test_pre_mount.py @@ -0,0 +1,452 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import parted +from pytest import MonkeyPatch + +from archinstall.lib.disk.device_handler import device_handler +from archinstall.lib.installer import Installer +from archinstall.lib.models.device import ( + BDevice, + DiskEncryption, + EncryptionType, + FilesystemType, + LsblkInfo, + ModificationStatus, + PartitionModification, + PartitionType, + SectorSize, + Size, + Unit, + _BtrfsSubvolumeInfo, + _PartitionInfo, +) + + +def _create_mock_parted_partition( + path: str, + name: str, + start: int, + length_bytes: int, + is_boot: bool = False, + is_esp: bool = False, +) -> MagicMock: + parted_part = MagicMock() + parted_part.type = parted.PARTITION_NORMAL + parted_part.name = name + parted_part.path = path + parted_part.geometry.start = start + parted_part.geometry.device.sectorSize = 512 + parted_part.getLength.return_value = length_bytes + parted_part.getFlag.side_effect = lambda f: (is_boot and f == parted.PARTITION_BOOT) or (is_esp and f == parted.PARTITION_ESP) + parted_part.disk.device.sectorSize = 512 + parted_part.disk.device.path = '/dev/nvme0n1' + parted_part.fileSystem = None + return parted_part + + +def test_lsblk_all_mountpoints_and_find_child() -> None: + child_crypt = LsblkInfo( + name='encrypted_p2', + path=Path('/dev/mapper/encrypted_p2'), + pkname='nvme0n1p2', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran=None, + partn=None, + partuuid=None, + parttype=None, + uuid='fs-uuid-p2', + fstype='ext4', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='crypt', + mountpoint=Path('/mnt'), + mountpoints=[Path('/mnt')], + fsroots=[], + children=[], + ) + + parent_part = LsblkInfo( + name='nvme0n1p2', + path=Path('/dev/nvme0n1p2'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=2, + partuuid='part-uuid-p2', + parttype=None, + uuid='luks-uuid-p2', + fstype='crypto_LUKS', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=None, + mountpoints=[], + fsroots=[], + children=[child_crypt], + ) + + assert parent_part.all_mountpoints == [Path('/mnt')] + found_child = parent_part.find_child_by_type('crypt') + assert found_child is not None + assert found_child.name == 'encrypted_p2' + assert found_child.path == Path('/dev/mapper/encrypted_p2') + + +def test_determine_fs_type_crypto_luks() -> None: + child_crypt = LsblkInfo( + name='encrypted_p2', + path=Path('/dev/mapper/encrypted_p2'), + pkname='nvme0n1p2', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran=None, + partn=None, + partuuid=None, + parttype=None, + uuid='fs-uuid-p2', + fstype='ext4', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='crypt', + mountpoint=Path('/mnt'), + mountpoints=[Path('/mnt')], + fsroots=[], + children=[], + ) + + parent_part = LsblkInfo( + name='nvme0n1p2', + path=Path('/dev/nvme0n1p2'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=2, + partuuid='part-uuid-p2', + parttype=None, + uuid='luks-uuid-p2', + fstype='crypto_LUKS', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=None, + mountpoints=[], + fsroots=[], + children=[child_crypt], + ) + + parted_part = _create_mock_parted_partition('/dev/nvme0n1p2', 'root', 2048, 100 * 1024 * 1024 * 1024) + fs_type = device_handler._determine_fs_type(parted_part, parent_part) + assert fs_type == FilesystemType.EXT4 + + +def test_determine_fs_type_crypto_luks_btrfs() -> None: + child_crypt = LsblkInfo( + name='encrypted_btrfs', + path=Path('/dev/mapper/encrypted_btrfs'), + pkname='nvme0n1p2', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran=None, + partn=None, + partuuid=None, + parttype=None, + uuid='fs-uuid-p2', + fstype='btrfs', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='crypt', + mountpoint=Path('/mnt'), + mountpoints=[Path('/mnt')], + fsroots=[Path('/@')], + children=[], + ) + + parent_part = LsblkInfo( + name='nvme0n1p2', + path=Path('/dev/nvme0n1p2'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=2, + partuuid='part-uuid-p2', + parttype=None, + uuid='luks-uuid-p2', + fstype='crypto_LUKS', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=None, + mountpoints=[], + fsroots=[], + children=[child_crypt], + ) + + parted_part = _create_mock_parted_partition('/dev/nvme0n1p2', 'root', 2048, 100 * 1024 * 1024 * 1024) + fs_type = device_handler._determine_fs_type(parted_part, parent_part) + assert fs_type == FilesystemType.BTRFS + + +def test_detect_pre_mounted_encrypted_root(monkeypatch: MonkeyPatch) -> None: + child_crypt = LsblkInfo( + name='encrypted_p2', + path=Path('/dev/mapper/encrypted_p2'), + pkname='nvme0n1p2', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran=None, + partn=None, + partuuid=None, + parttype=None, + uuid='fs-uuid-p2', + fstype='ext4', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='crypt', + mountpoint=Path('/mnt'), + mountpoints=[Path('/mnt')], + fsroots=[], + children=[], + ) + + parent_luks = LsblkInfo( + name='nvme0n1p2', + path=Path('/dev/nvme0n1p2'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=2, + partuuid='part-uuid-p2', + parttype=None, + uuid='luks-uuid-p2', + fstype='crypto_LUKS', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=None, + mountpoints=[], + fsroots=[], + children=[child_crypt], + ) + + boot_lsblk = LsblkInfo( + name='nvme0n1p1', + path=Path('/dev/nvme0n1p1'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(512, Unit.MiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=1, + partuuid='part-uuid-p1', + parttype=None, + uuid='uuid-p1', + fstype='vfat', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=Path('/mnt/boot'), + mountpoints=[Path('/mnt/boot')], + fsroots=[], + children=[], + ) + + parted_boot = _create_mock_parted_partition( + '/dev/nvme0n1p1', + 'boot', + 2048, + 512 * 1024 * 1024, + is_boot=True, + is_esp=True, + ) + parted_root = _create_mock_parted_partition( + '/dev/nvme0n1p2', + 'root', + 1050624, + 100 * 1024 * 1024 * 1024, + ) + + part_info_boot = _PartitionInfo.from_partition(parted_boot, boot_lsblk, FilesystemType.FAT32) + part_info_root = _PartitionInfo.from_partition(parted_root, parent_luks, FilesystemType.EXT4) + + assert part_info_root.is_encrypted is True + assert part_info_root.mapper_name == 'encrypted_p2' + assert part_info_root.mountpoints == [Path('/mnt')] + + bdev = MagicMock(spec=BDevice) + bdev.partition_infos = [part_info_boot, part_info_root] + monkeypatch.setattr(device_handler, '_devices', {Path('/dev/nvme0n1'): bdev}) + + device_mods = device_handler.detect_pre_mounted_mods(Path('/mnt')) + assert len(device_mods) == 1 + + dev_mod = device_mods[0] + assert len(dev_mod.partitions) == 2 + + root_mod = dev_mod.get_root_partition() + assert root_mod is not None + assert root_mod.is_root() is True + assert root_mod.mountpoint == Path('/') + assert root_mod.is_encrypted is True + assert root_mod.mapper_name == 'encrypted_p2' + + boot_mod = dev_mod.get_boot_partition() + assert boot_mod is not None + assert boot_mod.mountpoint == Path('/boot') + + +def test_detect_pre_mounted_encrypted_btrfs_root(monkeypatch: MonkeyPatch) -> None: + child_crypt = LsblkInfo( + name='crypt_btrfs', + path=Path('/dev/mapper/crypt_btrfs'), + pkname='nvme0n1p2', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran=None, + partn=None, + partuuid=None, + parttype=None, + uuid='fs-uuid-p2', + fstype='btrfs', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='crypt', + mountpoint=Path('/mnt'), + mountpoints=[Path('/mnt'), Path('/mnt/home')], + fsroots=[Path('/@'), Path('/@home')], + children=[], + ) + + parent_luks = LsblkInfo( + name='nvme0n1p2', + path=Path('/dev/nvme0n1p2'), + pkname='nvme0n1', + **{'log-sec': 512}, + size=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + pttype=None, + ptuuid=None, + rota=False, + tran='nvme', + partn=2, + partuuid='part-uuid-p2', + parttype=None, + uuid='luks-uuid-p2', + fstype='crypto_LUKS', + fsver=None, + fsavail=None, + **{'fsuse%': None}, + type='part', + mountpoint=None, + mountpoints=[], + fsroots=[], + children=[child_crypt], + ) + + parted_root = _create_mock_parted_partition( + '/dev/nvme0n1p2', + 'root', + 1050624, + 100 * 1024 * 1024 * 1024, + ) + + btrfs_subvols = [ + _BtrfsSubvolumeInfo(Path('@'), Path('/mnt')), + _BtrfsSubvolumeInfo(Path('@home'), Path('/mnt/home')), + ] + part_info = _PartitionInfo.from_partition( + parted_root, + parent_luks, + FilesystemType.BTRFS, + btrfs_subvol_infos=btrfs_subvols, + ) + + bdev = MagicMock(spec=BDevice) + bdev.partition_infos = [part_info] + monkeypatch.setattr(device_handler, '_devices', {Path('/dev/nvme0n1'): bdev}) + + device_mods = device_handler.detect_pre_mounted_mods(Path('/mnt')) + assert len(device_mods) == 1 + + root_mod = device_mods[0].get_root_partition() + assert root_mod is not None + assert root_mod.is_root() is True + assert root_mod.is_encrypted is True + assert root_mod.mapper_name == 'crypt_btrfs' + assert len(root_mod.btrfs_subvols) == 2 + assert root_mod.btrfs_subvols[0].mountpoint == Path('/') + assert root_mod.btrfs_subvols[1].mountpoint == Path('/home') + + +def test_kernel_params_pre_mounted_encrypted_root() -> None: + root_mod = PartitionModification( + status=ModificationStatus.EXIST, + type=PartitionType.PRIMARY, + start=Size(1050624, Unit.sectors, SectorSize(512, Unit.B)), + length=Size(100, Unit.GiB, SectorSize(512, Unit.B)), + fs_type=FilesystemType.EXT4, + dev_path=Path('/dev/nvme0n1p2'), + partn=2, + partuuid='part-uuid-p2', + uuid='luks-uuid-p2', + flags=[], + mountpoint=Path('/'), + is_encrypted=True, + _mapper_name='encrypted_p2', + ) + + installer = MagicMock(spec=Installer) + installer._disk_encryption = DiskEncryption(EncryptionType.NO_ENCRYPTION) + + # Test with partuuid=True + params_partuuid = Installer._get_kernel_params_partition(installer, root_mod, id_root=True, partuuid=True) + assert 'cryptdevice=PARTUUID=part-uuid-p2:encrypted_p2' in params_partuuid + assert 'root=/dev/mapper/encrypted_p2' in params_partuuid + + # Test with partuuid=False (falls back to UUID) + params_uuid = Installer._get_kernel_params_partition(installer, root_mod, id_root=True, partuuid=False) + assert 'cryptdevice=UUID=luks-uuid-p2:encrypted_p2' in params_uuid + assert 'root=/dev/mapper/encrypted_p2' in params_uuid