From 00b0ae7ba439a5a420095175b3bedd52c569db51 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 19 Apr 2023 20:55:42 +1000 Subject: PyParted and a large rewrite of the underlying partitioning (#1604) * Invert mypy files * Add optional pre-commit hooks * New profile structure * Serialize profiles * Use profile instead of classmethod * Custom profile setup * Separator between back * Support profile import via url * Move profiles module * Refactor files * Remove symlink * Add user to docker group * Update schema description * Handle list services * mypy fixes * mypy fixes * Rename profilesv2 to profiles * flake8 * mypy again * Support selecting DM * Fix mypy * Cleanup * Update greeter setting * Update schema * Revert toml changes * Poc external dependencies * Dependency support * New encryption menu * flake8 * Mypy and flake8 * Unify lsblk command * Update bootloader configuration * Git hooks * Fix import * Pyparted * Remove custom font setting * flake8 * Remove default preview * Manual partitioning menu * Update structure * Disk configuration * Update filesystem * luks2 encryption * Everything works until installation * Btrfsutil * Btrfs handling * Update btrfs * Save encryption config * Fix pipewire issue * Update mypy version * Update all pre-commit * Update package versions * Revert audio/pipewire * Merge master PRs * Add master changes * Merge master changes * Small renaming * Pull master changes * Reset disk enc after disk config change * Generate locals * Update naming * Fix imports * Fix broken sync * Fix pre selection on table menu * Profile menu * Update profile * Fix post_install * Added python-pyparted to PKGBUILD, this requires [testing] to be enabled in order to run makepkg. Package still works via python -m build etc. * Swaped around some setuptools logic in pyproject Since we define `package-data` and `packages` there should be no need for: ``` [tool.setuptools.packages.find] where = ["archinstall", "archinstall.*"] ``` * Removed pyproject collisions. Duplicate definitions. * Made sure pyproject.toml includes languages * Add example and update README * Fix pyproject issues * Generate locale * Refactor imports * Simplify imports * Add profile description and package examples * Align code * Fix mypy * Simplify imports * Fix saving config * Fix wrong luks merge * Refactor installation * Fix cdrom device loading * Fix wrongly merged code * Fix imports and greeter * Don't terminate on partprobe error * Use specific path on partprobe from luks * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update github workflow to test archinstall installation * Update sway merge * Generate locales * Update workflow --------- Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> --- archinstall/lib/disk/__init__.py | 47 +- archinstall/lib/disk/blockdevice.py | 301 ------- archinstall/lib/disk/btrfs/__init__.py | 56 -- archinstall/lib/disk/btrfs/btrfs_helpers.py | 136 --- archinstall/lib/disk/btrfs/btrfspartition.py | 109 --- archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py | 192 ---- archinstall/lib/disk/device_handler.py | 599 +++++++++++++ archinstall/lib/disk/device_model.py | 1033 ++++++++++++++++++++++ archinstall/lib/disk/diskinfo.py | 40 - archinstall/lib/disk/dmcryptdev.py | 48 - archinstall/lib/disk/encryption.py | 174 ---- archinstall/lib/disk/encryption_menu.py | 179 ++++ archinstall/lib/disk/fido.py | 94 ++ archinstall/lib/disk/filesystem.py | 343 ++----- archinstall/lib/disk/helpers.py | 556 ------------ archinstall/lib/disk/mapperdev.py | 92 -- archinstall/lib/disk/partition.py | 661 -------------- archinstall/lib/disk/partitioning_menu.py | 335 +++++++ archinstall/lib/disk/subvolume_menu.py | 101 +++ archinstall/lib/disk/user_guides.py | 240 ----- archinstall/lib/disk/validators.py | 48 - 21 files changed, 2451 insertions(+), 2933 deletions(-) delete mode 100644 archinstall/lib/disk/blockdevice.py delete mode 100644 archinstall/lib/disk/btrfs/__init__.py delete mode 100644 archinstall/lib/disk/btrfs/btrfs_helpers.py delete mode 100644 archinstall/lib/disk/btrfs/btrfspartition.py delete mode 100644 archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py create mode 100644 archinstall/lib/disk/device_handler.py create mode 100644 archinstall/lib/disk/device_model.py delete mode 100644 archinstall/lib/disk/diskinfo.py delete mode 100644 archinstall/lib/disk/dmcryptdev.py delete mode 100644 archinstall/lib/disk/encryption.py create mode 100644 archinstall/lib/disk/encryption_menu.py create mode 100644 archinstall/lib/disk/fido.py delete mode 100644 archinstall/lib/disk/helpers.py delete mode 100644 archinstall/lib/disk/mapperdev.py delete mode 100644 archinstall/lib/disk/partition.py create mode 100644 archinstall/lib/disk/partitioning_menu.py create mode 100644 archinstall/lib/disk/subvolume_menu.py delete mode 100644 archinstall/lib/disk/user_guides.py delete mode 100644 archinstall/lib/disk/validators.py (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 352d04b9..cdc96373 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -1,7 +1,40 @@ -from .btrfs import * -from .helpers import * -from .blockdevice import BlockDevice -from .filesystem import Filesystem, MBR, GPT -from .partition import * -from .user_guides import * -from .validators import * \ No newline at end of file +from .device_handler import device_handler, disk_layouts +from .fido import Fido2 +from .filesystem import FilesystemHandler +from .subvolume_menu import SubvolumeMenu +from .partitioning_menu import ( + manual_partitioning, + PartitioningList +) +from .device_model import ( + _DeviceInfo, + BDevice, + DiskLayoutType, + DiskLayoutConfiguration, + PartitionTable, + Unit, + Size, + SubvolumeModification, + DeviceGeometry, + PartitionType, + PartitionFlag, + FilesystemType, + ModificationStatus, + PartitionModification, + DeviceModification, + EncryptionType, + DiskEncryption, + Fido2Device, + LsblkInfo, + CleanType, + get_lsblk_info, + get_all_lsblk_info, + get_lsblk_by_mountpoint +) +from .encryption_menu import ( + select_encryption_type, + select_encrypted_password, + select_hsm, + select_partitions_to_encrypt, + DiskEncryptionMenu, +) diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py deleted file mode 100644 index 178b786a..00000000 --- a/archinstall/lib/disk/blockdevice.py +++ /dev/null @@ -1,301 +0,0 @@ -from __future__ import annotations -import json -import logging -import time - -from collections import OrderedDict -from dataclasses import dataclass -from typing import Optional, Dict, Any, Iterator, List, TYPE_CHECKING - -from ..exceptions import DiskError, SysCallError -from ..output import log -from ..general import SysCommand -from ..storage import storage - - -if TYPE_CHECKING: - from .partition import Partition - _: Any - - -@dataclass -class BlockSizeInfo: - start: str - end: str - size: str - - -@dataclass -class BlockInfo: - pttype: str - ptuuid: str - size: int - tran: Optional[str] - rota: bool - free_space: Optional[List[BlockSizeInfo]] - - -class BlockDevice: - def __init__(self, path :str, info :Optional[Dict[str, Any]] = None): - if not info: - from .helpers import all_blockdevices - # If we don't give any information, we need to auto-fill it. - # Otherwise any subsequent usage will break. - self.info = all_blockdevices(partitions=False)[path].info - else: - self.info = info - - self._path = path - self.keep_partitions = True - self._block_info = self._fetch_information() - self._partitions: Dict[str, 'Partition'] = {} - - self._load_partitions() - - # TODO: Currently disk encryption is a BIT misleading. - # It's actually partition-encryption, but for future-proofing this - # I'm placing the encryption password on a BlockDevice level. - - def __repr__(self, *args :str, **kwargs :str) -> str: - return self._str_repr - - @property - def path(self) -> str: - return self._path - - @property - def _str_repr(self) -> str: - return f"BlockDevice({self._device_or_backfile}, size={self.size}GB, free_space={self._safe_free_space()}, bus_type={self.bus_type})" - - def as_json(self) -> Dict[str, Any]: - return { - str(_('Device')): self._device_or_backfile, - str(_('Size')): f'{self.size}GB', - str(_('Free space')): f'{self._safe_free_space()}', - str(_('Bus-type')): f'{self.bus_type}' - } - - def __iter__(self) -> Iterator['Partition']: - for partition in self.partitions: - yield self.partitions[partition] - - def __getitem__(self, key :str, *args :str, **kwargs :str) -> Any: - if hasattr(self, key): - return getattr(self, key) - - if self.info and key in self.info: - return self.info[key] - - raise KeyError(f'{self.info} does not contain information: "{key}"') - - def __lt__(self, left_comparitor :'BlockDevice') -> bool: - return self._path < left_comparitor.path - - def json(self) -> str: - """ - json() has precedence over __dump__, so this is a way - to give less/partial information for user readability. - """ - return self._path - - def __dump__(self) -> Dict[str, Dict[str, Any]]: - return { - self._path: { - 'partuuid': self.uuid, - 'wipe': self.info.get('wipe', None), - 'partitions': [part.__dump__() for part in self.partitions.values()] - } - } - - def _call_lsblk(self, path: str) -> Dict[str, Any]: - output = SysCommand(f'lsblk --json -b -o+SIZE,PTTYPE,ROTA,TRAN,PTUUID {self._path}').decode('UTF-8') - if output: - lsblk_info = json.loads(output) - return lsblk_info - - raise DiskError(f'Failed to read disk "{self.path}" with lsblk') - - def _load_partitions(self): - from .partition import Partition - - self._partitions.clear() - - lsblk_info = self._call_lsblk(self._path) - device = lsblk_info['blockdevices'][0] - self._partitions.clear() - - if children := device.get('children', None): - root = f'/dev/{device["name"]}' - for child in children: - part_id = child['name'].removeprefix(device['name']) - self._partitions[part_id] = Partition(root + part_id, block_device=self, part_id=part_id) - - def _get_free_space(self) -> Optional[List[BlockSizeInfo]]: - # NOTE: parted -s will default to `cancel` on prompt, skipping any partition - # that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso, - # so the free will ignore the ESP partition and just give the "free" space. - # Doesn't harm us, but worth noting in case something weird happens. - try: - output = SysCommand(f"parted -s --machine {self._path} print free").decode('utf-8') - if output: - free_lines = [line for line in output.split('\n') if 'free' in line] - sizes = [] - for free_space in free_lines: - _, start, end, size, *_ = free_space.strip('\r\n;').split(':') - sizes.append(BlockSizeInfo(start, end, size)) - - return sizes - except SysCallError as error: - log(f"Could not get free space on {self._path}: {error}", level=logging.DEBUG) - - return None - - def _fetch_information(self) -> BlockInfo: - lsblk_info = self._call_lsblk(self._path) - device = lsblk_info['blockdevices'][0] - free_space = self._get_free_space() - - return BlockInfo( - pttype=device['pttype'], - ptuuid=device['ptuuid'], - size=device['size'], - tran=device['tran'], - rota=device['rota'], - free_space=free_space - ) - - @property - def _device_or_backfile(self) -> Optional[str]: - """ - Returns the actual device-endpoint of the BlockDevice. - If it's a loop-back-device it returns the back-file, - For other types it return self.device - """ - if self.info.get('type') == 'loop': - return self.info['back-file'] - else: - return self.device - - @property - def mountpoint(self) -> None: - """ - A dummy function to enable transparent comparisons of mountpoints. - As blockdevices can't be mounted directly, this will always be None - """ - return None - - @property - def device(self) -> Optional[str]: - """ - Returns the device file of the BlockDevice. - If it's a loop-back-device it returns the /dev/X device, - If it's a ATA-drive it returns the /dev/X device - And if it's a crypto-device it returns the parent device - """ - if "DEVTYPE" not in self.info: - raise DiskError(f'Could not locate backplane info for "{self._path}"') - - if self.info['DEVTYPE'] in ['disk','loop']: - return self._path - elif self.info['DEVTYPE'][:4] == 'raid': - # This should catch /dev/md## raid devices - return self._path - elif self.info['DEVTYPE'] == 'crypt': - if 'pkname' not in self.info: - raise DiskError(f'A crypt device ({self._path}) without a parent kernel device name.') - return f"/dev/{self.info['pkname']}" - else: - log(f"Unknown blockdevice type for {self._path}: {self.info['DEVTYPE']}", level=logging.DEBUG) - - return None - - @property - def partition_type(self) -> str: - return self._block_info.pttype - - @property - def uuid(self) -> str: - return self._block_info.ptuuid - - @property - def size(self) -> float: - from .helpers import convert_size_to_gb - return convert_size_to_gb(self._block_info.size) - - @property - def bus_type(self) -> Optional[str]: - return self._block_info.tran - - @property - def spinning(self) -> bool: - return self._block_info.rota - - @property - def partitions(self) -> Dict[str, 'Partition']: - return OrderedDict(sorted(self._partitions.items())) - - @property - def partition(self) -> List['Partition']: - return list(self.partitions.values()) - - @property - def first_free_sector(self) -> str: - if block_size := self._largest_free_space(): - return block_size.start - else: - return '512MB' - - @property - def first_end_sector(self) -> str: - if block_size := self._largest_free_space(): - return block_size.end - else: - return f"{self.size}GB" - - def _safe_free_space(self) -> str: - if self._block_info.free_space: - sizes = [free_space.size for free_space in self._block_info.free_space] - return '+'.join(sizes) - return '?' - - def _largest_free_space(self) -> Optional[BlockSizeInfo]: - if self._block_info.free_space: - sorted_sizes = sorted(self._block_info.free_space, key=lambda x: x.size, reverse=True) - return sorted_sizes[0] - return None - - def _partprobe(self) -> bool: - return SysCommand(['partprobe', self._path]).exit_code == 0 - - def flush_cache(self) -> None: - self._load_partitions() - - def get_partition(self, uuid :Optional[str] = None, partuuid :Optional[str] = None) -> Partition: - if not uuid and not partuuid: - raise ValueError(f"BlockDevice.get_partition() requires either a UUID or a PARTUUID for lookups.") - - log(f"Retrieving partition PARTUUID={partuuid} or UUID={uuid}", level=logging.DEBUG, fg="gray") - - for count in range(storage.get('DISK_RETRY_ATTEMPTS', 5)): - for partition_index, partition in self.partitions.items(): - try: - if uuid and partition.uuid and partition.uuid.lower() == uuid.lower(): - log(f"Matched UUID={uuid} against {partition.uuid}", level=logging.DEBUG, fg="gray") - return partition - elif partuuid and partition.part_uuid and partition.part_uuid.lower() == partuuid.lower(): - log(f"Matched PARTUUID={partuuid} against {partition.part_uuid}", level=logging.DEBUG, fg="gray") - return partition - except DiskError as error: - # Most likely a blockdevice that doesn't support or use UUID's - # (like Microsoft recovery partition) - log(f"Could not get UUID/PARTUUID of {partition}: {error}", level=logging.DEBUG, fg="gray") - pass - - log(f"uuid {uuid} or {partuuid} not found. Waiting {storage.get('DISK_TIMEOUTS', 1) * count}s for next attempt",level=logging.DEBUG) - self.flush_cache() - time.sleep(storage.get('DISK_TIMEOUTS', 1) * count) - - log(f"Could not find {uuid}/{partuuid} in disk after 5 retries", level=logging.INFO) - log(f"Cache: {self._partitions}") - log(f"Partitions: {self.partitions.items()}") - raise DiskError(f"Partition {uuid}/{partuuid} was never found on {self} despite several attempts.") diff --git a/archinstall/lib/disk/btrfs/__init__.py b/archinstall/lib/disk/btrfs/__init__.py deleted file mode 100644 index a26e0160..00000000 --- a/archinstall/lib/disk/btrfs/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations -import pathlib -import glob -import logging -from typing import Union, Dict, TYPE_CHECKING - -# https://stackoverflow.com/a/39757388/929999 -if TYPE_CHECKING: - from ...installer import Installer - -from .btrfs_helpers import ( - subvolume_info_from_path as subvolume_info_from_path, - find_parent_subvolume as find_parent_subvolume, - setup_subvolumes as setup_subvolumes, - mount_subvolume as mount_subvolume -) -from .btrfssubvolumeinfo import BtrfsSubvolumeInfo as BtrfsSubvolume -from .btrfspartition import BTRFSPartition as BTRFSPartition - -from ...exceptions import DiskError, Deprecated -from ...general import SysCommand -from ...output import log - - -def create_subvolume(installation: Installer, subvolume_location :Union[pathlib.Path, str]) -> bool: - """ - This function uses btrfs to create a subvolume. - - @installation: archinstall.Installer instance - @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot - """ - - installation_mountpoint = installation.target - if type(installation_mountpoint) == str: - installation_mountpoint = pathlib.Path(installation_mountpoint) - # Set up the required physical structure - if type(subvolume_location) == str: - subvolume_location = pathlib.Path(subvolume_location) - - target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) - - # Difference from mount_subvolume: - # We only check if the parent exists, since we'll run in to "target path already exists" otherwise - if not target.parent.exists(): - target.parent.mkdir(parents=True) - - if glob.glob(str(target / '*')): - raise DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)") - - # Remove the target if it exists - if target.exists(): - target.rmdir() - - log(f"Creating a subvolume on {target}", level=logging.INFO) - if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: - raise DiskError(f"Could not create a subvolume at {target}: {cmd}") diff --git a/archinstall/lib/disk/btrfs/btrfs_helpers.py b/archinstall/lib/disk/btrfs/btrfs_helpers.py deleted file mode 100644 index f6d2734a..00000000 --- a/archinstall/lib/disk/btrfs/btrfs_helpers.py +++ /dev/null @@ -1,136 +0,0 @@ -import logging -import re -from pathlib import Path -from typing import Optional, Dict, Any, TYPE_CHECKING - -from ...models.subvolume import Subvolume -from ...exceptions import SysCallError, DiskError -from ...general import SysCommand -from ...output import log -from ...plugins import plugins -from ..helpers import get_mount_info -from .btrfssubvolumeinfo import BtrfsSubvolumeInfo - -if TYPE_CHECKING: - from .btrfspartition import BTRFSPartition - from ...installer import Installer - - -class fstab_btrfs_compression_plugin(): - def __init__(self, partition_dict): - self.partition_dict = partition_dict - - def on_genfstab(self, installation): - with open(f"{installation.target}/etc/fstab", 'r') as fh: - fstab = fh.read() - - # Replace the {installation}/etc/fstab with entries - # using the compress=zstd where the mountpoint has compression set. - with open(f"{installation.target}/etc/fstab", 'w') as fh: - for line in fstab.split('\n'): - # So first we grab the mount options by using subvol=.*? as a locator. - # And we also grab the mountpoint for the entry, for instance /var/log - if (subvoldef := re.findall(',.*?subvol=.*?[\t ]', line)) and (mountpoint := re.findall('[\t ]/.*?[\t ]', line)): - for subvolume in self.partition_dict.get('btrfs', {}).get('subvolumes', []): - # We then locate the correct subvolume and check if it's compressed - if subvolume.compress and subvolume.mountpoint == mountpoint[0].strip(): - # We then sneak in the compress=zstd option if it doesn't already exist: - # We skip entries where compression is already defined - if ',compress=zstd,' not in line: - line = line.replace(subvoldef[0], f",compress=zstd{subvoldef[0]}") - break - - fh.write(f"{line}\n") - - return True - - -def mount_subvolume(installation: 'Installer', device: 'BTRFSPartition', subvolume: Subvolume): - # we normalize the subvolume name (getting rid of slash at the start if exists. - # In our implementation has no semantic load. - # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = subvolume.name.lstrip('/') - mountpoint = Path(subvolume.mountpoint) - installation_target = Path(installation.target) - - mountpoint = installation_target / mountpoint.relative_to(mountpoint.anchor) - mountpoint.mkdir(parents=True, exist_ok=True) - mount_options = subvolume.options + [f'subvol={name}'] - - log(f"Mounting subvolume {name} on {device} to {mountpoint}", level=logging.INFO, fg="gray") - SysCommand(f"mount {device.path} {mountpoint} -o {','.join(mount_options)}") - - -def setup_subvolumes(installation: 'Installer', partition_dict: Dict[str, Any]): - log(f"Setting up subvolumes: {partition_dict['btrfs']['subvolumes']}", level=logging.INFO, fg="gray") - - for subvolume in partition_dict['btrfs']['subvolumes']: - # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. - # Every subvolume is created from the top of the hierarchy- and simplifies its further use - name = subvolume.name.lstrip('/') - - # We create the subvolume using the BTRFSPartition instance. - # That way we ensure not only easy access, but also accurate mount locations etc. - partition_dict['device_instance'].create_subvolume(name, installation=installation) - - # Make the nodatacow processing now - # It will be the main cause of creation of subvolumes which are not to be mounted - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - if subvolume.nodatacow: - if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") - - # Make the compress processing now - # it is not an options which can be established by subvolume (but for whole file systems), and can be - # set up via a simple attribute change in a directory (if empty). And here the directories are brand new - # in this way only zstd compression is activaded - # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated - - if subvolume.compress: - if not any(['compress' in filesystem_option for filesystem_option in partition_dict.get('filesystem', {}).get('mount_options', [])]): - if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: - raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") - - if 'fstab_btrfs_compression_plugin' not in plugins: - plugins['fstab_btrfs_compression_plugin'] = fstab_btrfs_compression_plugin(partition_dict) - - -def subvolume_info_from_path(path: Path) -> Optional[BtrfsSubvolumeInfo]: - try: - subvolume_name = '' - result = {} - for index, line in enumerate(SysCommand(f"btrfs subvolume show {path}")): - if index == 0: - subvolume_name = line.strip().decode('UTF-8') - continue - - if b':' in line: - key, value = line.strip().decode('UTF-8').split(':', 1) - - # A bit of a hack, until I figure out how @dataclass - # allows for hooking in a pre-processor to do this we have to do it here: - result[key.lower().replace(' ', '_').replace('(s)', 's')] = value.strip() - - return BtrfsSubvolumeInfo(**{'full_path' : path, 'name' : subvolume_name, **result}) # type: ignore - except SysCallError as error: - log(f"Could not retrieve subvolume information from {path}: {error}", level=logging.WARNING, fg="orange") - - return None - - -def find_parent_subvolume(path: Path, filters=[]) -> Optional[BtrfsSubvolumeInfo]: - # A root path cannot have a parent - if str(path) == '/': - return None - - if found_mount := get_mount_info(str(path.parent), traverse=True, ignore=filters): - if not (subvolume := subvolume_info_from_path(found_mount['target'])): - if found_mount['target'] == '/': - return None - - return find_parent_subvolume(path.parent, filters=[*filters, found_mount['target']]) - - return subvolume - - return None diff --git a/archinstall/lib/disk/btrfs/btrfspartition.py b/archinstall/lib/disk/btrfs/btrfspartition.py deleted file mode 100644 index d04c9b98..00000000 --- a/archinstall/lib/disk/btrfs/btrfspartition.py +++ /dev/null @@ -1,109 +0,0 @@ -import glob -import pathlib -import logging -from typing import Optional, TYPE_CHECKING - -from ...exceptions import DiskError -from ...storage import storage -from ...output import log -from ...general import SysCommand -from ..partition import Partition -from ..helpers import findmnt -from .btrfs_helpers import ( - subvolume_info_from_path -) - -if TYPE_CHECKING: - from ...installer import Installer - from .btrfssubvolumeinfo import BtrfsSubvolumeInfo - - -class BTRFSPartition(Partition): - def __init__(self, *args, **kwargs): - Partition.__init__(self, *args, **kwargs) - - @property - def subvolumes(self): - for filesystem in findmnt(pathlib.Path(self.path), recurse=True).get('filesystems', []): - if '[' in filesystem.get('source', ''): - yield subvolume_info_from_path(filesystem['target']) - - def iterate_children(struct): - for c in struct.get('children', []): - if '[' in child.get('source', ''): - yield subvolume_info_from_path(c['target']) - - for sub_child in iterate_children(c): - yield sub_child - - for child in iterate_children(filesystem): - yield child - - def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolumeInfo': - """ - Subvolumes have to be created within a mountpoint. - This means we need to get the current installation target. - After we get it, we need to verify it is a btrfs subvolume filesystem. - Finally, the destination must be empty. - """ - - # Allow users to override the installation session - if not installation: - installation = storage.get('installation_session') - - # Determain if the path given, is an absolute path or a relative path. - # We do this by checking if the path contains a known mountpoint. - if str(subvolume)[0] == '/': - if filesystems := findmnt(subvolume, traverse=True).get('filesystems'): - if (target := filesystems[0].get('target')) and target != '/' and str(subvolume).startswith(target): - # Path starts with a known mountpoint which isn't / - # Which means it's an absolute path to a mounted location. - pass - else: - # Since it's not an absolute position with a known start. - # We omit the anchor ('/' basically) and make sure it's appendable - # to the installation.target later - subvolume = subvolume.relative_to(subvolume.anchor) - # else: We don't need to do anything about relative paths, they should be appendable to installation.target as-is. - - # If the subvolume is not absolute, then we do two checks: - # 1. Check if the partition itself is mounted somewhere, and use that as a root - # 2. Use an active Installer().target as the root, assuming it's filesystem is btrfs - # If both above fail, we need to warn the user that such setup is not supported. - if str(subvolume)[0] != '/': - if self.mountpoint is None and installation is None: - raise DiskError("When creating a subvolume on BTRFSPartition()'s, you need to either initiate a archinstall.Installer() or give absolute paths when creating the subvoulme.") - elif self.mountpoint: - subvolume = self.mountpoint / subvolume - elif installation: - ongoing_installation_destination = installation.target - if type(ongoing_installation_destination) == str: - ongoing_installation_destination = pathlib.Path(ongoing_installation_destination) - - subvolume = ongoing_installation_destination / subvolume - - subvolume.parent.mkdir(parents=True, exist_ok=True) - - # - - log(f'Attempting to create subvolume at {subvolume}', level=logging.DEBUG, fg="grey") - - if glob.glob(str(subvolume / '*')): - raise DiskError(f"Cannot create subvolume at {subvolume} because it contains data (non-empty folder target is not supported by BTRFS)") - # Ideally we would like to check if the destination is already a subvolume. - # But then we would need the mount-point at this stage as well. - # So we'll comment out this check: - # elif subvolinfo := subvolume_info_from_path(subvolume): - # raise DiskError(f"Destination {subvolume} is already a subvolume: {subvolinfo}") - - # And deal with it here: - SysCommand(f"btrfs subvolume create {subvolume}") - - return subvolume_info_from_path(subvolume) diff --git a/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py b/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py deleted file mode 100644 index 5f5bdea6..00000000 --- a/archinstall/lib/disk/btrfs/btrfssubvolumeinfo.py +++ /dev/null @@ -1,192 +0,0 @@ -import pathlib -import datetime -import logging -import string -import random -import shutil -from dataclasses import dataclass -from typing import Optional, List# , TYPE_CHECKING -from functools import cached_property - -# if TYPE_CHECKING: -# from ..blockdevice import BlockDevice - -from ...exceptions import DiskError -from ...general import SysCommand -from ...output import log -from ...storage import storage - - -@dataclass -class BtrfsSubvolumeInfo: - full_path :pathlib.Path - name :str - uuid :str - parent_uuid :str - creation_time :datetime.datetime - subvolume_id :int - generation :int - gen_at_creation :int - parent_id :int - top_level_id :int - send_transid :int - send_time :datetime.datetime - receive_transid :int - received_uuid :Optional[str] = None - flags :Optional[str] = None - receive_time :Optional[datetime.datetime] = None - snapshots :Optional[List] = None - - def __post_init__(self): - self.full_path = pathlib.Path(self.full_path) - - # Convert "-" entries to `None` - if self.parent_uuid == "-": - self.parent_uuid = None - if self.received_uuid == "-": - self.received_uuid = None - if self.flags == "-": - self.flags = None - if self.receive_time == "-": - self.receive_time = None - if self.snapshots == "": - self.snapshots = [] - - # Convert timestamps into datetime workable objects (and preserve timezone by using ISO formats) - self.creation_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.creation_time)) - self.send_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.send_time)) - if self.receive_time: - self.receive_time = datetime.datetime.fromisoformat(self.convert_to_ISO_format(self.receive_time)) - - @property - def parent_subvolume(self): - from .btrfs_helpers import find_parent_subvolume - - return find_parent_subvolume(self.full_path) - - @property - def root(self) -> bool: - from .btrfs_helpers import subvolume_info_from_path - - # TODO: Make this function traverse storage['MOUNT_POINT'] and find the first - # occurrence of a mountpoint that is a btrfs volume instead of lazy assume / is a subvolume. - # It would also be nice if it could use findmnt(self.full_path) and traverse backwards - # finding the last occurrence of a subvolume which 'self' belongs to. - if volume := subvolume_info_from_path(storage['MOUNT_POINT']): - return self.full_path == volume.full_path - - return False - - @cached_property - def partition(self): - from ..helpers import findmnt, get_parent_of_partition, all_blockdevices - from ..partition import Partition - from ..blockdevice import BlockDevice - from ..mapperdev import MapperDev - from .btrfspartition import BTRFSPartition - from .btrfs_helpers import subvolume_info_from_path - - try: - # If the subvolume is mounted, it's pretty trivial to lookup the partition (parent) device. - if filesystem := findmnt(self.full_path).get('filesystems', []): - if source := filesystem[0].get('source', None): - # Strip away subvolume definitions from findmnt - if '[' in source: - source = source[:source.find('[')] - - if filesystem[0].get('fstype', '') == 'btrfs': - return BTRFSPartition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - elif filesystem[0].get('source', '').startswith('/dev/mapper'): - return MapperDev(source) - else: - return Partition(source, BlockDevice(get_parent_of_partition(pathlib.Path(source)))) - except DiskError: - # Subvolume has never been mounted, we have no reliable way of finding where it is. - # But we have the UUID of the partition, and can begin looking for it by mounting - # all blockdevices that we can reliably support.. This is taxing tho and won't cover all devices. - - log(f"Looking up {self}, this might take time.", fg="orange", level=logging.WARNING) - for blockdevice, instance in all_blockdevices(mappers=True, partitions=True, error=True).items(): - if type(instance) in (Partition, MapperDev): - we_mounted_it = False - detection_mountpoint = instance.mountpoint - if not detection_mountpoint: - if type(instance) == Partition and instance.encrypted: - # TODO: Perhaps support unlocking encrypted volumes? - # This will cause a lot of potential user interactions tho. - log(f"Ignoring {blockdevice} because it's encrypted.", fg="gray", level=logging.DEBUG) - continue - - detection_mountpoint = pathlib.Path(f"/tmp/{''.join([random.choice(string.ascii_letters) for x in range(20)])}") - detection_mountpoint.mkdir(parents=True, exist_ok=True) - - instance.mount(str(detection_mountpoint)) - we_mounted_it = True - - if (filesystem := findmnt(detection_mountpoint)) and (filesystem := filesystem.get('filesystems', [])): - if subvolume := subvolume_info_from_path(filesystem[0]['target']): - if subvolume.uuid == self.uuid: - # The top level subvolume matched of ourselves, - # which means the instance we're iterating has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - def iterate_children(struct): - for child in struct.get('children', []): - if '[' in child.get('source', ''): - yield subvolume_info_from_path(child['target']) - - for sub_child in iterate_children(child): - yield sub_child - - for child in iterate_children(filesystem[0]): - if child.uuid == self.uuid: - # We found a child within the instance that has the subvol we're looking for. - log(f"Found the subvolume on device {instance}", level=logging.DEBUG, fg="gray") - return instance - - if we_mounted_it: - instance.unmount() - shutil.rmtree(detection_mountpoint) - - @cached_property - def mount_options(self) -> Optional[List[str]]: - from ..helpers import findmnt - - if filesystem := findmnt(self.full_path).get('filesystems', []): - return filesystem[0].get('options').split(',') - - def convert_to_ISO_format(self, time_string): - time_string_almost_done = time_string.replace(' ', 'T', 1).replace(' ', '') - iso_string = f"{time_string_almost_done[:-2]}:{time_string_almost_done[-2:]}" - return iso_string - - def mount(self, mountpoint :pathlib.Path, options=None, include_previously_known_options=True): - from ..helpers import findmnt - - try: - if mnt_info := findmnt(pathlib.Path(mountpoint), traverse=False): - log(f"Unmounting {mountpoint} as it was already mounted using {mnt_info}") - SysCommand(f"umount {mountpoint}") - except DiskError: - # No previously mounted device at the mountpoint - pass - - if not options: - options = [] - - try: - if include_previously_known_options and (cached_options := self.mount_options): - options += cached_options - except DiskError: - pass - - if not any('subvol=' in x for x in options): - options += f'subvol={self.name}' - - SysCommand(f"mount {self.partition.path} {mountpoint} -o {','.join(options)}") - log(f"{self} has successfully been mounted to {mountpoint}", level=logging.INFO, fg="gray") - - def unmount(self, recurse :bool = True): - SysCommand(f"umount {'-R' if recurse else ''} {self.full_path}") - log(f"Successfully unmounted {self}", level=logging.INFO, fg="gray") diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py new file mode 100644 index 00000000..12cf18ea --- /dev/null +++ b/archinstall/lib/disk/device_handler.py @@ -0,0 +1,599 @@ +from __future__ import annotations + +import json +import logging +import os +import time +from pathlib import Path +from typing import List, Dict, Any, Optional, TYPE_CHECKING + +from parted import ( # type: ignore + Disk, Geometry, FileSystem, + PartitionException, DiskLabelException, + getAllDevices, freshDisk, Partition, +) + +from .device_model import ( + DeviceModification, PartitionModification, + BDevice, _DeviceInfo, _PartitionInfo, + FilesystemType, Unit, PartitionTable, + ModificationStatus, get_lsblk_info, LsblkInfo, + _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption +) + +from ..exceptions import DiskError, UnknownFilesystemFormat +from ..general import SysCommand, SysCallError, JSON +from ..luks import Luks2 +from ..output import log +from ..utils.util import is_subpath + +if TYPE_CHECKING: + _: Any + + +class DeviceHandler(object): + _TMP_BTRFS_MOUNT = Path('/mnt/arch_btrfs') + + def __init__(self): + self._devices: Dict[Path, BDevice] = {} + self.load_devices() + + @property + def devices(self) -> List[BDevice]: + return list(self._devices.values()) + + def load_devices(self): + block_devices = {} + + for device in getAllDevices(): + try: + disk = Disk(device) + except DiskLabelException as error: + if 'unrecognised disk label' in getattr(error, 'message', str(error)): + disk = freshDisk(device, PartitionTable.GPT.value) + else: + log(f'Unable to get disk from device: {device}', level=logging.DEBUG) + continue + + device_info = _DeviceInfo.from_disk(disk) + partition_infos = [] + + for partition in disk.partitions: + lsblk_info = get_lsblk_info(partition.path) + fs_type = self._determine_fs_type(partition, lsblk_info) + subvol_infos = [] + + if fs_type == FilesystemType.Btrfs: + subvol_infos = self.get_btrfs_info(partition.path) + + partition_infos.append( + _PartitionInfo.from_partition( + partition, + fs_type, + lsblk_info.partuuid, + lsblk_info.mountpoints, + subvol_infos + ) + ) + + block_device = BDevice(disk, device_info, partition_infos) + block_devices[block_device.device_info.path] = block_device + + self._devices = block_devices + + def _determine_fs_type( + self, + partition: Partition, + lsblk_info: Optional[LsblkInfo] = None + ) -> Optional[FilesystemType]: + try: + if partition.fileSystem: + return FilesystemType(partition.fileSystem.type) + elif lsblk_info is not None: + return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None + return None + except ValueError: + log(f'Could not determine the filesystem: {partition.fileSystem}', level=logging.DEBUG) + + return None + + def get_device(self, path: Path) -> Optional[BDevice]: + return self._devices.get(path, None) + + def get_device_by_partition_path(self, partition_path: Path) -> Optional[BDevice]: + partition = self.find_partition(partition_path) + if partition: + return partition.disk.device + return None + + def find_partition(self, path: Path) -> Optional[_PartitionInfo]: + for device in self._devices.values(): + part = next(filter(lambda x: str(x.path) == str(path), device.partition_infos), None) + if part is not None: + return part + return None + + def get_uuid_for_path(self, path: Path) -> Optional[str]: + partition = self.find_partition(path) + return partition.partuuid if partition else None + + def get_btrfs_info(self, dev_path: Path) -> List[_BtrfsSubvolumeInfo]: + lsblk_info = get_lsblk_info(dev_path) + subvol_infos: List[_BtrfsSubvolumeInfo] = [] + + if not lsblk_info.mountpoint: + self.mount(dev_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 + # "mountpoint": "/mnt/archinstall/.snapshots" + # "mountpoints": ["/mnt/archinstall/.snapshots", "/mnt/archinstall/home", ..] + # so we'll determine the minimum common path and assume that's the root + path_strings = [str(m) for m in lsblk_info.mountpoints] + common_prefix = os.path.commonprefix(path_strings) + mountpoint = Path(common_prefix) + + try: + result = SysCommand(f'btrfs subvolume list {mountpoint}') + except SysCallError as err: + log(f'Failed to read btrfs subvolume information: {err}', level=logging.DEBUG) + return subvol_infos + + if result.exit_code == 0: + try: + if decoded := result.decode('utf-8'): + # ID 256 gen 16 top level 5 path @ + for line in decoded.splitlines(): + # expected output format: + # ID 257 gen 8 top level 5 path @home + name = Path(line.split(' ')[-1]) + sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) + subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) + except json.decoder.JSONDecodeError as err: + log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + raise err + + if not lsblk_info.mountpoint: + self.umount(dev_path) + + return subvol_infos + + def _perform_formatting( + self, + fs_type: FilesystemType, + path: Path, + additional_parted_options: List[str] = [] + ): + options = [] + command = '' + + match fs_type: + case FilesystemType.Btrfs: + options += ['-f'] + command += 'mkfs.btrfs' + case FilesystemType.Fat16: + options += ['-F16'] + command += 'mkfs.fat' + case FilesystemType.Fat32: + options += ['-F32'] + command += 'mkfs.fat' + case FilesystemType.Ext2: + options += ['-F'] + command += 'mkfs.ext2' + case FilesystemType.Ext3: + options += ['-F'] + command += 'mkfs.ext3' + case FilesystemType.Ext4: + options += ['-F'] + command += 'mkfs.ext4' + case FilesystemType.Xfs: + options += ['-f'] + command += 'mkfs.xfs' + case FilesystemType.F2fs: + options += ['-f'] + command += 'mkfs.f2fs' + case FilesystemType.Ntfs: + options += ['-f', '-Q'] + command += 'mkfs.ntfs' + case FilesystemType.Reiserfs: + command += 'mkfs.reiserfs' + case _: + raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported') + + options += additional_parted_options + options_str = ' '.join(options) + + log(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + + try: + if (handle := SysCommand(f"/usr/bin/{command} {options_str} {path}")).exit_code != 0: + mkfs_error = handle.decode() + raise DiskError(f'Could not format {path} with {fs_type.value}: {mkfs_error}') + except SysCallError as error: + msg = f'Could not format {path} with {fs_type.value}: {error.message}' + log(msg, fg='red') + raise DiskError(msg) from error + + def _perform_enc_formatting( + self, + dev_path: Path, + mapper_name: Optional[str], + fs_type: FilesystemType, + enc_conf: DiskEncryption + ): + luks_handler = Luks2( + dev_path, + mapper_name=mapper_name, + password=enc_conf.encryption_password + ) + + key_file = luks_handler.encrypt() + + log(f'Unlocking luks2 device: {dev_path}', level=logging.DEBUG) + luks_handler.unlock(key_file=key_file) + + if not luks_handler.mapper_dev: + raise DiskError('Failed to unlock luks device') + + log(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}', level=logging.INFO) + self._perform_formatting(fs_type, luks_handler.mapper_dev) + + log(f'luks2 locking device: {dev_path}', level=logging.INFO) + luks_handler.lock() + + def format( + self, + modification: DeviceModification, + enc_conf: Optional['DiskEncryption'] = None + ): + """ + Format can be given an overriding path, for instance /dev/null to test + the formatting functionality and in essence the support for the given filesystem. + """ + + # verify that all partitions have a path set (which implies that they have been created) + missing_path = next(filter(lambda x: x.dev_path is None, modification.partitions), None) + if missing_path is not None: + raise ValueError('When formatting, all partitions must have a path set') + + # crypto luks is not known to parted and can therefore not + # be used as a filesystem type in that sense; + invalid_fs_type = next(filter(lambda x: x.fs_type is FilesystemType.Crypto_luks, modification.partitions), None) + if invalid_fs_type is not None: + raise ValueError('Crypto luks cannot be set as a filesystem type') + + # make sure all devices are unmounted + self._umount_all_existing(modification) + + for part_mod in modification.partitions: + # partition will be encrypted + if enc_conf is not None and part_mod in enc_conf.partitions: + self._perform_enc_formatting( + part_mod.real_dev_path, + part_mod.mapper_name, + part_mod.fs_type, + enc_conf + ) + else: + self._perform_formatting(part_mod.fs_type, part_mod.real_dev_path) + + def _perform_partitioning( + self, + part_mod: PartitionModification, + block_device: BDevice, + disk: Disk, + requires_delete: bool + ): + # when we require a delete and the partition to be (re)created + # already exists then we have to delete it first + if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: + log(f'Delete existing partition: {part_mod.real_dev_path}', level=logging.INFO) + part_info = self.find_partition(part_mod.real_dev_path) + + if not part_info: + raise DiskError(f'No partition for dev path found: {part_mod.real_dev_path}') + + disk.deletePartition(part_info.partition) + disk.commit() + + if part_mod.status == ModificationStatus.Delete: + return + + start_sector = part_mod.start.convert( + Unit.sectors, + block_device.device_info.sector_size + ) + + length_sector = part_mod.length.convert( + Unit.sectors, + block_device.device_info.sector_size + ) + + geometry = Geometry( + device=block_device.disk.device, + start=start_sector.value, + length=length_sector.value + ) + + filesystem = FileSystem(type=part_mod.fs_type.value, geometry=geometry) + + partition = Partition( + disk=disk, + type=part_mod.type.get_partition_code(), + fs=filesystem, + geometry=geometry + ) + + for flag in part_mod.flags: + partition.setFlag(flag.value) + + log(f'\tType: {part_mod.type.value}', level=logging.DEBUG) + log(f'\tFilesystem: {part_mod.fs_type.value}', level=logging.DEBUG) + log(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length', level=logging.DEBUG) + + try: + disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint) + disk.commit() + + # the creation will take a bit of time + time.sleep(3) + + # the partition has a real path now as it was created + part_mod.dev_path = Path(partition.path) + + info = get_lsblk_info(part_mod.dev_path) + + if not info.partuuid: + raise DiskError(f'Unable to determine new partition uuid: {part_mod.dev_path}') + + part_mod.partuuid = info.partuuid + part_mod.uuid = info.uuid + except PartitionException as ex: + raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex + + def create_btrfs_volumes( + self, + part_mod: PartitionModification, + enc_conf: Optional['DiskEncryption'] = None + ): + log(f'Creating subvolumes: {part_mod.real_dev_path}', level=logging.INFO) + + luks_handler = None + + # unlock the partition first if it's encrypted + if enc_conf is not None and part_mod in enc_conf.partitions: + if not part_mod.mapper_name: + raise ValueError('No device path specified for modification') + + luks_handler = self.unlock_luks2_dev( + part_mod.real_dev_path, + part_mod.mapper_name, + enc_conf.encryption_password + ) + + if not luks_handler.mapper_dev: + raise DiskError('Failed to unlock luks device') + + self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + else: + self.mount(part_mod.real_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + + for sub_vol in part_mod.btrfs_subvols: + log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG) + + if luks_handler is not None: + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + else: + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + + SysCommand(f"btrfs subvolume create {subvol_path}") + + if sub_vol.nodatacow: + if (result := SysCommand(f'chattr +C {subvol_path}')).exit_code != 0: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {result.decode()}') + + if sub_vol.compress: + if (result := SysCommand(f'chattr +c {subvol_path}')).exit_code != 0: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {result}') + + if luks_handler is not None and luks_handler.mapper_dev is not None: + self.umount(luks_handler.mapper_dev) + luks_handler.lock() + else: + self.umount(part_mod.real_dev_path) + + def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2: + luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password) + + if not luks_handler.is_unlocked(): + luks_handler.unlock() + + if not luks_handler.is_unlocked(): + raise DiskError(f'Failed to unlock luks2 device: {dev_path}') + + return luks_handler + + def _umount_all_existing(self, modification: DeviceModification): + log(f'Unmounting all partitions: {modification.device_path}', level=logging.INFO) + + existing_partitions = self._devices[modification.device_path].partition_infos + + for partition in existing_partitions: + log(f'Unmounting: {partition.path}', level=logging.DEBUG) + + # un-mount for existing encrypted partitions + if partition.fs_type == FilesystemType.Crypto_luks: + Luks2(partition.path).lock() + else: + self.umount(partition.path, recursive=True) + + def partition( + self, + modification: DeviceModification, + partition_table: Optional[PartitionTable] = None + ): + """ + Create a partition table on the block device and create all partitions. + """ + if modification.wipe: + if partition_table is None: + raise ValueError('Modification is marked as wipe but no partitioning table was provided') + + if partition_table.MBR and len(modification.partitions) > 3: + raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions') + + # make sure all devices are unmounted + self._umount_all_existing(modification) + + # WARNING: the entire device will be wiped and all data lost + if modification.wipe: + self.wipe_dev(modification.device) + part_table = partition_table.value if partition_table else None + disk = freshDisk(modification.device.disk.device, part_table) + else: + log(f'Use existing device: {modification.device_path}') + disk = modification.device.disk + + log(f'Creating partitions: {modification.device_path}') + + # TODO sort by delete first + + for part_mod in modification.partitions: + # don't touch existing partitions + if part_mod.exists(): + continue + + # if the entire disk got nuked then we don't have to delete + # any existing partitions anymore because they're all gone already + requires_delete = modification.wipe is False + self._perform_partitioning(part_mod, modification.device, disk, requires_delete=requires_delete) + + self.partprobe(modification.device.device_info.path) + + def mount( + self, + dev_path: Path, + target_mountpoint: Path, + mount_fs: Optional[str] = None, + create_target_mountpoint: bool = True, + options: List[str] = [] + ): + if create_target_mountpoint and not target_mountpoint.exists(): + target_mountpoint.mkdir(parents=True, exist_ok=True) + + if not target_mountpoint.exists(): + raise ValueError('Target mountpoint does not exist') + + lsblk_info = get_lsblk_info(dev_path) + if target_mountpoint in lsblk_info.mountpoints: + log(f'Device already mounted at {target_mountpoint}') + return + + str_options = ','.join(options) + str_options = f'-o {str_options}' if str_options else '' + + mount_fs = f'-t {mount_fs}' if mount_fs else '' + + command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' + + log(f'Mounting {dev_path}: command', level=logging.DEBUG) + + try: + result = SysCommand(command) + if result.exit_code != 0: + raise DiskError(f'Could not mount {dev_path}: {command}\n{result.decode()}') + except SysCallError as err: + raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}') + + def umount(self, mountpoint: Path, recursive: bool = False): + try: + lsblk_info = get_lsblk_info(mountpoint) + except SysCallError as ex: + # this could happen if before partitioning the device contained 3 partitions + # and after partitioning only 2 partitions were created, then the modifications object + # will have a reference to /dev/sX3 which is being tried to umount here now + if 'not a block device' in ex.message: + return + raise ex + + if len(lsblk_info.mountpoints) > 0: + log(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}', level=logging.DEBUG) + + for mountpoint in lsblk_info.mountpoints: + log(f'Unmounting mountpoint: {mountpoint}', level=logging.DEBUG) + + command = 'umount' + + if recursive: + command += ' -R' + + SysCommand(f'{command} {mountpoint}') + + def detect_pre_mounted_mods(self, base_mountpoint: Path) -> List[DeviceModification]: + part_mods: Dict[Path, List[PartitionModification]] = {} + + for device in self.devices: + for part_info in device.partition_infos: + for mountpoint in part_info.mountpoints: + if is_subpath(mountpoint, base_mountpoint): + path = Path(part_info.disk.device.path) + part_mods.setdefault(path, []) + part_mods[path].append(PartitionModification.from_existing_partition(part_info)) + break + + device_mods: List[DeviceModification] = [] + for device_path, mods in part_mods.items(): + device_mod = DeviceModification(self._devices[device_path], False, mods) + device_mods.append(device_mod) + + return device_mods + + def partprobe(self, path: Optional[Path] = None): + if path is not None: + command = f'partprobe {path}' + else: + command = 'partprobe' + + try: + result = SysCommand(command) + if result.exit_code != 0: + log(f'Error calling partprobe: {result.decode()}', level=logging.DEBUG) + raise DiskError(f'Could not perform partprobe on {path}: {result.decode()}') + except SysCallError as error: + log(f"partprobe experienced an error with {path}: {error}", level=logging.DEBUG) + + def _wipe(self, dev_path: Path): + """ + Wipe a device (partition or otherwise) of meta-data, be it file system, LVM, etc. + @param dev_path: Device path of the partition to be wiped. + @type dev_path: str + """ + with open(dev_path, 'wb') as p: + p.write(bytearray(1024)) + + def wipe_dev(self, block_device: BDevice): + """ + Wipe the block device of meta-data, be it file system, LVM, etc. + This is not intended to be secure, but rather to ensure that + auto-discovery tools don't recognize anything here. + """ + log(f'Wiping partitions and metadata: {block_device.device_info.path}') + for partition in block_device.partition_infos: + self._wipe(partition.path) + + self._wipe(block_device.device_info.path) + + +device_handler = DeviceHandler() + + +def disk_layouts() -> str: + try: + lsblk_info = get_all_lsblk_info() + return json.dumps(lsblk_info, indent=4, sort_keys=True, cls=JSON) + except SysCallError as err: + log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + return '' + except json.decoder.JSONDecodeError as err: + log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + return '' diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py new file mode 100644 index 00000000..0270a4dd --- /dev/null +++ b/archinstall/lib/disk/device_model.py @@ -0,0 +1,1033 @@ +from __future__ import annotations + +import dataclasses +import json +import logging +import math +import time +import uuid +from dataclasses import dataclass, field +from enum import Enum +from enum import auto +from pathlib import Path +from typing import Optional, List, Dict, TYPE_CHECKING, Any +from typing import Union + +import parted # type: ignore +from parted import Disk, Geometry, Partition + +from ..exceptions import DiskError, SysCallError +from ..general import SysCommand +from ..output import log +from ..storage import storage + +if TYPE_CHECKING: + _: Any + + +class DiskLayoutType(Enum): + Default = 'default_layout' + Manual = 'manual_partitioning' + Pre_mount = 'pre_mounted_config' + + def display_msg(self) -> str: + match self: + case DiskLayoutType.Default: return str(_('Use a best-effort default partition layout')) + case DiskLayoutType.Manual: return str(_('Manual Partitioning')) + case DiskLayoutType.Pre_mount: return str(_('Pre-mounted configuration')) + + +@dataclass +class DiskLayoutConfiguration: + config_type: DiskLayoutType + device_modifications: List[DeviceModification] = field(default_factory=list) + # used for pre-mounted config + relative_mountpoint: Optional[Path] = None + + def __post_init__(self): + if self.config_type == DiskLayoutType.Pre_mount and self.relative_mountpoint is None: + raise ValueError('Must set a relative mountpoint when layout type is pre-mount"') + + def __dump__(self) -> Dict[str, Any]: + return { + 'config_type': self.config_type.value, + 'device_modifications': [mod.__dump__() for mod in self.device_modifications] + } + + @classmethod + def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: + from .device_handler import device_handler + + device_modifications: List[DeviceModification] = [] + config_type = disk_config.get('config_type', None) + + if not config_type: + raise ValueError('Missing disk layout configuration: config_type') + + config = DiskLayoutConfiguration( + config_type=DiskLayoutType(config_type), + device_modifications=device_modifications + ) + + for entry in disk_config.get('device_modifications', []): + device_path = Path(entry.get('device', None)) if entry.get('device', None) else None + + if not device_path: + continue + + device = device_handler.get_device(device_path) + + if not device: + continue + + device_modification = DeviceModification( + wipe=entry.get('wipe', False), + device=device + ) + + device_partitions: List[PartitionModification] = [] + + for partition in entry.get('partitions', []): + device_partition = PartitionModification( + status=ModificationStatus(partition['status']), + fs_type=FilesystemType(partition['fs_type']), + start=Size.parse_args(partition['start']), + length=Size.parse_args(partition['length']), + mount_options=partition['mount_options'], + mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, + type=PartitionType(partition['type']), + flags=[PartitionFlag[f] for f in partition.get('flags', [])], + btrfs_subvols=SubvolumeModification.parse_args(partition.get('btrfs', [])), + ) + # special 'invisible attr to internally identify the part mod + setattr(device_partition, '_obj_id', partition['obj_id']) + device_partitions.append(device_partition) + + device_modification.partitions = device_partitions + device_modifications.append(device_modification) + + return config + + +class PartitionTable(Enum): + GPT = 'gpt' + MBR = 'msdos' + + +class Unit(Enum): + B = 1 # byte + kB = 1000**1 # kilobyte + MB = 1000**2 # megabyte + GB = 1000**3 # gigabyte + TB = 1000**4 # terabyte + PB = 1000**5 # petabyte + EB = 1000**6 # exabyte + ZB = 1000**7 # zettabyte + YB = 1000**8 # yottabyte + + KiB = 1024**1 # kibibyte + MiB = 1024**2 # mebibyte + GiB = 1024**3 # gibibyte + TiB = 1024**4 # tebibyte + PiB = 1024**5 # pebibyte + EiB = 1024**6 # exbibyte + ZiB = 1024**7 # zebibyte + YiB = 1024**8 # yobibyte + + sectors = 'sectors' # size in sector + + Percent = '%' # size in percentile + + +@dataclass +class Size: + value: int + unit: Unit + sector_size: Optional[Size] = None # only required when unit is sector + total_size: Optional[Size] = None # required when operating on percentages + + def __post_init__(self): + if self.unit == Unit.sectors and self.sector_size is None: + raise ValueError('Sector size is required when unit is sectors') + elif self.unit == Unit.Percent: + if self.value < 0 or self.value > 100: + raise ValueError('Percentage must be between 0 and 100') + elif self.total_size is None: + raise ValueError('Total size is required when unit is percentage') + + @property + def _total_size(self) -> Size: + """ + Save method to get the total size, mainly to satisfy mypy + This shouldn't happen as the Size object fails instantiation on missing total size + """ + if self.unit == Unit.Percent and self.total_size is None: + raise ValueError('Percent unit size must specify a total size') + return self.total_size # type: ignore + + def __dump__(self) -> Dict[str, Any]: + return { + 'value': self.value, + 'unit': self.unit.name, + 'sector_size': self.sector_size.__dump__() if self.sector_size else None, + 'total_size': self._total_size.__dump__() if self._total_size else None + } + + @classmethod + def parse_args(cls, size_arg: Dict[str, Any]) -> Size: + sector_size = size_arg['sector_size'] + total_size = size_arg['total_size'] + + return Size( + size_arg['value'], + Unit[size_arg['unit']], + Size.parse_args(sector_size) if sector_size else None, + Size.parse_args(total_size) if total_size else None + ) + + def convert( + self, + target_unit: Unit, + sector_size: Optional[Size] = None, + total_size: Optional[Size] = None + ) -> Size: + if target_unit == Unit.sectors and sector_size is None: + raise ValueError('If target has unit sector, a sector size must be provided') + + # not sure why we would ever wanna convert to percentages + if target_unit == Unit.Percent and total_size is None: + raise ValueError('Missing paramter total size to be able to convert to percentage') + + if self.unit == target_unit: + return self + elif self.unit == Unit.Percent: + amount = int(self._total_size._normalize() * (self.value / 100)) + return Size(amount, Unit.B) + elif self.unit == Unit.sectors: + norm = self._normalize() + return Size(norm, Unit.B).convert(target_unit, sector_size) + else: + if target_unit == Unit.sectors and sector_size is not None: + norm = self._normalize() + sectors = math.ceil(norm / sector_size.value) + return Size(sectors, Unit.sectors, sector_size) + else: + value = int(self._normalize() / target_unit.value) # type: ignore + return Size(value, target_unit) + + def format_size( + self, + target_unit: Unit, + sector_size: Optional[Size] = None + ) -> str: + if self.unit == Unit.Percent: + return f'{self.value}%' + else: + target_size = self.convert(target_unit, sector_size) + return f'{target_size.value} {target_unit.name}' + + def _normalize(self) -> int: + """ + will normalize the value of the unit to Byte + """ + if self.unit == Unit.Percent: + return self.convert(Unit.B).value + elif self.unit == Unit.sectors and self.sector_size is not None: + return self.value * self.sector_size._normalize() + return int(self.value * self.unit.value) # type: ignore + + def __sub__(self, other: Size) -> Size: + src_norm = self._normalize() + dest_norm = other._normalize() + return Size(abs(src_norm - dest_norm), Unit.B) + + def __lt__(self, other): + return self._normalize() < other._normalize() + + def __le__(self, other): + return self._normalize() <= other._normalize() + + def __eq__(self, other): + return self._normalize() == other._normalize() + + def __ne__(self, other): + return self._normalize() != other._normalize() + + def __gt__(self, other): + return self._normalize() > other._normalize() + + def __ge__(self, other): + return self._normalize() >= other._normalize() + + +@dataclass +class _BtrfsSubvolumeInfo: + name: Path + mountpoint: Optional[Path] + + +@dataclass +class _PartitionInfo: + partition: Partition + name: str + type: PartitionType + fs_type: FilesystemType + path: Path + start: Size + length: Size + flags: List[PartitionFlag] + partuuid: str + disk: Disk + mountpoints: List[Path] + btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) + + def as_json(self) -> Dict[str, Any]: + info = { + 'Name': self.name, + 'Type': self.type.value, + 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), + 'Path': str(self.path), + 'Start': self.start.format_size(Unit.MiB), + 'Length': self.length.format_size(Unit.MiB), + 'Flags': ', '.join([f.name for f in self.flags]) + } + + if self.btrfs_subvol_infos: + info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' + + return info + + @classmethod + def from_partition( + cls, + partition: Partition, + fs_type: FilesystemType, + partuuid: str, + mountpoints: List[Path], + btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = [] + ) -> _PartitionInfo: + partition_type = PartitionType.get_type_from_code(partition.type) + flags = [f for f in PartitionFlag if partition.getFlag(f.value)] + + start = Size( + partition.geometry.start, + Unit.sectors, + Size(partition.disk.device.sectorSize, Unit.B) + ) + + length = Size(int(partition.getLength(unit='B')), Unit.B) + + return _PartitionInfo( + partition=partition, + name=partition.get_name(), + type=partition_type, + fs_type=fs_type, + path=partition.path, + start=start, + length=length, + flags=flags, + partuuid=partuuid, + disk=partition.disk, + mountpoints=mountpoints, + btrfs_subvol_infos=btrfs_subvol_infos + ) + + +@dataclass +class _DeviceInfo: + model: str + path: Path + type: str + total_size: Size + free_space_regions: List[DeviceGeometry] + sector_size: Size + read_only: bool + dirty: bool + + def as_json(self) -> Dict[str, Any]: + total_free_space = sum([region.get_length(unit=Unit.MiB) for region in self.free_space_regions]) + return { + 'Model': self.model, + 'Path': str(self.path), + 'Type': self.type, + 'Size': self.total_size.format_size(Unit.MiB), + 'Free space': int(total_free_space), + 'Sector size': self.sector_size.value, + 'Read only': self.read_only + } + + @classmethod + def from_disk(cls, disk: Disk) -> _DeviceInfo: + device = disk.device + device_type = parted.devices[device.type] + + sector_size = Size(device.sectorSize, Unit.B) + free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] + + return _DeviceInfo( + model=device.model.strip(), + path=Path(device.path), + type=device_type, + sector_size=sector_size, + total_size=Size(int(device.getLength(unit='B')), Unit.B), + free_space_regions=free_space, + read_only=device.readOnly, + dirty=device.dirty + ) + + +@dataclass +class SubvolumeModification: + name: Path + mountpoint: Optional[Path] = None + compress: bool = False + nodatacow: bool = False + + @classmethod + def from_existing_subvol_info(cls, info: _BtrfsSubvolumeInfo) -> SubvolumeModification: + return SubvolumeModification(info.name, mountpoint=info.mountpoint) + + @classmethod + def parse_args(cls, subvol_args: List[Dict[str, Any]]) -> List[SubvolumeModification]: + mods = [] + for entry in subvol_args: + if not entry.get('name', None) or not entry.get('mountpoint', None): + log(f'Subvolume arg is missing name: {entry}', level=logging.DEBUG) + continue + + mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None + + mods.append( + SubvolumeModification( + entry['name'], + mountpoint, + entry.get('compress', False), + entry.get('nodatacow', False) + ) + ) + + return mods + + @property + def mount_options(self) -> List[str]: + options = [] + options += ['compress'] if self.compress else [] + options += ['nodatacow'] if self.nodatacow else [] + return options + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint is not None: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + if self.mountpoint: + if relative_mountpoint is not None: + return self.mountpoint.relative_to(relative_mountpoint) == Path('.') + return self.mountpoint == Path('/') + return False + + def __dump__(self) -> Dict[str, Any]: + return { + 'name': str(self.name), + 'mountpoint': str(self.mountpoint), + 'compress': self.compress, + 'nodatacow': self.nodatacow + } + + def as_json(self) -> Dict[str, Any]: + return { + 'name': str(self.name), + 'mountpoint': str(self.mountpoint), + 'compress': self.compress, + 'nodatacow': self.nodatacow + } + + +class DeviceGeometry: + def __init__(self, geometry: Geometry, sector_size: Size): + self._geometry = geometry + self._sector_size = sector_size + + @property + def start(self) -> int: + return self._geometry.start + + @property + def end(self) -> int: + return self._geometry.end + + def get_length(self, unit: Unit = Unit.sectors) -> int: + return self._geometry.getLength(unit.name) + + def as_json(self) -> Dict[str, Any]: + return { + 'Sector size': self._sector_size.value, + 'Start sector': self._geometry.start, + 'End sector': self._geometry.end, + 'Length': self._geometry.getLength() + } + + +@dataclass +class BDevice: + disk: Disk + device_info: _DeviceInfo + partition_infos: List[_PartitionInfo] + + def __hash__(self): + return hash(self.disk.device.path) + + +class PartitionType(Enum): + Boot = 'boot' + Primary = 'primary' + + @classmethod + def get_type_from_code(cls, code: int) -> PartitionType: + if code == parted.PARTITION_NORMAL: + return PartitionType.Primary + + raise DiskError(f'Partition code not supported: {code}') + + def get_partition_code(self) -> Optional[int]: + if self == PartitionType.Primary: + return parted.PARTITION_NORMAL + elif self == PartitionType.Boot: + return parted.PARTITION_BOOT + return None + + +class PartitionFlag(Enum): + Boot = 1 + + +class FilesystemType(Enum): + Btrfs = 'btrfs' + Ext2 = 'ext2' + Ext3 = 'ext3' + Ext4 = 'ext4' + F2fs = 'f2fs' + Fat16 = 'fat16' + Fat32 = 'fat32' + Ntfs = 'ntfs' + Reiserfs = 'reiserfs' + Xfs = 'xfs' + + # this is not a FS known to parted, so be careful + # with the usage from this enum + Crypto_luks = 'crypto_LUKS' + + def is_crypto(self) -> bool: + return self == FilesystemType.Crypto_luks + + @property + def fs_type_mount(self) -> str: + match self: + case FilesystemType.Ntfs: return 'ntfs3' + case FilesystemType.Fat32: return 'vfat' + case _: return self.value # type: ignore + + @property + def installation_pkg(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs-progs' + case FilesystemType.Xfs: return 'xfsprogs' + case FilesystemType.F2fs: return 'f2fs-tools' + case _: return None + + @property + def installation_module(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs' + case _: return None + + @property + def installation_binary(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return '/usr/bin/btrfs' + case _: return None + + @property + def installation_hooks(self) -> Optional[str]: + match self: + case FilesystemType.Btrfs: return 'btrfs' + case _: return None + + +class ModificationStatus(Enum): + Exist = 'existing' + Modify = 'modify' + Delete = 'delete' + Create = 'create' + + +@dataclass +class PartitionModification: + status: ModificationStatus + type: PartitionType + start: Size + length: Size + fs_type: FilesystemType + mountpoint: Optional[Path] = None + mount_options: List[str] = field(default_factory=list) + flags: List[PartitionFlag] = field(default_factory=list) + btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + + # only set if the device was created or exists + dev_path: Optional[Path] = None + partuuid: Optional[str] = None + uuid: Optional[str] = None + + def __post_init__(self): + # needed to use the object as a dictionary key due to hash func + if not hasattr(self, '_obj_id'): + self._obj_id = uuid.uuid4() + + if self.is_exists_or_modify() and not self.dev_path: + raise ValueError('If partition marked as existing a path must be set') + + def __hash__(self): + return hash(self._obj_id) + + @property + def obj_id(self) -> str: + if hasattr(self, '_obj_id'): + return str(self._obj_id) + return '' + + @property + def real_dev_path(self) -> Path: + if self.dev_path is None: + raise ValueError('Device path was not set') + return self.dev_path + + @classmethod + def from_existing_partition(cls, partition_info: _PartitionInfo) -> PartitionModification: + if partition_info.btrfs_subvol_infos: + mountpoint = None + subvol_mods = [] + for info in partition_info.btrfs_subvol_infos: + subvol_mods.append( + SubvolumeModification.from_existing_subvol_info(info) + ) + else: + mountpoint = partition_info.mountpoints[0] if partition_info.mountpoints else None + subvol_mods = [] + + return PartitionModification( + status=ModificationStatus.Exist, + type=partition_info.type, + start=partition_info.start, + length=partition_info.length, + fs_type=partition_info.fs_type, + dev_path=partition_info.path, + flags=partition_info.flags, + mountpoint=mountpoint, + btrfs_subvols=subvol_mods + ) + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + def is_boot(self) -> bool: + return PartitionFlag.Boot in self.flags + + def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + if relative_mountpoint is not None and self.mountpoint is not None: + return self.mountpoint.relative_to(relative_mountpoint) == Path('.') + elif self.mountpoint is not None: + return Path('/') == self.mountpoint + else: + for subvol in self.btrfs_subvols: + if subvol.is_root(relative_mountpoint): + return True + + return False + + def is_modify(self) -> bool: + return self.status == ModificationStatus.Modify + + def exists(self) -> bool: + return self.status == ModificationStatus.Exist + + def is_exists_or_modify(self) -> bool: + return self.status in [ModificationStatus.Exist, ModificationStatus.Modify] + + @property + def mapper_name(self) -> Optional[str]: + if self.dev_path: + return f'{storage.get("ENC_IDENTIFIER", "ai")}{self.dev_path.name}' + return None + + def set_flag(self, flag: PartitionFlag): + if flag not in self.flags: + self.flags.append(flag) + + def invert_flag(self, flag: PartitionFlag): + if flag in self.flags: + self.flags = [f for f in self.flags if f != flag] + else: + self.set_flag(flag) + + def json(self) -> Dict[str, Any]: + """ + Called for configuration settings + """ + return { + 'obj_id': self.obj_id, + 'status': self.status.value, + 'type': self.type.value, + 'start': self.start.__dump__(), + 'length': self.length.__dump__(), + 'fs_type': self.fs_type.value, + 'mountpoint': str(self.mountpoint) if self.mountpoint else None, + 'mount_options': self.mount_options, + 'flags': [f.name for f in self.flags], + 'btrfs': [vol.__dump__() for vol in self.btrfs_subvols] + } + + def as_json(self) -> Dict[str, Any]: + """ + Called for displaying data in table format + """ + info = { + 'Status': self.status.value, + 'Device': str(self.dev_path) if self.dev_path else '', + 'Type': self.type.value, + 'Start': self.start.format_size(Unit.MiB), + 'Length': self.length.format_size(Unit.MiB), + 'FS type': self.fs_type.value, + 'Mountpoint': self.mountpoint if self.mountpoint else '', + 'Mount options': ', '.join(self.mount_options), + 'Flags': ', '.join([f.name for f in self.flags]), + } + + if self.btrfs_subvols: + info['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' + + return info + + +@dataclass +class DeviceModification: + device: BDevice + wipe: bool + partitions: List[PartitionModification] = field(default_factory=list) + + @property + def device_path(self) -> Path: + return self.device.device_info.path + + def add_partition(self, partition: PartitionModification): + self.partitions.append(partition) + + def get_boot_partition(self) -> Optional[PartitionModification]: + liltered = filter(lambda x: x.is_boot(), self.partitions) + return next(liltered, None) + + def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: + filtered = filter(lambda x: x.is_root(relative_path), self.partitions) + return next(filtered, None) + + def __dump__(self) -> Dict[str, Any]: + """ + Called when generating configuration files + """ + return { + 'device': str(self.device.device_info.path), + 'wipe': self.wipe, + 'partitions': [p.json() for p in self.partitions] + } + + +class EncryptionType(Enum): + NoEncryption = "no_encryption" + Partition = "partition" + + @classmethod + def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: + return { + # str(_('Full disk encryption')): EncryptionType.FullDiskEncryption, + str(_('Partition encryption')): EncryptionType.Partition + } + + @classmethod + def text_to_type(cls, text: str) -> 'EncryptionType': + mapping = cls._encryption_type_mapper() + return mapping[text] + + @classmethod + def type_to_text(cls, type_: 'EncryptionType') -> str: + mapping = cls._encryption_type_mapper() + type_to_text = {type_: text for text, type_ in mapping.items()} + return type_to_text[type_] + + +@dataclass +class DiskEncryption: + encryption_type: EncryptionType = EncryptionType.Partition + encryption_password: str = '' + partitions: List[PartitionModification] = field(default_factory=list) + hsm_device: Optional[Fido2Device] = None + + def should_generate_encryption_file(self, part_mod: PartitionModification) -> bool: + return part_mod in self.partitions and part_mod.mountpoint != Path('/') + + def json(self) -> Dict[str, Any]: + obj: Dict[str, Any] = { + 'encryption_type': self.encryption_type.value, + 'partitions': [p.obj_id for p in self.partitions] + } + + if self.hsm_device: + obj['hsm_device'] = self.hsm_device.json() + + return obj + + @classmethod + def parse_arg( + cls, + disk_config: DiskLayoutConfiguration, + arg: Dict[str, Any], + password: str = '' + ) -> 'DiskEncryption': + enc_partitions = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('partitions', []): + enc_partitions.append(part) + + enc = DiskEncryption( + EncryptionType(arg['encryption_type']), + password, + enc_partitions + ) + + if hsm := arg.get('hsm_device', None): + enc.hsm_device = Fido2Device.parse_arg(hsm) + + return enc + + +@dataclass +class Fido2Device: + path: Path + manufacturer: str + product: str + + def json(self) -> Dict[str, str]: + return { + 'path': str(self.path), + 'manufacturer': self.manufacturer, + 'product': self.product + } + + @classmethod + def parse_arg(cls, arg: Dict[str, str]) -> 'Fido2Device': + return Fido2Device( + Path(arg['path']), + arg['manufacturer'], + arg['product'] + ) + + +@dataclass +class LsblkInfo: + name: str = '' + path: Path = Path() + pkname: str = '' + size: Size = Size(0, Unit.B) + log_sec: int = 0 + pttype: str = '' + ptuuid: str = '' + rota: bool = False + tran: Optional[str] = None + partuuid: Optional[str] = None + uuid: Optional[str] = None + fstype: Optional[str] = None + fsver: Optional[str] = None + fsavail: Optional[str] = None + fsuse_percentage: Optional[str] = None + type: Optional[str] = None + mountpoint: Optional[Path] = None + mountpoints: List[Path] = field(default_factory=list) + fsroots: List[Path] = field(default_factory=list) + children: List[LsblkInfo] = field(default_factory=list) + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'path': str(self.path), + 'pkname': self.pkname, + 'size': self.size.format_size(Unit.MiB), + 'log_sec': self.log_sec, + 'pttype': self.pttype, + 'ptuuid': self.ptuuid, + 'rota': self.rota, + 'tran': self.tran, + 'partuuid': self.partuuid, + 'uuid': self.uuid, + 'fstype': self.fstype, + 'fsver': self.fsver, + 'fsavail': self.fsavail, + 'fsuse_percentage': self.fsuse_percentage, + 'type': self.type, + 'mountpoint': self.mountpoint, + 'mountpoints': [str(m) for m in self.mountpoints], + 'fsroots': [str(r) for r in self.fsroots], + 'children': [c.json() for c in self.children] + } + + @property + def btrfs_subvol_info(self) -> Dict[Path, Path]: + """ + It is assumed that lsblk will contain the fields as + + "mountpoints": ["/mnt/archinstall/log", "/mnt/archinstall/home", "/mnt/archinstall", ...] + "fsroots": ["/@log", "/@home", "/@"...] + + we'll thereby map the fsroot, which are the mounted filesystem roots + to the corresponding mountpoints + """ + return dict(zip(self.fsroots, self.mountpoints)) + + @classmethod + def exclude(cls) -> List[str]: + return ['children'] + + @classmethod + def fields(cls) -> List[str]: + return [f.name for f in dataclasses.fields(LsblkInfo) if f.name not in cls.exclude()] + + @classmethod + def from_json(cls, blockdevice: Dict[str, Any]) -> LsblkInfo: + info = cls() + + for f in cls.fields(): + lsblk_field = _clean_field(f, CleanType.Blockdevice) + data_field = _clean_field(f, CleanType.Dataclass) + + val: Any = None + if isinstance(getattr(info, data_field), Path): + val = Path(blockdevice[lsblk_field]) + elif isinstance(getattr(info, data_field), Size): + val = Size(blockdevice[lsblk_field], Unit.B) + else: + val = blockdevice[lsblk_field] + + setattr(info, data_field, val) + + info.children = [LsblkInfo.from_json(child) for child in blockdevice.get('children', [])] + + # sometimes lsblk returns 'mountpoints': [null] + info.mountpoints = [Path(mnt) for mnt in info.mountpoints if mnt] + + fs_roots = [] + for r in info.fsroots: + if r: + path = Path(r) + # store the fsroot entries without the leading / + fs_roots.append(path.relative_to(path.anchor)) + info.fsroots = fs_roots + + return info + + +class CleanType(Enum): + Blockdevice = auto() + Dataclass = auto() + Lsblk = auto() + + +def _clean_field(name: str, clean_type: CleanType) -> str: + match clean_type: + case CleanType.Blockdevice: + return name.replace('_percentage', '%').replace('_', '-') + case CleanType.Dataclass: + return name.lower().replace('-', '_').replace('%', '_percentage') + case CleanType.Lsblk: + return name.replace('_percentage', '%').replace('_', '-') + + +def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = 3) -> List[LsblkInfo]: + fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] + lsblk_fields = ','.join(fields) + + if not dev_path: + dev_path = '' + + if retry == 0: + retry = 1 + + result = None + + for i in range(retry): + try: + result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') + except SysCallError as error: + # Get the output minus the message/info from lsblk if it returns a non-zero exit code. + if error.worker: + err = error.worker.decode('UTF-8') + log(f'Error calling lsblk: {err}', level=logging.DEBUG) + time.sleep(1) + else: + raise error + + if result and result.exit_code == 0: + try: + if decoded := result.decode('utf-8'): + block_devices = json.loads(decoded) + blockdevices = block_devices['blockdevices'] + return [LsblkInfo.from_json(device) for device in blockdevices] + except json.decoder.JSONDecodeError as err: + log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + raise err + + raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + + +def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: + if infos := _fetch_lsblk_info(dev_path): + return infos[0] + + raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') + + +def get_all_lsblk_info() -> List[LsblkInfo]: + return _fetch_lsblk_info() + + +def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> List[LsblkInfo]: + def _check(infos: List[LsblkInfo]) -> List[LsblkInfo]: + devices = [] + for entry in infos: + if as_prefix: + matches = [m for m in entry.mountpoints if str(m).startswith(str(mountpoint))] + if matches: + devices += [entry] + elif mountpoint in entry.mountpoints: + devices += [entry] + + if len(entry.children) > 0: + if len(match := _check(entry.children)) > 0: + devices += match + + return devices + + all_info = get_all_lsblk_info() + return _check(all_info) diff --git a/archinstall/lib/disk/diskinfo.py b/archinstall/lib/disk/diskinfo.py deleted file mode 100644 index b56ba282..00000000 --- a/archinstall/lib/disk/diskinfo.py +++ /dev/null @@ -1,40 +0,0 @@ -import dataclasses -import json -from dataclasses import dataclass, field -from typing import Optional, List - -from ..general import SysCommand -from ..exceptions import DiskError - -@dataclass -class LsblkInfo: - size: int = 0 - log_sec: int = 0 - pttype: Optional[str] = None - rota: bool = False - tran: Optional[str] = None - ptuuid: Optional[str] = None - partuuid: Optional[str] = None - uuid: Optional[str] = None - fstype: Optional[str] = None - type: Optional[str] = None - mountpoints: List[str] = field(default_factory=list) - - -def get_lsblk_info(dev_path: str) -> LsblkInfo: - fields = [f.name for f in dataclasses.fields(LsblkInfo)] - lsblk_fields = ','.join([f.upper().replace('_', '-') for f in fields]) - - output = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode('UTF-8') - - if output: - block_devices = json.loads(output) - info = block_devices['blockdevices'][0] - lsblk_info = LsblkInfo() - - for f in fields: - setattr(lsblk_info, f, info[f.replace('_', '-')]) - - return lsblk_info - - raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') diff --git a/archinstall/lib/disk/dmcryptdev.py b/archinstall/lib/disk/dmcryptdev.py deleted file mode 100644 index 63392ffb..00000000 --- a/archinstall/lib/disk/dmcryptdev.py +++ /dev/null @@ -1,48 +0,0 @@ -import pathlib -import logging -import json -from dataclasses import dataclass -from typing import Optional -from ..exceptions import SysCallError -from ..general import SysCommand -from ..output import log -from .mapperdev import MapperDev - -@dataclass -class DMCryptDev: - dev_path :pathlib.Path - - @property - def name(self): - with open(f"/sys/devices/virtual/block/{pathlib.Path(self.path).name}/dm/name", "r") as fh: - return fh.read().strip() - - @property - def path(self): - return f"/dev/mapper/{self.dev_path}" - - @property - def blockdev(self): - pass - - @property - def MapperDev(self): - return MapperDev(mappername=self.name) - - @property - def mountpoint(self) -> Optional[str]: - try: - data = json.loads(SysCommand(f"findmnt --json -R {self.dev_path}").decode()) - for filesystem in data['filesystems']: - return filesystem.get('target') - - except SysCallError as error: - # Not mounted anywhere most likely - log(f"Could not locate mount information for {self.dev_path}: {error}", level=logging.WARNING, fg="yellow") - pass - - return None - - @property - def filesystem(self) -> Optional[str]: - return self.MapperDev.filesystem \ No newline at end of file diff --git a/archinstall/lib/disk/encryption.py b/archinstall/lib/disk/encryption.py deleted file mode 100644 index c7496bfa..00000000 --- a/archinstall/lib/disk/encryption.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import Dict, Optional, Any, TYPE_CHECKING, List - -from ..menu.abstract_menu import Selector, AbstractSubMenu -from ..menu.menu import MenuSelectionType -from ..menu.table_selection_menu import TableMenu -from ..models.disk_encryption import EncryptionType, DiskEncryption -from ..user_interaction.partitioning_conf import current_partition_layout -from ..user_interaction.utils import get_password -from ..menu import Menu -from ..general import secret -from ..hsm.fido import Fido2Device, Fido2 - -if TYPE_CHECKING: - _: Any - - -class DiskEncryptionMenu(AbstractSubMenu): - def __init__(self, data_store: Dict[str, Any], preset: Optional[DiskEncryption], disk_layouts: Dict[str, Any]): - if preset: - self._preset = preset - else: - self._preset = DiskEncryption() - - self._disk_layouts = disk_layouts - super().__init__(data_store=data_store) - - def _setup_selection_menu_options(self): - self._menu_options['encryption_password'] = \ - Selector( - _('Encryption password'), - lambda x: select_encrypted_password(), - display_func=lambda x: secret(x) if x else '', - default=self._preset.encryption_password, - enabled=True - ) - self._menu_options['encryption_type'] = \ - Selector( - _('Encryption type'), - func=lambda preset: select_encryption_type(preset), - display_func=lambda x: EncryptionType.type_to_text(x) if x else None, - dependencies=['encryption_password'], - default=self._preset.encryption_type, - enabled=True - ) - self._menu_options['partitions'] = \ - Selector( - _('Partitions'), - func=lambda preset: select_partitions_to_encrypt(self._disk_layouts, preset), - display_func=lambda x: f'{sum([len(y) for y in x.values()])} {_("Partitions")}' if x else None, - dependencies=['encryption_password'], - default=self._preset.partitions, - preview_func=self._prev_disk_layouts, - enabled=True - ) - self._menu_options['HSM'] = \ - Selector( - description=_('Use HSM to unlock encrypted drive'), - func=lambda preset: select_hsm(preset), - display_func=lambda x: self._display_hsm(x), - dependencies=['encryption_password'], - default=self._preset.hsm_device, - enabled=True - ) - - def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: - super().run(allow_reset=allow_reset) - - if self._data_store.get('encryption_password', None): - return DiskEncryption( - encryption_password=self._data_store.get('encryption_password', None), - encryption_type=self._data_store['encryption_type'], - partitions=self._data_store.get('partitions', None), - hsm_device=self._data_store.get('HSM', None) - ) - - return None - - def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]: - if device: - return device.manufacturer - - if not Fido2.get_fido2_devices(): - return str(_('No HSM devices available')) - return None - - def _prev_disk_layouts(self) -> Optional[str]: - selector = self._menu_options['partitions'] - if selector.has_selection(): - partitions: Dict[str, Any] = selector.current_selection - - all_partitions = [] - for parts in partitions.values(): - all_partitions += parts - - output = str(_('Partitions to be encrypted')) + '\n' - output += current_partition_layout(all_partitions, with_title=False) - return output.rstrip() - return None - - -def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: - title = str(_('Select disk encryption option')) - options = [ - # _type_to_text(EncryptionType.FullDiskEncryption), - EncryptionType.type_to_text(EncryptionType.Partition) - ] - - preset_value = EncryptionType.type_to_text(preset) - choice = Menu(title, options, preset_values=preset_value).run() - - match choice.type_: - case MenuSelectionType.Reset: return None - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore - - -def select_encrypted_password() -> Optional[str]: - if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): - return passwd - return None - - -def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]: - title = _('Select a FIDO2 device to use for HSM') - fido_devices = Fido2.get_fido2_devices() - - if fido_devices: - choice = TableMenu(title, data=fido_devices).run() - match choice.type_: - case MenuSelectionType.Reset: - return None - case MenuSelectionType.Skip: - return preset - case MenuSelectionType.Selection: - return choice.value # type: ignore - - return None - - -def select_partitions_to_encrypt(disk_layouts: Dict[str, Any], preset: Dict[str, Any]) -> Dict[str, Any]: - # If no partitions was marked as encrypted, but a password was supplied and we have some disks to format.. - # Then we need to identify which partitions to encrypt. This will default to / (root). - all_partitions = [] - for blockdevice in disk_layouts.values(): - if partitions := blockdevice.get('partitions'): - partitions = [p for p in partitions if p['mountpoint'] != '/boot'] - all_partitions += partitions - - if all_partitions: - title = str(_('Select which partitions to encrypt')) - partition_table = current_partition_layout(all_partitions, with_title=False).strip() - - choice = TableMenu( - title, - table_data=(all_partitions, partition_table), - multi=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset - case MenuSelectionType.Selection: - selections: List[Any] = choice.value # type: ignore - partitions = {} - - for path, device in disk_layouts.items(): - for part in selections: - if part in device.get('partitions', []): - partitions.setdefault(path, []).append(part) - - return partitions - return {} diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py new file mode 100644 index 00000000..285270fb --- /dev/null +++ b/archinstall/lib/disk/encryption_menu.py @@ -0,0 +1,179 @@ +from pathlib import Path +from typing import Dict, Optional, Any, TYPE_CHECKING, List + +from ..disk import ( + DeviceModification, + PartitionModification, + DiskEncryption, + EncryptionType +) +from ..menu import ( + Selector, + AbstractSubMenu, + MenuSelectionType, + TableMenu +) +from ..user_interaction.utils import get_password +from ..menu import Menu +from ..general import secret +from .fido import Fido2Device, Fido2 +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class DiskEncryptionMenu(AbstractSubMenu): + def __init__( + self, + mods: List[DeviceModification], + data_store: Dict[str, Any], + preset: Optional[DiskEncryption] = None + ): + if preset: + self._preset = preset + else: + self._preset = DiskEncryption() + + self._modifications = mods + super().__init__(data_store=data_store) + + def setup_selection_menu_options(self): + self._menu_options['encryption_password'] = \ + Selector( + _('Encryption password'), + lambda x: select_encrypted_password(), + display_func=lambda x: secret(x) if x else '', + default=self._preset.encryption_password, + enabled=True + ) + self._menu_options['encryption_type'] = \ + Selector( + _('Encryption type'), + func=lambda preset: select_encryption_type(preset), + display_func=lambda x: EncryptionType.type_to_text(x) if x else None, + dependencies=['encryption_password'], + default=self._preset.encryption_type, + enabled=True + ) + self._menu_options['partitions'] = \ + Selector( + _('Partitions'), + func=lambda preset: select_partitions_to_encrypt(self._modifications.device_modifications, preset), + display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None, + dependencies=['encryption_password'], + default=self._preset.partitions, + preview_func=self._prev_disk_layouts, + enabled=True + ) + self._menu_options['HSM'] = \ + Selector( + description=_('Use HSM to unlock encrypted drive'), + func=lambda preset: select_hsm(preset), + display_func=lambda x: self._display_hsm(x), + dependencies=['encryption_password'], + default=self._preset.hsm_device, + enabled=True + ) + + def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: + super().run(allow_reset=allow_reset) + + if self._data_store.get('encryption_password', None): + return DiskEncryption( + encryption_password=self._data_store.get('encryption_password', None), + encryption_type=self._data_store['encryption_type'], + partitions=self._data_store.get('partitions', None), + hsm_device=self._data_store.get('HSM', None) + ) + + return None + + def _display_hsm(self, device: Optional[Fido2Device]) -> Optional[str]: + if device: + return device.manufacturer + + if not Fido2.get_fido2_devices(): + return str(_('No HSM devices available')) + return None + + def _prev_disk_layouts(self) -> Optional[str]: + partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection + if partitions: + output = str(_('Partitions to be encrypted')) + '\n' + output += FormattedOutput.as_table(partitions) + return output.rstrip() + + return None + + +def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: + title = str(_('Select disk encryption option')) + options = [ + EncryptionType.type_to_text(EncryptionType.Partition) + ] + + preset_value = EncryptionType.type_to_text(preset) + choice = Menu(title, options, preset_values=preset_value).run() + + match choice.type_: + case MenuSelectionType.Reset: return None + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return EncryptionType.text_to_type(choice.value) # type: ignore + + +def select_encrypted_password() -> Optional[str]: + if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): + return passwd + return None + + +def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]: + title = _('Select a FIDO2 device to use for HSM') + fido_devices = Fido2.get_fido2_devices() + + if fido_devices: + choice = TableMenu(title, data=fido_devices).run() + match choice.type_: + case MenuSelectionType.Reset: + return None + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.value # type: ignore + + return None + + +def select_partitions_to_encrypt( + modification: List[DeviceModification], + preset: List[PartitionModification] +) -> List[PartitionModification]: + partitions: List[PartitionModification] = [] + + # do not allow encrypting the boot partition + for mod in modification: + partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) + + # do not allow encrypting existing partitions that are not marked as wipe + avail_partitions = list(filter(lambda x: not x.exists(), partitions)) + + if avail_partitions: + title = str(_('Select which partitions to encrypt')) + partition_table = FormattedOutput.as_table(avail_partitions) + + choice = TableMenu( + title, + table_data=(avail_partitions, partition_table), + preset=preset, + multi=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return [] + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.multi_value + return [] diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py new file mode 100644 index 00000000..436be4d4 --- /dev/null +++ b/archinstall/lib/disk/fido.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import getpass +import logging +from typing import List + +from .device_model import PartitionModification, Fido2Device +from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes +from ..output import log + + +class Fido2: + _loaded: bool = False + _fido2_devices: List[Fido2Device] = [] + + @classmethod + def get_fido2_devices(cls, reload: bool = False) -> List[Fido2Device]: + """ + Uses systemd-cryptenroll to list the FIDO2 devices + connected that supports FIDO2. + Some devices might show up in udevadm as FIDO2 compliant + when they are in fact not. + + The drawback of systemd-cryptenroll is that it uses human readable format. + That means we get this weird table like structure that is of no use. + + So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index + and we split each line based on those positions. + + Output example: + + PATH MANUFACTURER PRODUCT + /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID + """ + + # to prevent continous reloading which will slow + # down moving the cursor in the menu + if not cls._loaded or reload: + ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + if not ret: + log('Unable to retrieve fido2 devices', level=logging.ERROR) + return [] + + fido_devices = clear_vt100_escape_codes(ret) + + manufacturer_pos = 0 + product_pos = 0 + devices = [] + + for line in fido_devices.split('\r\n'): + if '/dev' not in line: + manufacturer_pos = line.find('MANUFACTURER') + product_pos = line.find('PRODUCT') + continue + + path = line[:manufacturer_pos].rstrip() + manufacturer = line[manufacturer_pos:product_pos].rstrip() + product = line[product_pos:] + + devices.append( + Fido2Device(path, manufacturer, product) + ) + + cls._loaded = True + cls._fido2_devices = devices + + return cls._fido2_devices + + @classmethod + def fido2_enroll( + cls, + hsm_device: Fido2Device, + part_mod: PartitionModification, + password: str + ): + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {part_mod.dev_path}", peek_output=True) + pw_inputted = False + pin_inputted = False + + while worker.is_alive(): + if pw_inputted is False: + if bytes(f"please enter current passphrase for disk {part_mod.dev_path}", 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(password, 'UTF-8')) + pw_inputted = True + elif pin_inputted is False: + if bytes(f"please enter security token pin", 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(getpass.getpass(" "), 'UTF-8')) + pin_inputted = True + + log( + f"You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds.", + level=logging.INFO, + fg="yellow" + ) diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 1083df53..6ea99340 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -1,301 +1,98 @@ from __future__ import annotations -import time -import logging -import json -import pathlib -from typing import Optional, Dict, Any, TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 -from ..models.disk_encryption import DiskEncryption -if TYPE_CHECKING: - from .blockdevice import BlockDevice - _: Any +import logging +import signal +import sys +import time +from typing import Any, Optional, TYPE_CHECKING -from .partition import Partition -from .validators import valid_fs_type -from ..exceptions import DiskError, SysCallError -from ..general import SysCommand +from .device_model import DiskLayoutConfiguration, DiskLayoutType, PartitionTable, FilesystemType, DiskEncryption +from .device_handler import device_handler +from ..hardware import has_uefi from ..output import log -from ..storage import storage - -GPT = 0b00000001 -MBR = 0b00000010 - -# A sane default is 5MiB, that allows for plenty of buffer for GRUB on MBR -# but also 4MiB for memory cards for instance. And another 1MiB to avoid issues. -# (we've been pestered by disk issues since the start, so please let this be here for a few versions) -DEFAULT_PARTITION_START = '5MiB' - -class Filesystem: - # TODO: - # When instance of a HDD is selected, check all usages and gracefully unmount them - # as well as close any crypto handles. - def __init__(self, blockdevice :BlockDevice, mode :int): - self.blockdevice = blockdevice - self.mode = mode - - def __enter__(self, *args :str, **kwargs :str) -> 'Filesystem': - return self - - def __repr__(self) -> str: - return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})" +from ..menu import Menu - def __exit__(self, *args :str, **kwargs :str) -> bool: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - if len(args) >= 2 and args[1]: - raise args[1] - - SysCommand('sync') - return True - - def partuuid_to_index(self, uuid :str) -> Optional[int]: - for i in range(storage['DISK_RETRY_ATTEMPTS']): - self.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) - - # We'll use unreliable lbslk to grab children under the /dev/ - output = json.loads(SysCommand(f"lsblk --json {self.blockdevice.device}").decode('UTF-8')) +if TYPE_CHECKING: + _: Any - for device in output['blockdevices']: - for index, partition in enumerate(device.get('children', [])): - # But we'll use blkid to reliably grab the PARTUUID for that child device (partition) - partition_uuid = SysCommand(f"blkid -s PARTUUID -o value /dev/{partition.get('name')}").decode().strip() - if partition_uuid.lower() == uuid.lower(): - return index - raise DiskError(f"Failed to convert PARTUUID {uuid} to a partition index number on blockdevice {self.blockdevice.device}") +class FilesystemHandler: + def __init__( + self, + disk_config: DiskLayoutConfiguration, + enc_conf: Optional[DiskEncryption] = None + ): + self._disk_config = disk_config + self._enc_config = enc_conf - def load_layout(self, layout :Dict[str, Any]) -> None: - from ..luks import luks2 - from .btrfs import BTRFSPartition + def perform_filesystem_operations(self, show_countdown: bool = True): + if self._disk_config.config_type == DiskLayoutType.Pre_mount: + log('Disk layout configuration is set to pre-mount, not performing any operations', level=logging.DEBUG) + return - # If the layout tells us to wipe the drive, we do so - if layout.get('wipe', False): - if self.mode == GPT: - if not self.parted_mklabel(self.blockdevice.device, "gpt"): - raise KeyError(f"Could not create a GPT label on {self}") - elif self.mode == MBR: - if not self.parted_mklabel(self.blockdevice.device, "msdos"): - raise KeyError(f"Could not create a MS-DOS label on {self}") + device_mods = list(filter(lambda x: len(x.partitions) > 0, self._disk_config.device_modifications)) - self.blockdevice.flush_cache() - time.sleep(3) + if not device_mods: + log('No modifications required', level=logging.DEBUG) + return - prev_partition = None - # We then iterate the partitions in order - for partition in layout.get('partitions', []): - # We don't want to re-add an existing partition (those containing a UUID already) - if partition.get('wipe', False) and not partition.get('PARTUUID', None): - start = partition.get('start') or ( - prev_partition and f'{prev_partition["device_instance"].end_sectors}s' or DEFAULT_PARTITION_START) - partition['device_instance'] = self.add_partition(partition.get('type', 'primary'), - start=start, - end=partition.get('size', '100%'), - partition_format=partition.get('filesystem', {}).get('format', 'btrfs'), - skip_mklabel=layout.get('wipe', False) is not False) + device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods]) - elif (partition_uuid := partition.get('PARTUUID')): - # We try to deal with both UUID and PARTUUID of a partition when it's being re-used. - # We should re-name or separate this logi based on partition.get('PARTUUID') and partition.get('UUID') - # but for now, lets just attempt to deal with both. - try: - partition['device_instance'] = self.blockdevice.get_partition(uuid=partition_uuid) - except DiskError: - partition['device_instance'] = self.blockdevice.get_partition(partuuid=partition_uuid) + # Issue a final warning before we continue with something un-revertable. + # We mention the drive one last time, and count from 5 to 0. + print(str(_(' ! Formatting {} in ')).format(device_paths)) - log(_("Re-using partition instance: {}").format(partition['device_instance']), level=logging.DEBUG, fg="gray") - else: - log(f"{self}.load_layout() doesn't know how to work without 'wipe' being set or UUID ({partition.get('PARTUUID')}) was given and found.", fg="yellow", level=logging.WARNING) - continue + if show_countdown: + self._do_countdown() - if partition.get('filesystem', {}).get('format', False): - # needed for backward compatibility with the introduction of the new "format_options" - format_options = partition.get('options',[]) + partition.get('filesystem',{}).get('format_options',[]) - disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') + # Setup the blockdevice, filesystem (and optionally encryption). + # Once that's done, we'll hand over to perform_installation() + partition_table = PartitionTable.GPT + if has_uefi() is False: + partition_table = PartitionTable.MBR - if disk_encryption and partition in disk_encryption.all_partitions: - if not partition['device_instance']: - raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!") + for mod in device_mods: + device_handler.partition(mod, partition_table=partition_table) + device_handler.format(mod, enc_conf=self._enc_config) - if partition.get('mountpoint',None): - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop" - else: - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" + for part_mod in mod.partitions: + if part_mod.fs_type == FilesystemType.Btrfs: + device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) - partition['device_instance'].encrypt(password=disk_encryption.encryption_password) - # Immediately unlock the encrypted device to format the inner volume - with luks2(partition['device_instance'], loopdev, disk_encryption.encryption_password, auto_unmount=True) as unlocked_device: - if not partition.get('wipe'): - if storage['arguments'] == 'silent': - raise ValueError(f"Missing fs-type to format on newly created encrypted partition {partition['device_instance']}") - else: - if not partition.get('filesystem'): - partition['filesystem'] = {} + def _do_countdown(self) -> bool: + SIG_TRIGGER = False - if not partition['filesystem'].get('format', False): - while True: - partition['filesystem']['format'] = input(f"Enter a valid fs-type for newly encrypted partition {partition['filesystem']['format']}: ").strip() - if not partition['filesystem']['format'] or valid_fs_type(partition['filesystem']['format']) is False: - log(_("You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's.")) - continue - break + def kill_handler(sig: int, frame: Any) -> None: + print() + exit(0) - unlocked_device.format(partition['filesystem']['format'], options=format_options) + def sig_handler(sig: int, frame: Any) -> None: + signal.signal(signal.SIGINT, kill_handler) - elif partition.get('wipe', False): - if not partition['device_instance']: - raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!") + original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, sig_handler) - partition['device_instance'].format(partition['filesystem']['format'], options=format_options) + for i in range(5, 0, -1): + print(f"{i}", end='') - if partition['filesystem']['format'] == 'btrfs': - # We upgrade the device instance to a BTRFSPartition if we format it as such. - # This is so that we can gain access to more features than otherwise available in Partition() - partition['device_instance'] = BTRFSPartition( - partition['device_instance'].path, - block_device=partition['device_instance'].block_device, - encrypted=False, - filesystem='btrfs', - autodetect_filesystem=False - ) + for x in range(4): + sys.stdout.flush() + time.sleep(0.25) + print(".", end='') - if partition.get('boot', False): - log(f"Marking partition {partition['device_instance']} as bootable.") - self.set(self.partuuid_to_index(partition['device_instance'].part_uuid), 'boot on') + if SIG_TRIGGER: + prompt = _('Do you really want to abort?') + choice = Menu(prompt, Menu.yes_no(), skip=False).run() + if choice.value == Menu.yes(): + exit(0) - prev_partition = partition + if SIG_TRIGGER is False: + sys.stdin.read() - def find_partition(self, mountpoint :str) -> Partition: - for partition in self.blockdevice: - if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint: - return partition + SIG_TRIGGER = False + signal.signal(signal.SIGINT, sig_handler) - def partprobe(self) -> bool: - try: - SysCommand(f'partprobe {self.blockdevice.device}') - except SysCallError as error: - log(f"Could not execute partprobe: {error!r}", level=logging.ERROR, fg="red") - raise DiskError(f"Could not run partprobe on {self.blockdevice.device}: {error!r}") + print() + signal.signal(signal.SIGINT, original_sigint_handler) return True - - def raw_parted(self, string: str) -> SysCommand: - try: - cmd_handle = SysCommand(f'/usr/bin/parted -s {string}') - time.sleep(0.5) - return cmd_handle - except SysCallError as error: - log(f"Parted ended with a bad exit code: {error.exit_code} ({error})", level=logging.ERROR, fg="red") - return error - - def parted(self, string: str) -> bool: - """ - Performs a parted execution of the given string - - :param string: A raw string passed to /usr/bin/parted -s - :type string: str - """ - if (parted_handle := self.raw_parted(string)).exit_code == 0: - return self.partprobe() - else: - raise DiskError(f"Parted failed to add a partition: {parted_handle}") - - def use_entire_disk(self, root_filesystem_type :str = 'ext4') -> Partition: - # TODO: Implement this with declarative profiles instead. - raise ValueError("Installation().use_entire_disk() has to be re-worked.") - - def add_partition( - self, - partition_type :str, - start :str, - end :str, - partition_format :Optional[str] = None, - skip_mklabel :bool = False - ) -> Partition: - log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO) - - if len(self.blockdevice.partitions) == 0 and skip_mklabel is False: - # If it's a completely empty drive, and we're about to add partitions to it - # we need to make sure there's a filesystem label. - if self.mode == GPT: - if not self.parted_mklabel(self.blockdevice.device, "gpt"): - raise KeyError(f"Could not create a GPT label on {self}") - elif self.mode == MBR: - if not self.parted_mklabel(self.blockdevice.device, "msdos"): - raise KeyError(f"Could not create a MS-DOS label on {self}") - - self.blockdevice.flush_cache() - - previous_partuuids = [] - for partition in self.blockdevice.partitions.values(): - try: - previous_partuuids.append(partition.part_uuid) - except DiskError: - pass - - # TODO this check should probably run in the setup process rather than during the installation - if self.mode == MBR: - if len(self.blockdevice.partitions) > 3: - DiskError("Too many partitions on disk, MBR disks can only have 3 primary partitions") - - if partition_format: - parted_string = f'{self.blockdevice.device} mkpart {partition_type} {partition_format} {start} {end}' - else: - parted_string = f'{self.blockdevice.device} mkpart {partition_type} {start} {end}' - - log(f"Adding partition using the following parted command: {parted_string}", level=logging.DEBUG) - - if self.parted(parted_string): - for count in range(storage.get('DISK_RETRY_ATTEMPTS', 3)): - self.blockdevice.flush_cache() - - new_partition_uuids = [partition.part_uuid for partition in self.blockdevice.partitions.values()] - new_partuuid_set = (set(previous_partuuids) ^ set(new_partition_uuids)) - - if len(new_partuuid_set) and (new_partuuid := new_partuuid_set.pop()): - try: - return self.blockdevice.get_partition(partuuid=new_partuuid) - except Exception as err: - log(f'Blockdevice: {self.blockdevice}', level=logging.ERROR, fg="red") - log(f'Partitions: {self.blockdevice.partitions}', level=logging.ERROR, fg="red") - log(f'Partition set: {new_partuuid_set}', level=logging.ERROR, fg="red") - log(f'New PARTUUID: {[new_partuuid]}', level=logging.ERROR, fg="red") - log(f'get_partition(): {self.blockdevice.get_partition}', level=logging.ERROR, fg="red") - raise err - else: - log(f"Could not get UUID for partition. Waiting {storage.get('DISK_TIMEOUTS', 1) * count}s before retrying.",level=logging.DEBUG) - self.partprobe() - time.sleep(max(0.1, storage.get('DISK_TIMEOUTS', 1))) - else: - print("Parted did not return True during partition creation") - - total_partitions = set([partition.part_uuid for partition in self.blockdevice.partitions.values()]) - total_partitions.update(previous_partuuids) - - # TODO: This should never be able to happen - log(f"Could not find the new PARTUUID after adding the partition.", level=logging.ERROR, fg="red") - log(f"Previous partitions: {previous_partuuids}", level=logging.ERROR, fg="red") - log(f"New partitions: {total_partitions}", level=logging.ERROR, fg="red") - - raise DiskError(f"Could not add partition using: {parted_string}") - - def set_name(self, partition: int, name: str) -> bool: - return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0 - - def set(self, partition: int, string: str) -> bool: - log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO) - return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0 - - def parted_mklabel(self, device: str, disk_label: str) -> bool: - log(f"Creating a new partition label on {device}", level=logging.INFO, fg="yellow") - # Try to unmount devices before attempting to run mklabel - try: - SysCommand(f'bash -c "umount {device}?"') - except: - pass - - self.partprobe() - worked = self.raw_parted(f'{device} mklabel {disk_label}').exit_code == 0 - self.partprobe() - - return worked diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py deleted file mode 100644 index 80d0cb53..00000000 --- a/archinstall/lib/disk/helpers.py +++ /dev/null @@ -1,556 +0,0 @@ -from __future__ import annotations -import json -import logging -import os # type: ignore -import pathlib -import re -import time -import glob - -from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING -# https://stackoverflow.com/a/39757388/929999 -from .diskinfo import get_lsblk_info -from ..models.subvolume import Subvolume - -from .blockdevice import BlockDevice -from .dmcryptdev import DMCryptDev -from .mapperdev import MapperDev -from ..exceptions import SysCallError, DiskError -from ..general import SysCommand -from ..output import log -from ..storage import storage - -if TYPE_CHECKING: - from .partition import Partition - - -ROOT_DIR_PATTERN = re.compile('^.*?/devices') -GIGA = 2 ** 30 - -def convert_size_to_gb(size :Union[int, float]) -> float: - return round(size / GIGA,1) - -def sort_block_devices_based_on_performance(block_devices :List[BlockDevice]) -> Dict[BlockDevice, int]: - result = {device: 0 for device in block_devices} - - for device, weight in result.items(): - if device.spinning: - weight -= 10 - else: - weight += 5 - - if device.bus_type == 'nvme': - weight += 20 - elif device.bus_type == 'sata': - weight += 10 - - result[device] = weight - - return result - -def filter_disks_below_size_in_gb(devices :List[BlockDevice], gigabytes :int) -> Iterator[BlockDevice]: - for disk in devices: - if disk.size >= gigabytes: - yield disk - -def select_largest_device(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice: - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - copy_devices = list(filter_disks_below_size_in_gb(copy_devices, gigabytes)) - - if not len(copy_devices): - return None - - return max(copy_devices, key=(lambda device : device.size)) - -def select_disk_larger_than_or_close_to(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice: - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - if not len(copy_devices): - return None - - return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) - -def convert_to_gigabytes(string :str) -> float: - unit = string.strip()[-1] - size = float(string.strip()[:-1]) - - if unit == 'M': - size = size / 1024 - elif unit == 'T': - size = size * 1024 - - return size - -def device_state(name :str, *args :str, **kwargs :str) -> Optional[bool]: - # Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709 - if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)): - with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f: - if f.read(1) == '1': - return - - path = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/block/{}'.format(name))) - hotplug_buses = ("usb", "ieee1394", "mmc", "pcmcia", "firewire") - for bus in hotplug_buses: - if os.path.exists('/sys/bus/{}'.format(bus)): - for device_bus in os.listdir('/sys/bus/{}/devices'.format(bus)): - device_link = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/bus/{}/devices/{}'.format(bus, device_bus))) - if re.search(device_link, path): - return - return True - - -def cleanup_bash_escapes(data :str) -> str: - return data.replace(r'\ ', ' ') - -def blkid(cmd :str) -> Dict[str, Any]: - if '-o' in cmd and '-o export' not in cmd: - raise ValueError(f"blkid() requires '-o export' to be used and can therefore not continue reliably.") - elif '-o' not in cmd: - cmd += ' -o export' - - try: - raw_data = SysCommand(cmd).decode() - except SysCallError as error: - log(f"Could not get block device information using blkid() using command {cmd}", level=logging.DEBUG) - raise error - - result = {} - # Process the raw result - devname = None - for line in raw_data.split('\r\n'): - if not len(line): - devname = None - continue - - key, val = line.split('=', 1) - if key.lower() == 'devname': - devname = val - # Lowercase for backwards compatibility with all_disks() previous use cases - result[devname] = { - "path": devname, - "PATH": devname - } - continue - - result[devname][key] = cleanup_bash_escapes(val) - - return result - -def get_loop_info(path :str) -> Dict[str, Any]: - for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: - if not drive['name'] == path: - continue - - return { - path: { - **drive, - 'type' : 'loop', - 'TYPE' : 'loop', - 'DEVTYPE' : 'loop', - 'PATH' : drive['name'], - 'path' : drive['name'] - } - } - - return {} - -def enrich_blockdevice_information(information :Dict[str, Any]) -> Dict[str, Any]: - result = {} - for device_path, device_information in information.items(): - dev_name = pathlib.Path(device_information['PATH']).name - if not device_information.get('TYPE') or not device_information.get('DEVTYPE'): - with open(f"/sys/class/block/{dev_name}/uevent") as fh: - device_information.update(uevent(fh.read())) - - if (dmcrypt_name := pathlib.Path(f"/sys/class/block/{dev_name}/dm/name")).exists(): - with dmcrypt_name.open('r') as fh: - device_information['DMCRYPT_NAME'] = fh.read().strip() - - result[device_path] = device_information - - return result - -def uevent(data :str) -> Dict[str, Any]: - information = {} - - for line in data.replace('\r\n', '\n').split('\n'): - if len((line := line.strip())): - key, val = line.split('=', 1) - information[key] = val - - return information - -def get_blockdevice_uevent(dev_name :str) -> Dict[str, Any]: - device_information = {} - with open(f"/sys/class/block/{dev_name}/uevent") as fh: - device_information.update(uevent(fh.read())) - - return { - f"/dev/{dev_name}" : { - **device_information, - 'path' : f'/dev/{dev_name}', - 'PATH' : f'/dev/{dev_name}', - 'PTTYPE' : None - } - } - - -def all_disks() -> List[BlockDevice]: - log(f"[Deprecated] archinstall.all_disks() is deprecated. Use archinstall.all_blockdevices() with the appropriate filters instead.", level=logging.WARNING, fg="yellow") - return all_blockdevices(partitions=False, mappers=False) - -def get_blockdevice_info(device_path, exclude_iso_dev :bool = True) -> Dict[str, Any]: - for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS']): - partprobe(device_path) - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * retry_attempt)) - - try: - if exclude_iso_dev: - # exclude all devices associated with the iso boot locations - iso_devs = ['/run/archiso/airootfs', '/run/archiso/bootmnt'] - - try: - lsblk_info = get_lsblk_info(device_path) - except DiskError: - continue - - if any([dev in lsblk_info.mountpoints for dev in iso_devs]): - continue - - information = blkid(f'blkid -p -o export {device_path}') - return enrich_blockdevice_information(information) - except SysCallError as ex: - if ex.exit_code == 2: - # Assume that it's a loop device, and try to get info on it - try: - resolved_device_name = device_path.readlink().name - except OSError: - resolved_device_name = device_path.name - - try: - information = get_loop_info(device_path) - if not information: - raise SysCallError(f"Could not get loop information for {resolved_device_name}", exit_code=1) - return enrich_blockdevice_information(information) - - except SysCallError: - information = get_blockdevice_uevent(resolved_device_name) - return enrich_blockdevice_information(information) - else: - # We could not reliably get any information, perhaps the disk is clean of information? - if retry_attempt == storage['DISK_RETRY_ATTEMPTS'] - 1: - raise ex - -def all_blockdevices( - mappers: bool = False, - partitions: bool = False, - error: bool = False, - exclude_iso_dev: bool = True -) -> Dict[str, Any]: - """ - Returns BlockDevice() and Partition() objects for all available devices. - """ - from .partition import Partition - - instances = {} - - # Due to lsblk being highly unreliable for this use case, - # we'll iterate the /sys/class definitions and find the information - # from there. - for block_device in glob.glob("/sys/class/block/*"): - try: - device_path = pathlib.Path(f"/dev/{pathlib.Path(block_device).readlink().name}") - except FileNotFoundError: - log(f"Unknown device found by '/sys/class/block/*', ignoring: {device_path}", level=logging.WARNING, fg="yellow") - - if device_path.exists() is False: - log(f"Unknown device found by '/sys/class/block/*', ignoring: {device_path}", level=logging.WARNING, fg="yellow") - continue - - information = get_blockdevice_info(device_path) - if not information: - continue - - for path, path_info in information.items(): - if path_info.get('DMCRYPT_NAME'): - instances[path] = DMCryptDev(dev_path=path) - elif path_info.get('PARTUUID') or path_info.get('PART_ENTRY_NUMBER'): - if partitions: - instances[path] = Partition(path, block_device=BlockDevice(get_parent_of_partition(pathlib.Path(path)))) - elif path_info.get('PTTYPE', False) is not False or path_info.get('TYPE') == 'loop': - instances[path] = BlockDevice(path, path_info) - elif path_info.get('TYPE') in ('squashfs', 'erofs'): - # We can ignore squashfs devices (usually /dev/loop0 on Arch ISO) - continue - else: - log(f"Unknown device found by all_blockdevices(), ignoring: {information}", level=logging.WARNING, fg="yellow") - - if mappers: - for block_device in glob.glob("/dev/mapper/*"): - if (pathobj := pathlib.Path(block_device)).is_symlink(): - instances[f"/dev/mapper/{pathobj.name}"] = MapperDev(mappername=pathobj.name) - - return instances - - -def get_parent_of_partition(path :pathlib.Path) -> pathlib.Path: - partition_name = path.name - pci_device = (pathlib.Path("/sys/class/block") / partition_name).resolve() - return f"/dev/{pci_device.parent.name}" - -def harddrive(size :Optional[float] = None, model :Optional[str] = None, fuzzy :bool = False) -> Optional[BlockDevice]: - collection = all_blockdevices(partitions=False) - for drive in collection: - if size and convert_to_gigabytes(collection[drive]['size']) != size: - continue - if model and (collection[drive]['model'] is None or collection[drive]['model'].lower() != model.lower()): - continue - - return collection[drive] - -def split_bind_name(path :Union[pathlib.Path, str]) -> list: - # log(f"[Deprecated] Partition().subvolumes now contain the split bind name via it's subvolume.name instead.", level=logging.WARNING, fg="yellow") - # we check for the bind notation. if exist we'll only use the "true" device path - if '[' in str(path) : # is a bind path (btrfs subvolume path) - device_path, bind_path = str(path).split('[') - bind_path = bind_path[:-1].strip() # remove the ] - else: - device_path = path - bind_path = None - return device_path,bind_path - -def find_mountpoint(device_path :str) -> Dict[str, Any]: - try: - for filesystem in json.loads(SysCommand(f'/usr/bin/findmnt -R --json {device_path}').decode())['filesystems']: - yield filesystem - except SysCallError: - return {} - -def findmnt(path :pathlib.Path, traverse :bool = False, ignore :List = [], recurse :bool = True) -> Dict[str, Any]: - for traversal in list(map(str, [str(path)] + list(path.parents))): - if traversal in ignore: - continue - - try: - log(f"Getting mount information for device path {traversal}", level=logging.DEBUG) - if (output := SysCommand(f"/usr/bin/findmnt --json {'--submounts' if recurse else ''} {traversal}").decode('UTF-8')): - return json.loads(output) - - except SysCallError as error: - log(f"Could not get mount information on {path} but continuing and ignoring: {error}", level=logging.INFO, fg="gray") - pass - - if not traverse: - break - - raise DiskError(f"Could not get mount information for path {path}") - - -def get_mount_info(path :Union[pathlib.Path, str], traverse :bool = False, return_real_path :bool = False, ignore :List = []) -> Dict[str, Any]: - import traceback - - log(f"Deprecated: archinstall.get_mount_info(). Use archinstall.findmnt() instead, which does not do any automatic parsing. Please change at:\n{''.join(traceback.format_stack())}") - device_path, bind_path = split_bind_name(path) - output = {} - - for traversal in list(map(str, [str(device_path)] + list(pathlib.Path(str(device_path)).parents))): - if traversal in ignore: - continue - - try: - log(f"Getting mount information for device path {traversal}", level=logging.DEBUG) - if (output := SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8')): - break - - except SysCallError as error: - print('ERROR:', error) - pass - - if not traverse: - break - - if not output: - raise DiskError(f"Could not get mount information for device path {device_path}") - - output = json.loads(output) - - # for btrfs partitions we redice the filesystem list to the one with the source equals to the parameter - # i.e. the subvolume filesystem we're searching for - if 'filesystems' in output and len(output['filesystems']) > 1 and bind_path is not None: - output['filesystems'] = [entry for entry in output['filesystems'] if entry['source'] == str(path)] - - if 'filesystems' in output: - if len(output['filesystems']) > 1: - raise DiskError(f"Path '{device_path}' contains multiple mountpoints: {output['filesystems']}") - - if return_real_path: - return output['filesystems'][0], traversal - else: - return output['filesystems'][0] - - if return_real_path: - return {}, traversal - else: - return {} - - -def get_all_targets(data :Dict[str, Any], filters :Dict[str, None] = {}) -> Dict[str, None]: - for info in data: - if info.get('target') not in filters: - filters[info.get('target')] = None - - filters.update(get_all_targets(info.get('children', []))) - - return filters - -def get_partitions_in_use(mountpoint :str) -> Dict[str, Any]: - from .partition import Partition - - try: - output = SysCommand(f"/usr/bin/findmnt --json -R {mountpoint}").decode('UTF-8') - except SysCallError: - return {} - - if not output: - return {} - - output = json.loads(output) - - mounts = {} - - block_devices_available = all_blockdevices(mappers=True, partitions=True, error=True) - - block_devices_mountpoints = {} - for blockdev in block_devices_available.values(): - if not type(blockdev) in (Partition, MapperDev): - continue - - if isinstance(blockdev, Partition): - if blockdev.mountpoints: - for blockdev_mountpoint in blockdev.mountpoints: - block_devices_mountpoints[blockdev_mountpoint] = blockdev - else: - if blockdev.mount_information: - for blockdev_mountpoint in blockdev.mount_information: - block_devices_mountpoints[blockdev_mountpoint['target']] = blockdev - - log(f'Filtering available mounts {block_devices_mountpoints} to those under {mountpoint}', level=logging.DEBUG) - - for mountpoint in list(get_all_targets(output['filesystems']).keys()): - # Since all_blockdevices() returns PosixPath objects, we need to convert - # findmnt paths to pathlib.Path() first: - mountpoint = pathlib.Path(mountpoint) - - if mountpoint in block_devices_mountpoints: - if mountpoint not in mounts: - mounts[mountpoint] = block_devices_mountpoints[mountpoint] - # If the already defined mountpoint is a DMCryptDev, and the newly found - # mountpoint is a MapperDev, it has precedence and replaces the old mountpoint definition. - elif type(mounts[mountpoint]) == DMCryptDev and type(block_devices_mountpoints[mountpoint]) == MapperDev: - mounts[mountpoint] = block_devices_mountpoints[mountpoint] - - log(f"Available partitions: {mounts}", level=logging.DEBUG) - - return mounts - - -def get_filesystem_type(path :str) -> Optional[str]: - try: - return SysCommand(f"blkid -o value -s TYPE {path}").decode('UTF-8').strip() - except SysCallError: - return None - - -def disk_layouts() -> Optional[Dict[str, Any]]: - try: - if (handle := SysCommand("lsblk -f -o+TYPE,SIZE -J")).exit_code == 0: - return {str(key): val for key, val in json.loads(handle.decode('UTF-8')).items()} - else: - log(f"Could not return disk layouts: {handle}", level=logging.WARNING, fg="yellow") - return None - except SysCallError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") - return None - except json.decoder.JSONDecodeError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") - return None - - -def find_partition_by_mountpoint(block_devices :List[BlockDevice], relative_mountpoint :str) -> Partition: - for device in block_devices: - for partition in block_devices[device]['partitions']: - if partition.get('mountpoint', None) == relative_mountpoint: - return partition - -def partprobe(path :str = '') -> bool: - try: - if SysCommand(f'bash -c "partprobe {path}"').exit_code == 0: - return True - except SysCallError: - pass - return False - -def convert_device_to_uuid(path :str) -> str: - device_name, bind_name = split_bind_name(path) - - for i in range(storage['DISK_RETRY_ATTEMPTS']): - partprobe(device_name) - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) # TODO: Remove, we should be relying on blkid instead of lsblk - - # TODO: Convert lsblk to blkid - # (lsblk supports BlockDev and Partition UUID grabbing, blkid requires you to pick PTUUID and PARTUUID) - output = json.loads(SysCommand(f"lsblk --json -o+UUID {device_name}").decode('UTF-8')) - - for device in output['blockdevices']: - if (dev_uuid := device.get('uuid', None)): - return dev_uuid - - raise DiskError(f"Could not retrieve the UUID of {path} within a timely manner.") - - -def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, strict: bool = True) -> bool: - """ Determine if a certain partition is mounted (or has a mountpoint) as specific target (path) - Coded for clarity rather than performance - - Input parms: - :parm partition the partition we check - :type Either a Partition object or a dict with the contents of a partition definition in the disk_layouts schema - - :parm target (a string representing a mount path we want to check for. - :type str - - :parm strict if the check will be strict, target is exactly the mountpoint, or no, where the target is a leaf (f.i. to check if it is in /mnt/archinstall/). Not available for root check ('/') for obvious reasons - - """ - # we create the mountpoint list - if isinstance(partition,dict): - subvolumes: List[Subvolume] = partition.get('btrfs',{}).get('subvolumes', []) - mountpoints = [partition.get('mountpoint')] - mountpoints += [volume.mountpoint for volume in subvolumes] - else: - mountpoints = [partition.mountpoint,] + [subvol.target for subvol in partition.subvolumes] - - # we check - if strict or target == '/': - if target in mountpoints: - return True - else: - return False - else: - for mp in mountpoints: - if mp and mp.endswith(target): - return True - return False diff --git a/archinstall/lib/disk/mapperdev.py b/archinstall/lib/disk/mapperdev.py deleted file mode 100644 index bf1b3583..00000000 --- a/archinstall/lib/disk/mapperdev.py +++ /dev/null @@ -1,92 +0,0 @@ -import glob -import pathlib -import logging -import json -from dataclasses import dataclass -from typing import Optional, List, Dict, Any, Iterator, TYPE_CHECKING - -from ..exceptions import SysCallError -from ..general import SysCommand -from ..output import log - -if TYPE_CHECKING: - from .btrfs import BtrfsSubvolumeInfo - -@dataclass -class MapperDev: - mappername :str - - @property - def name(self): - return self.mappername - - @property - def path(self): - return f"/dev/mapper/{self.mappername}" - - @property - def part_uuid(self): - return self.partition.part_uuid - - @property - def partition(self): - from .helpers import uevent, get_parent_of_partition - from .partition import Partition - from .blockdevice import BlockDevice - - for mapper in glob.glob('/dev/mapper/*'): - path_obj = pathlib.Path(mapper) - if path_obj.name == self.mappername and pathlib.Path(mapper).is_symlink(): - dm_device = (pathlib.Path("/dev/mapper/") / path_obj.readlink()).resolve() - - for slave in glob.glob(f"/sys/class/block/{dm_device.name}/slaves/*"): - partition_belonging_to_dmcrypt_device = pathlib.Path(slave).name - - try: - uevent_data = SysCommand(f"blkid -o export /dev/{partition_belonging_to_dmcrypt_device}").decode() - except SysCallError as error: - log(f"Could not get information on device /dev/{partition_belonging_to_dmcrypt_device}: {error}", level=logging.ERROR, fg="red") - - information = uevent(uevent_data) - block_device = BlockDevice(get_parent_of_partition('/dev/' / pathlib.Path(information['DEVNAME']))) - - return Partition(information['DEVNAME'], block_device=block_device) - - raise ValueError(f"Could not convert {self.mappername} to a real dm-crypt device") - - @property - def mountpoint(self) -> Optional[pathlib.Path]: - try: - data = json.loads(SysCommand(f"findmnt --json -R {self.path}").decode()) - for filesystem in data['filesystems']: - return pathlib.Path(filesystem.get('target')) - - except SysCallError as error: - # Not mounted anywhere most likely - log(f"Could not locate mount information for {self.path}: {error}", level=logging.WARNING, fg="yellow") - pass - - return None - - @property - def mountpoints(self) -> List[Dict[str, Any]]: - return [obj['target'] for obj in self.mount_information] - - @property - def mount_information(self) -> List[Dict[str, Any]]: - from .helpers import find_mountpoint - return [{**obj, 'target' : pathlib.Path(obj.get('target', '/dev/null'))} for obj in find_mountpoint(self.path)] - - @property - def filesystem(self) -> Optional[str]: - from .helpers import get_filesystem_type - return get_filesystem_type(self.path) - - @property - def subvolumes(self) -> Iterator['BtrfsSubvolumeInfo']: - from .btrfs import subvolume_info_from_path - - for mountpoint in self.mount_information: - if target := mountpoint.get('target'): - if subvolume := subvolume_info_from_path(pathlib.Path(target)): - yield subvolume diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py deleted file mode 100644 index 87eaa6a7..00000000 --- a/archinstall/lib/disk/partition.py +++ /dev/null @@ -1,661 +0,0 @@ -import glob -import time -import logging -import json -import os -import hashlib -import typing -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional, Dict, Any, List, Union, Iterator - -from .blockdevice import BlockDevice -from .helpers import get_filesystem_type, convert_size_to_gb, split_bind_name -from ..storage import storage -from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat -from ..output import log -from ..general import SysCommand -from .btrfs.btrfs_helpers import subvolume_info_from_path -from .btrfs.btrfssubvolumeinfo import BtrfsSubvolumeInfo - -@dataclass -class PartitionInfo: - partition_object: 'Partition' - device_path: str # This would be /dev/sda1 for instance - bootable: bool - size: float - sector_size: int - start: Optional[int] - end: Optional[int] - pttype: Optional[str] - filesystem_type: Optional[str] - partuuid: Optional[str] - uuid: Optional[str] - mountpoints: List[Path] = field(default_factory=list) - - def __post_init__(self): - if not all([self.partuuid, self.uuid]): - for i in range(storage['DISK_RETRY_ATTEMPTS']): - lsblk_info = SysCommand(f"lsblk --json -b -o+LOG-SEC,SIZE,PTTYPE,PARTUUID,UUID,FSTYPE {self.device_path}").decode('UTF-8') - try: - lsblk_info = json.loads(lsblk_info) - except json.decoder.JSONDecodeError: - log(f"Could not decode JSON: {lsblk_info}", fg="red", level=logging.ERROR) - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - if not (device := lsblk_info.get('blockdevices', [None])[0]): - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - self.partuuid = device.get('partuuid') - self.uuid = device.get('uuid') - - # Lets build a list of requirements that we would like - # to retry and build (stuff that can take time between partprobes) - requirements = [] - requirements.append(self.partuuid) - - # Unformatted partitions won't have a UUID - if lsblk_info.get('fstype') is not None: - requirements.append(self.uuid) - - if all(requirements): - break - - self.partition_object.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) - - def get_first_mountpoint(self) -> Optional[Path]: - if len(self.mountpoints) > 0: - return self.mountpoints[0] - return None - - -class Partition: - def __init__( - self, - path: str, - block_device: BlockDevice, - part_id :Optional[str] = None, - filesystem :Optional[str] = None, - mountpoint :Optional[str] = None, - encrypted :bool = False, - autodetect_filesystem :bool = True, - ): - if not part_id: - part_id = os.path.basename(path) - - if type(block_device) is str: - raise ValueError(f"Partition()'s 'block_device' parameter has to be a archinstall.BlockDevice() instance!") - - self.block_device = block_device - self._path = path - self._part_id = part_id - self._target_mountpoint = mountpoint - self._encrypted = encrypted - self._wipe = False - self._type = 'primary' - - if mountpoint: - self.mount(mountpoint) - - try: - self._partition_info = self._fetch_information() - - if not autodetect_filesystem and filesystem: - self._partition_info.filesystem_type = filesystem - - if self._partition_info.filesystem_type == 'crypto_LUKS': - self._encrypted = True - except DiskError: - self._partition_info = None - - @typing.no_type_check # I hate doint this but I'm currently unsure where this is used. - def __lt__(self, left_comparitor :BlockDevice) -> bool: - if type(left_comparitor) == Partition: - left_comparitor = left_comparitor.path - else: - left_comparitor = str(left_comparitor) - - # The goal is to check if /dev/nvme0n1p1 comes before /dev/nvme0n1p5 - return self._path < left_comparitor - - def __repr__(self, *args :str, **kwargs :str) -> str: - mount_repr = '' - if self._partition_info: - if mountpoint := self._partition_info.get_first_mountpoint(): - mount_repr = f", mounted={mountpoint}" - elif self._target_mountpoint: - mount_repr = f", rel_mountpoint={self._target_mountpoint}" - - classname = self.__class__.__name__ - - if not self._partition_info: - return f'{classname}(path={self._path})' - elif self._encrypted: - return f'{classname}(path={self._path}, size={self.size}, PARTUUID={self.part_uuid}, parent={self.real_device}, fs={self._partition_info.filesystem_type}{mount_repr})' - else: - return f'{classname}(path={self._path}, size={self.size}, PARTUUID={self.part_uuid}, fs={self._partition_info.filesystem_type}{mount_repr})' - - def as_json(self) -> Dict[str, Any]: - """ - this is used for the table representation of the partition (see FormattedOutput) - """ - partition_info = { - 'type': self._type, - 'PARTUUID': self.part_uuid, - 'wipe': self._wipe, - 'boot': self.boot, - 'ESP': self.boot, - 'mountpoint': self._target_mountpoint, - 'encrypted': self._encrypted, - 'start': self.start, - 'size': self.end, - 'filesystem': self._partition_info.filesystem_type if self._partition_info else 'Unknown' - } - - return partition_info - - def __dump__(self) -> Dict[str, Any]: - # TODO remove this in favour of as_json - return { - 'type': self._type, - 'PARTUUID': self.part_uuid, - 'wipe': self._wipe, - 'boot': self.boot, - 'ESP': self.boot, - 'mountpoint': self._target_mountpoint, - 'encrypted': self._encrypted, - 'start': self.start, - 'size': self.end, - 'filesystem': { - 'format': self._partition_info.filesystem_type if self._partition_info else 'None' - } - } - - def _call_lsblk(self) -> Dict[str, Any]: - for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS']): - self.partprobe() - time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * retry_attempt)) # TODO: Remove, we should be relying on blkid instead of lsblk - # This sleep might be overkill, but lsblk is known to - # work against a chaotic cache that can change during call - # causing no information to be returned (blkid is better) - # time.sleep(1) - - # TODO: Maybe incorporate a re-try system here based on time.sleep(max(0.1, storage.get('DISK_TIMEOUTS', 1))) - - try: - output = SysCommand(f"lsblk --json -b -o+LOG-SEC,SIZE,PTTYPE,PARTUUID,UUID,FSTYPE {self.device_path}").decode('UTF-8') - except SysCallError as error: - # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - output = error.worker.decode('UTF-8') - if '{' in output: - output = output[output.find('{'):] - - if output: - try: - lsblk_info = json.loads(output) - return lsblk_info - except json.decoder.JSONDecodeError: - log(f"Could not decode JSON: {output}", fg="red", level=logging.ERROR) - - raise DiskError(f'Failed to get partition information "{self.device_path}" with lsblk') - - def _call_sfdisk(self) -> Dict[str, Any]: - output = SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8') - - if output: - sfdisk_info = json.loads(output) - partitions = sfdisk_info.get('partitiontable', {}).get('partitions', []) - node = list(filter(lambda x: x['node'] == self._path, partitions)) - - if len(node) > 0: - return node[0] - - return {} - - raise DiskError(f'Failed to read disk "{self.block_device.path}" with sfdisk') - - def _fetch_information(self) -> PartitionInfo: - lsblk_info = self._call_lsblk() - sfdisk_info = self._call_sfdisk() - - if not (device := lsblk_info.get('blockdevices', [])): - raise DiskError(f'Failed to retrieve information for "{self.device_path}" with lsblk') - - # Grab the first (and only) block device in the list as we're targeting a specific partition - device = device[0] - - mountpoints = [Path(mountpoint) for mountpoint in device['mountpoints'] if mountpoint] - bootable = sfdisk_info.get('bootable', False) or sfdisk_info.get('type', '') == 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' - - return PartitionInfo( - partition_object=self, - device_path=self._path, - pttype=device['pttype'], - partuuid=device['partuuid'], - uuid=device['uuid'], - sector_size=device['log-sec'], - size=convert_size_to_gb(device['size']), - start=sfdisk_info.get('start', None), - end=sfdisk_info.get('size', None), - bootable=bootable, - filesystem_type=device['fstype'], - mountpoints=mountpoints - ) - - @property - def target_mountpoint(self) -> Optional[str]: - return self._target_mountpoint - - @property - def path(self) -> str: - return self._path - - @property - def filesystem(self) -> str: - if self._partition_info: - return self._partition_info.filesystem_type - - @property - def mountpoint(self) -> Optional[Path]: - if len(self.mountpoints) > 0: - return self.mountpoints[0] - return None - - @property - def mountpoints(self) -> List[Path]: - if self._partition_info: - return self._partition_info.mountpoints - - @property - def sector_size(self) -> int: - if self._partition_info: - return self._partition_info.sector_size - - @property - def start(self) -> Optional[int]: - if self._partition_info: - return self._partition_info.start - - @property - def end(self) -> Optional[int]: - if self._partition_info: - return self._partition_info.end - - @property - def end_sectors(self) -> Optional[int]: - if self._partition_info: - start = self._partition_info.start - end = self._partition_info.end - if start and end: - return start + end - - @property - def size(self) -> Optional[float]: - if self._partition_info: - return self._partition_info.size - - @property - def boot(self) -> bool: - if self._partition_info: - return self._partition_info.bootable - - @property - def partition_type(self) -> Optional[str]: - if self._partition_info: - return self._partition_info.pttype - - @property - def part_uuid(self) -> str: - if self._partition_info: - return self._partition_info.partuuid - - @property - def uuid(self) -> Optional[str]: - """ - Returns the UUID as returned by lsblk for the **partition**. - This is more reliable than relying on /dev/disk/by-uuid as - it doesn't seam to be able to detect md raid partitions. - For bind mounts all the subvolumes share the same uuid - """ - for i in range(storage['DISK_RETRY_ATTEMPTS']): - if not self.partprobe(): - raise DiskError(f"Could not perform partprobe on {self.device_path}") - - time.sleep(storage.get('DISK_TIMEOUTS', 1) * i) - - partuuid = self._safe_uuid - if partuuid: - return partuuid - - raise DiskError(f"Could not get PARTUUID for {self.path} using 'blkid -s PARTUUID -o value {self.path}'") - - @property - def _safe_uuid(self) -> Optional[str]: - """ - A near copy of self.uuid but without any delays. - This function should only be used where uuid is not crucial. - For instance when you want to get a __repr__ of the class. - """ - if not self.partprobe(): - if self.block_device.partition_type == 'iso9660': - return None - - log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) - - try: - return SysCommand(f'blkid -s UUID -o value {self.device_path}').decode('UTF-8').strip() - except SysCallError as error: - if self.block_device.partition_type == 'iso9660': - # Parent device is a Optical Disk (.iso dd'ed onto a device for instance) - return None - - log(f"Could not get PARTUUID of partition using 'blkid -s UUID -o value {self.device_path}': {error}") - - @property - def _safe_part_uuid(self) -> Optional[str]: - """ - A near copy of self.uuid but without any delays. - This function should only be used where uuid is not crucial. - For instance when you want to get a __repr__ of the class. - """ - if not self.partprobe(): - if self.block_device.partition_type == 'iso9660': - return None - - log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) - - try: - return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip() - except SysCallError as error: - if self.block_device.partition_type == 'iso9660': - # Parent device is a Optical Disk (.iso dd'ed onto a device for instance) - return None - - log(f"Could not get PARTUUID of partition using 'blkid -s PARTUUID -o value {self.device_path}': {error}") - - if self._partition_info: - return self._partition_info.uuid - - @property - def encrypted(self) -> Union[bool, None]: - return self._encrypted - - @property - def parent(self) -> str: - return self.real_device - - @property - def real_device(self) -> str: - output = SysCommand('lsblk -J').decode('UTF-8') - - if output: - for blockdevice in json.loads(output)['blockdevices']: - if parent := self.find_parent_of(blockdevice, os.path.basename(self.device_path)): - return f"/dev/{parent}" - return self._path - - raise DiskError('Unable to get disk information for command "lsblk -J"') - - @property - def device_path(self) -> str: - """ for bind mounts returns the physical path of the partition - """ - device_path, bind_name = split_bind_name(self._path) - return device_path - - @property - def bind_name(self) -> str: - """ for bind mounts returns the bind name (subvolume path). - Returns none if this property does not exist - """ - device_path, bind_name = split_bind_name(self._path) - return bind_name - - @property - def subvolumes(self) -> Iterator[BtrfsSubvolumeInfo]: - from .helpers import findmnt - - def iterate_children_recursively(information): - for child in information.get('children', []): - if target := child.get('target'): - if child.get('fstype') == 'btrfs': - if subvolume := subvolume_info_from_path(Path(target)): - yield subvolume - - if child.get('children'): - for subchild in iterate_children_recursively(child): - yield subchild - - if self._partition_info.filesystem_type == 'btrfs': - for mountpoint in self._partition_info.mountpoints: - if result := findmnt(mountpoint): - for filesystem in result.get('filesystems', []): - if subvolume := subvolume_info_from_path(mountpoint): - yield subvolume - - for child in iterate_children_recursively(filesystem): - yield child - - def partprobe(self) -> bool: - try: - if self.block_device: - return 0 == SysCommand(f'partprobe {self.block_device.device}').exit_code - except SysCallError as error: - log(f"Unreliable results might be given for {self._path} due to partprobe error: {error}", level=logging.DEBUG) - - return False - - def detect_inner_filesystem(self, password :str) -> Optional[str]: - log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) - from ..luks import luks2 - - try: - with luks2(self, storage.get('ENC_IDENTIFIER', 'ai') + 'loop', password, auto_unmount=True) as unlocked_device: - return unlocked_device.filesystem - except SysCallError: - pass - return None - - def has_content(self) -> bool: - fs_type = self._partition_info.filesystem_type - if not fs_type or "swap" in fs_type: - return False - - temporary_mountpoint = '/tmp/' + hashlib.md5(bytes(f"{time.time()}", 'UTF-8') + os.urandom(12)).hexdigest() - temporary_path = Path(temporary_mountpoint) - - temporary_path.mkdir(parents=True, exist_ok=True) - if (handle := SysCommand(f'/usr/bin/mount {self._path} {temporary_mountpoint}')).exit_code != 0: - raise DiskError(f'Could not mount and check for content on {self._path} because: {handle}') - - files = len(glob.glob(f"{temporary_mountpoint}/*")) - iterations = 0 - while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations + 1) < 10: - time.sleep(1) - - temporary_path.rmdir() - - return True if files > 0 else False - - def encrypt(self, password: Optional[str] = None) -> str: - """ - A wrapper function for luks2() instances and the .encrypt() method of that instance. - """ - from ..luks import luks2 - - handle = luks2(self, None, None) - return handle.encrypt(self, password=password) - - def format(self, filesystem :Optional[str] = None, path :Optional[str] = None, log_formatting :bool = True, options :List[str] = [], retry :bool = True) -> bool: - """ - Format can be given an overriding path, for instance /dev/null to test - the formatting functionality and in essence the support for the given filesystem. - """ - if filesystem is None: - filesystem = self._partition_info.filesystem_type - - if path is None: - path = self._path - - # This converts from fat32 -> vfat to unify filesystem names - filesystem = get_mount_fs_type(filesystem) - - # To avoid "unable to open /dev/x: No such file or directory" - start_wait = time.time() - while Path(path).exists() is False and time.time() - start_wait < 10: - time.sleep(0.025) - - if log_formatting: - log(f'Formatting {path} -> {filesystem}', level=logging.INFO) - - try: - if filesystem == 'btrfs': - options = ['-f'] + options - - mkfs = SysCommand(f"/usr/bin/mkfs.btrfs {' '.join(options)} {path}").decode('UTF-8') - if mkfs and 'UUID:' not in mkfs: - raise DiskError(f'Could not format {path} with {filesystem} because: {mkfs}') - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'vfat': - options = ['-F32'] + options - log(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}") - if (handle := SysCommand(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ext4': - options = ['-F'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ext4 {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ext2': - options = ['-F'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ext2 {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = 'ext2' - elif filesystem == 'xfs': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.xfs {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'f2fs': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.f2fs {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'ntfs3': - options = ['-f'] + options - - if (handle := SysCommand(f"/usr/bin/mkfs.ntfs -Q {' '.join(options)} {path}")).exit_code != 0: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self._partition_info.filesystem_type = filesystem - - elif filesystem == 'crypto_LUKS': - # from ..luks import luks2 - # encrypted_partition = luks2(self, None, None) - # encrypted_partition.format(path) - self._partition_info.filesystem_type = filesystem - - else: - raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.") - except SysCallError as error: - log(f"Formatting ran in to an error: {error}", level=logging.WARNING, fg="orange") - if retry is True: - log(f"Retrying in {storage.get('DISK_TIMEOUTS', 1)} seconds.", level=logging.WARNING, fg="orange") - time.sleep(storage.get('DISK_TIMEOUTS', 1)) - - return self.format(filesystem, path, log_formatting, options, retry=False) - - if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS': - self._encrypted = True - else: - self._encrypted = False - - return True - - def find_parent_of(self, data :Dict[str, Any], name :str, parent :Optional[str] = None) -> Optional[str]: - if data['name'] == name: - return parent - elif 'children' in data: - for child in data['children']: - if parent := self.find_parent_of(child, name, parent=data['name']): - return parent - - return None - - def mount(self, target :str, fs :Optional[str] = None, options :str = '') -> bool: - if not self._partition_info.get_first_mountpoint(): - log(f'Mounting {self} to {target}', level=logging.INFO) - - if not fs: - fs = self._partition_info.filesystem_type - - fs_type = get_mount_fs_type(fs) - - Path(target).mkdir(parents=True, exist_ok=True) - - if self.bind_name: - device_path = self.device_path - # TODO options should be better be a list than a string - if options: - options = f"{options},subvol={self.bind_name}" - else: - options = f"subvol={self.bind_name}" - else: - device_path = self._path - try: - if options: - mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} -o {options} {device_path} {target}") - else: - mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} {device_path} {target}") - - # TODO: Should be redundant to check for exit_code - if mnt_handle.exit_code != 0: - raise DiskError(f"Could not mount {self._path} to {target} using options {options}") - except SysCallError as err: - raise err - - # Update the partition info since the mount info has changed after this call. - self._partition_info = self._fetch_information() - return True - - return False - - def unmount(self) -> bool: - SysCommand(f"/usr/bin/umount {self._path}") - - # Update the partition info since the mount info has changed after this call. - self._partition_info = self._fetch_information() - return True - - def filesystem_supported(self) -> bool: - """ - The support for a filesystem (this partition) is tested by calling - partition.format() with a path set to '/dev/null' which returns two exceptions: - 1. SysCallError saying that /dev/null is not formattable - but the filesystem is supported - 2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type - """ - try: - self.format(self._partition_info.filesystem_type, '/dev/null', log_formatting=False) - except (SysCallError, DiskError): - pass # We supported it, but /dev/null is not formattable as expected so the mkfs call exited with an error code - except UnknownFilesystemFormat as err: - raise err - return True - - -def get_mount_fs_type(fs :str) -> str: - if fs == 'ntfs': - return 'ntfs3' # Needed to use the Paragon R/W NTFS driver - elif fs == 'fat32': - return 'vfat' # This is the actual type used for fat32 mounting - return fs diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py new file mode 100644 index 00000000..686e8c29 --- /dev/null +++ b/archinstall/lib/disk/partitioning_menu.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple + +from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ + ModificationStatus +from ..menu import Menu, ListManager, MenuSelection, TextInput +from ..output import FormattedOutput, log +from .subvolume_menu import SubvolumeMenu + +if TYPE_CHECKING: + _: Any + + +class PartitioningList(ListManager): + """ + subclass of ListManager for the managing of user accounts + """ + def __init__(self, prompt: str, device: BDevice, device_partitions: List[PartitionModification]): + self._device = device + self._actions = { + 'create_new_partition': str(_('Create a new partition')), + 'suggest_partition_layout': str(_('Suggest partition layout')), + 'remove_added_partitions': str(_('Remove all newly added partitions')), + 'assign_mountpoint': str(_('Assign mountpoint')), + 'mark_formatting': str(_('Mark/Unmark to be formatted (wipes data)')), + 'mark_bootable': str(_('Mark/Unmark as bootable')), + 'set_filesystem': str(_('Change filesystem')), + 'btrfs_mark_compressed': str(_('Mark/Unmark as compressed')), # btrfs only + 'btrfs_set_subvolumes': str(_('Set subvolumes')), # btrfs only + 'delete_partition': str(_('Delete partition')) + } + + display_actions = list(self._actions.values()) + super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:]) + + def reformat(self, data: List[PartitionModification]) -> Dict[str, Optional[PartitionModification]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[PartitionModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, partition: PartitionModification) -> str: + return str(_('Partition')) + + def filter_options(self, selection: PartitionModification, options: List[str]) -> List[str]: + not_filter = [] + + # only display formatting if the partition exists already + if not selection.exists(): + not_filter += [self._actions['mark_formatting']] + else: + # only allow these options if the existing partition + # was marked as formatting, otherwise we run into issues where + # 1. select a new fs -> potentially mark as wipe now + # 2. Switch back to old filesystem -> should unmark wipe now, but + # how do we know it was the original one? + not_filter += [ + self._actions['set_filesystem'], + self._actions['assign_mountpoint'], + self._actions['mark_bootable'], + self._actions['btrfs_mark_compressed'], + self._actions['btrfs_set_subvolumes'] + ] + + # non btrfs partitions shouldn't get btrfs options + if selection.fs_type != FilesystemType.Btrfs: + not_filter += [self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes']] + else: + not_filter += [self._actions['assign_mountpoint']] + + return [o for o in options if o not in not_filter] + + def handle_action( + self, + action: str, + entry: Optional[PartitionModification], + data: List[PartitionModification] + ) -> List[PartitionModification]: + action_key = [k for k, v in self._actions.items() if v == action][0] + + match action_key: + case 'create_new_partition': + new_partition = self._create_new_partition() + data += [new_partition] + case 'suggest_partition_layout': + new_partitions = self._suggest_partition_layout(data) + if len(new_partitions) > 0: + data = new_partitions + case 'remove_added_partitions': + choice = self._reset_confirmation() + if choice.value == Menu.yes(): + data = [part for part in data if part.is_exists_or_modify()] + case 'assign_mountpoint' if entry: + entry.mountpoint = self._prompt_mountpoint() + if entry.mountpoint == Path('/boot'): + entry.set_flag(PartitionFlag.Boot) + case 'mark_formatting' if entry: + self._prompt_formatting(entry) + case 'mark_bootable' if entry: + entry.invert_flag(PartitionFlag.Boot) + case 'set_filesystem' if entry: + fs_type = self._prompt_partition_fs_type() + if fs_type: + entry.fs_type = fs_type + # btrfs subvolumes will define mountpoints + if fs_type == FilesystemType.Btrfs: + entry.mountpoint = None + case 'btrfs_mark_compressed' if entry: + self._set_compressed(entry) + case 'btrfs_set_subvolumes' if entry: + self._set_btrfs_subvolumes(entry) + case 'delete_partition' if entry: + data = self._delete_partition(entry, data) + + return data + + def _delete_partition( + self, + entry: PartitionModification, + data: List[PartitionModification] + ) -> List[PartitionModification]: + if entry.is_exists_or_modify(): + entry.status = ModificationStatus.Delete + return data + else: + return [d for d in data if d != entry] + + def _set_compressed(self, partition: PartitionModification): + compression = 'compress=zstd' + + if compression in partition.mount_options: + partition.mount_options = [o for o in partition.mount_options if o != compression] + else: + partition.mount_options.append(compression) + + def _set_btrfs_subvolumes(self, partition: PartitionModification): + partition.btrfs_subvols = SubvolumeMenu( + _("Manage btrfs subvolumes for current partition"), + partition.btrfs_subvols + ).run() + + def _prompt_formatting(self, partition: PartitionModification): + # an existing partition can toggle between Exist or Modify + if partition.is_modify(): + partition.status = ModificationStatus.Exist + return + elif partition.exists(): + partition.status = ModificationStatus.Modify + + # If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really + # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set, + # it's safe to change the filesystem for this partition. + if partition.fs_type == FilesystemType.Crypto_luks: + prompt = str(_('This partition is currently encrypted, to format it a filesystem has to be specified')) + fs_type = self._prompt_partition_fs_type(prompt) + partition.fs_type = fs_type + + if fs_type == FilesystemType.Btrfs: + partition.mountpoint = None + + def _prompt_mountpoint(self) -> Path: + header = str(_('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) + '\n' + header += str(_('If mountpoint /boot is set, then the partition will also be marked as bootable.')) + '\n' + prompt = str(_('Mountpoint: ')) + + print(header) + + while True: + value = TextInput(prompt).run().strip() + + if value: + mountpoint = Path(value) + break + + return mountpoint + + def _prompt_partition_fs_type(self, prompt: str = '') -> FilesystemType: + options = {fs.value: fs for fs in FilesystemType if fs != FilesystemType.Crypto_luks} + + prompt = prompt + '\n' + str(_('Enter a desired filesystem type for the partition')) + choice = Menu(prompt, options, sort=False, skip=False).run() + return options[choice.single_value] + + def _validate_sector(self, start_sector: str, end_sector: Optional[str] = None) -> bool: + if not start_sector.isdigit(): + return False + + if end_sector: + if end_sector.endswith('%'): + if not end_sector[:-1].isdigit(): + return False + elif not end_sector.isdigit(): + return False + elif int(start_sector) > int(end_sector): + return False + + return True + + def _prompt_sectors(self) -> Tuple[Size, Size]: + device_info = self._device.device_info + + text = str(_('Current free sectors on device {}:')).format(device_info.path) + '\n\n' + free_space_table = FormattedOutput.as_table(device_info.free_space_regions) + prompt = text + free_space_table + '\n' + + total_sectors = device_info.total_size.format_size(Unit.sectors, device_info.sector_size) + prompt += str(_('Total sectors: {}')).format(total_sectors) + '\n' + print(prompt) + + largest_free_area = max(device_info.free_space_regions, key=lambda r: r.get_length()) + + # prompt until a valid start sector was entered + while True: + start_prompt = str(_('Enter the start sector (default: {}): ')).format(largest_free_area.start) + start_sector = TextInput(start_prompt).run().strip() + + if not start_sector or self._validate_sector(start_sector): + break + + log(f'Invalid start sector entered: {start_sector}', fg='red', level=logging.INFO) + + if not start_sector: + start_sector = str(largest_free_area.start) + end_sector = str(largest_free_area.end) + else: + end_sector = '100%' + + # prompt until valid end sector was entered + while True: + end_prompt = str(_('Enter the end sector of the partition (percentage or block number, default: {}): ')).format(end_sector) + end_value = TextInput(end_prompt).run().strip() + + if not end_value or self._validate_sector(start_sector, end_value): + break + + log(f'Invalid end sector entered: {start_sector}', fg='red', level=logging.INFO) + + # override the default value with the user value + if end_value: + end_sector = end_value + + start_size = Size(int(start_sector), Unit.sectors, device_info.sector_size) + + if end_sector.endswith('%'): + end_size = Size(int(end_sector[:-1]), Unit.Percent, device_info.sector_size, device_info.total_size) + else: + end_size = Size(int(end_sector), Unit.sectors, device_info.sector_size) + + return start_size, end_size + + def _create_new_partition(self) -> PartitionModification: + fs_type = self._prompt_partition_fs_type() + + start_size, end_size = self._prompt_sectors() + length = end_size - start_size + + # new line for the next prompt + print() + + mountpoint = None + if fs_type != FilesystemType.Btrfs: + mountpoint = self._prompt_mountpoint() + + partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=start_size, + length=length, + fs_type=fs_type, + mountpoint=mountpoint + ) + + if partition.mountpoint == Path('/boot'): + partition.set_flag(PartitionFlag.Boot) + + return partition + + def _reset_confirmation(self) -> MenuSelection: + prompt = str(_('This will remove all newly added partitions, continue?')) + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() + return choice + + def _suggest_partition_layout(self, data: List[PartitionModification]) -> List[PartitionModification]: + # if modifications have been done already, inform the user + # that this operation will erase those modifications + if any([not entry.exists() for entry in data]): + choice = self._reset_confirmation() + if choice.value == Menu.no(): + return [] + + from ..user_interaction.disk_conf import suggest_single_disk_layout + + device_modification = suggest_single_disk_layout(self._device) + return device_modification.partitions + + +def manual_partitioning( + device: BDevice, + prompt: str = '', + preset: List[PartitionModification] = [] +) -> List[PartitionModification]: + if not prompt: + prompt = str(_('Partition management: {}')).format(device.device_info.path) + '\n' + prompt += str(_('Total length: {}')).format(device.device_info.total_size.format_size(Unit.MiB)) + + manual_preset = [] + + if not preset: + # we'll display the existing partitions of the device + for partition in device.partition_infos: + manual_preset.append( + PartitionModification.from_existing_partition(partition) + ) + else: + manual_preset = preset + + menu_list = PartitioningList(prompt, device, manual_preset) + partitions = menu_list.run() + + if menu_list.is_last_choice_cancel(): + return preset + + return partitions diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py new file mode 100644 index 00000000..32a0e616 --- /dev/null +++ b/archinstall/lib/disk/subvolume_menu.py @@ -0,0 +1,101 @@ +from pathlib import Path +from typing import Dict, List, Optional, Any, TYPE_CHECKING + +from .device_model import SubvolumeModification +from ..menu import Menu, TextInput, MenuSelectionType, ListManager +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class SubvolumeMenu(ListManager): + def __init__(self, prompt: str, btrfs_subvols: List[SubvolumeModification]): + self._actions = [ + str(_('Add subvolume')), + str(_('Edit subvolume')), + str(_('Delete subvolume')) + ] + super().__init__(prompt, btrfs_subvols, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[SubvolumeModification]) -> Dict[str, Optional[SubvolumeModification]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[SubvolumeModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, subvol in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = subvol + + return display_data + + def selected_action_display(self, subvolume: SubvolumeModification) -> str: + return str(subvolume.name) + + def _prompt_options(self, editing: Optional[SubvolumeModification] = None) -> List[str]: + preset_options = [] + if editing: + preset_options = editing.mount_options + + choice = Menu( + str(_("Select the desired subvolume options ")), + ['nodatacow', 'compress'], + skip=True, + preset_values=preset_options, + multi=True + ).run() + + if choice.type_ == MenuSelectionType.Selection: + return choice.value # type: ignore + + return [] + + def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]: + name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() + + if not name: + return None + + mountpoint = TextInput(f'{_("Subvolume mountpoint")}: ', str(editing.mountpoint) if editing else '').run() + + if not mountpoint: + return None + + options = self._prompt_options(editing) + + subvolume = SubvolumeModification(Path(name), Path(mountpoint)) + subvolume.compress = 'compress' in options + subvolume.nodatacow = 'nodatacow' in options + + return subvolume + + def handle_action( + self, + action: str, + entry: Optional[SubvolumeModification], + data: List[SubvolumeModification] + ) -> List[SubvolumeModification]: + if action == self._actions[0]: # add + new_subvolume = self._add_subvolume() + + if new_subvolume is not None: + # in case a user with the same username as an existing user + # was created we'll replace the existing one + data = [d for d in data if d.name != new_subvolume.name] + data += [new_subvolume] + elif entry is not None: + if action == self._actions[1]: # edit subvolume + new_subvolume = self._add_subvolume(entry) + + if new_subvolume is not None: + # we'll remove the original subvolume and add the modified version + data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] + data += [new_subvolume] + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py deleted file mode 100644 index 5809c073..00000000 --- a/archinstall/lib/disk/user_guides.py +++ /dev/null @@ -1,240 +0,0 @@ -from __future__ import annotations -import logging -from typing import Optional, Dict, Any, List, TYPE_CHECKING - -# https://stackoverflow.com/a/39757388/929999 -from ..models.subvolume import Subvolume - -if TYPE_CHECKING: - from .blockdevice import BlockDevice - _: Any - -from .helpers import sort_block_devices_based_on_performance, select_largest_device, select_disk_larger_than_or_close_to -from ..hardware import has_uefi -from ..output import log -from ..menu import Menu - - -def suggest_single_disk_layout(block_device :BlockDevice, - default_filesystem :Optional[str] = None, - advanced_options :bool = False) -> Dict[str, Any]: - - if not default_filesystem: - from ..user_interaction import ask_for_main_filesystem_format - default_filesystem = ask_for_main_filesystem_format(advanced_options) - - MIN_SIZE_TO_ALLOW_HOME_PART = 40 # GiB - using_subvolumes = False - using_home_partition = False - compression = False - - if default_filesystem == 'btrfs': - prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_subvolumes = choice.value == Menu.yes() - - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - layout = { - block_device.path : { - "wipe" : True, - "partitions" : [] - } - } - - # Used for reference: https://wiki.archlinux.org/title/partitioning - - # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for - # other bootloaders? - - # TODO: On BIOS, /boot partition is only needed if the drive will - # be encrypted, otherwise it is not recommended. We should probably - # add a check for whether the drive will be encrypted or not. - layout[block_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "3MiB", - "size" : "203MiB", - "boot" : True, - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - - # Increase the UEFI partition if UEFI is detected. - # Also re-align the start to 1MiB since we don't need the first sectors - # like we do in MBR layouts where the boot loader is installed traditionally. - if has_uefi(): - layout[block_device.path]['partitions'][-1]['start'] = '1MiB' - layout[block_device.path]['partitions'][-1]['size'] = '512MiB' - - layout[block_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "206MiB", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/" if not using_subvolumes else None, - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - if has_uefi(): - layout[block_device.path]['partitions'][-1]['start'] = '513MiB' - - if not using_subvolumes and block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: - prompt = str(_('Would you like to create a separate partition for /home?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_home_partition = choice.value == Menu.yes() - - # Set a size for / (/root) - if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART or not using_home_partition: - # We'll use subvolumes - # Or the disk size is too small to allow for a separate /home - # Or the user doesn't want to create a separate partition for /home - layout[block_device.path]['partitions'][-1]['size'] = '100%' - else: - layout[block_device.path]['partitions'][-1]['size'] = f"{min(block_device.size, 20)}GiB" - - if default_filesystem == 'btrfs' and using_subvolumes: - # if input('Do you want to use a recommended structure? (Y/n): ').strip().lower() in ('', 'y', 'yes'): - # https://btrfs.wiki.kernel.org/index.php/FAQ - # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash - # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh - layout[block_device.path]['partitions'][1]['btrfs'] = { - 'subvolumes': [ - Subvolume('@', '/'), - Subvolume('@home', '/home'), - Subvolume('@log', '/var/log'), - Subvolume('@pkg', '/var/cache/pacman/pkg'), - Subvolume('@.snapshots', '/.snapshots') - ] - } - elif using_home_partition: - # If we don't want to use subvolumes, - # But we want to be able to re-use data between re-installs.. - # A second partition for /home would be nice if we have the space for it - layout[block_device.path]['partitions'].append({ - # Home - "type" : "primary", - "start" : f"{min(block_device.size, 20)}GiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - return layout - - -def suggest_multi_disk_layout(block_devices :List[BlockDevice], default_filesystem :Optional[str] = None, advanced_options :bool = False): - - if not default_filesystem: - from ..user_interaction import ask_for_main_filesystem_format - default_filesystem = ask_for_main_filesystem_format(advanced_options) - - # Not really a rock solid foundation of information to stand on, but it's a start: - # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ - # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - - MIN_SIZE_TO_ALLOW_HOME_PART = 40 # GiB - ARCH_LINUX_INSTALLED_SIZE = 20 # GiB, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - - block_devices = sort_block_devices_based_on_performance(block_devices).keys() - - home_device = select_largest_device(block_devices, gigabytes=MIN_SIZE_TO_ALLOW_HOME_PART) - root_device = select_disk_larger_than_or_close_to(block_devices, gigabytes=ARCH_LINUX_INSTALLED_SIZE, filter_out=[home_device]) - - if home_device is None or root_device is None: - text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') - text += _('Minimum capacity for /home partition: {}GB\n').format(MIN_SIZE_TO_ALLOW_HOME_PART) - text += _('Minimum capacity for Arch Linux partition: {}GB').format(ARCH_LINUX_INSTALLED_SIZE) - Menu(str(text), [str(_('Continue'))], skip=False).run() - return None - - compression = False - - if default_filesystem == 'btrfs': - # prompt = 'Would you like to use BTRFS subvolumes with a default structure?' - # choice = Menu(prompt, ['yes', 'no'], skip=False, default_option='yes').run() - # using_subvolumes = choice == 'yes' - - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG) - - layout = { - root_device.path : { - "wipe" : True, - "partitions" : [] - }, - home_device.path : { - "wipe" : True, - "partitions" : [] - }, - } - - # TODO: Same deal as with the single disk layout, we should - # probably check if the drive will be encrypted. - layout[root_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "3MiB", - "size" : "203MiB", - "boot" : True, - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - - if has_uefi(): - layout[root_device.path]['partitions'][-1]['start'] = '1MiB' - layout[root_device.path]['partitions'][-1]['size'] = '512MiB' - - layout[root_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "206MiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - if has_uefi(): - layout[root_device.path]['partitions'][-1]['start'] = '513MiB' - - layout[home_device.path]['partitions'].append({ - # Home - "type" : "primary", - "start" : "1MiB", - "size" : "100%", - "encrypted" : False, - "wipe" : True, - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem, - "mount_options" : ["compress=zstd"] if compression else [] - } - }) - - return layout diff --git a/archinstall/lib/disk/validators.py b/archinstall/lib/disk/validators.py deleted file mode 100644 index 076a8ba2..00000000 --- a/archinstall/lib/disk/validators.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List - -def valid_parted_position(pos :str) -> bool: - if not len(pos): - return False - - if pos.isdigit(): - return True - - pos_lower = pos.lower() - - if (pos_lower.endswith('b') or pos_lower.endswith('s')) and pos[:-1].isdigit(): - return True - - if any(pos_lower.endswith(size) and pos[:-len(size)].replace(".", "", 1).isdigit() - for size in ['%', 'kb', 'mb', 'gb', 'tb', 'kib', 'mib', 'gib', 'tib']): - return True - - return False - - -def fs_types() -> List[str]: - # https://www.gnu.org/software/parted/manual/html_node/mkpart.html - # Above link doesn't agree with `man parted` /mkpart documentation: - """ - fs-type can - be one of "btrfs", "ext2", - "ext3", "ext4", "fat16", - "fat32", "hfs", "hfs+", - "linux-swap", "ntfs", "reis‐ - erfs", "udf", or "xfs". - """ - return [ - "btrfs", - "ext2", - "ext3", "ext4", # `man parted` allows these - "fat16", "fat32", - "hfs", "hfs+", # "hfsx", not included in `man parted` - "linux-swap", - "ntfs", - "reiserfs", - "udf", # "ufs", not included in `man parted` - "xfs", # `man parted` allows this - ] - - -def valid_fs_type(fstype :str) -> bool: - return fstype.lower() in fs_types() -- cgit v1.2.3-54-g00ecf From e78ddb03e1bbc46e59fd6a9889699b12808d0fec Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 28 Apr 2023 22:18:48 +1000 Subject: Attempt to fetch partuuid multiple times (#1770) * Attempt to fetch partuuid multiple times * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 12cf18ea..ba325cda 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -341,16 +341,35 @@ class DeviceHandler(object): # the partition has a real path now as it was created part_mod.dev_path = Path(partition.path) - info = get_lsblk_info(part_mod.dev_path) - - if not info.partuuid: - raise DiskError(f'Unable to determine new partition uuid: {part_mod.dev_path}') + info = self._fetch_partuuid(part_mod.dev_path) part_mod.partuuid = info.partuuid part_mod.uuid = info.uuid except PartitionException as ex: raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex + def _fetch_partuuid(self, path: Path) -> LsblkInfo: + attempts = 3 + info: Optional[LsblkInfo] = None + + self.partprobe(path) + for attempt_nr in range(attempts): + time.sleep(attempt_nr + 1) + info = get_lsblk_info(path) + + if info.partuuid: + break + + self.partprobe(path) + + if not info or not info.partuuid: + log(f'Unable to determine new partition uuid: {path}\n{info}', level=logging.DEBUG) + raise DiskError(f'Unable to determine new partition uuid: {path}') + + log(f'partuuid found: {info.json()}', level=logging.DEBUG) + + return info + def create_btrfs_volumes( self, part_mod: PartitionModification, @@ -555,12 +574,13 @@ class DeviceHandler(object): command = 'partprobe' try: + log(f'Calling partprobe: {command}', level=logging.DEBUG) result = SysCommand(command) + if result.exit_code != 0: - log(f'Error calling partprobe: {result.decode()}', level=logging.DEBUG) - raise DiskError(f'Could not perform partprobe on {path}: {result.decode()}') + log(f'"{command}" returned a failure: {result.decode()}', level=logging.DEBUG) except SysCallError as error: - log(f"partprobe experienced an error with {path}: {error}", level=logging.DEBUG) + log(f'"{command}" failed to run: {error}', level=logging.DEBUG) def _wipe(self, dev_path: Path): """ -- cgit v1.2.3-54-g00ecf From ec4ecbcb7a839ab06b739f01ce42bfd18376c620 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 4 May 2023 00:36:46 +1000 Subject: Full mypy compliance and small fixes (#1777) * Fix mypy compliance --------- Co-authored-by: Daniel Girtler --- .github/workflows/mypy.yaml | 2 +- archinstall/__init__.py | 3 +- archinstall/lib/disk/device_handler.py | 18 +- archinstall/lib/disk/device_model.py | 2 +- archinstall/lib/disk/fido.py | 9 +- archinstall/lib/general.py | 55 +++--- archinstall/lib/hardware.py | 55 +++--- archinstall/lib/installer.py | 239 +++++++++++------------ archinstall/lib/menu/abstract_menu.py | 4 +- archinstall/lib/menu/menu.py | 104 ++++++---- archinstall/lib/mirrors.py | 46 +++-- archinstall/lib/models/network_configuration.py | 78 ++++---- archinstall/lib/plugins.py | 66 ++++--- archinstall/lib/profile/profiles_handler.py | 16 +- archinstall/lib/systemd.py | 71 ++----- archinstall/lib/user_interaction/general_conf.py | 68 ++++--- archinstall/lib/user_interaction/locale_conf.py | 22 ++- archinstall/scripts/guided.py | 2 +- archinstall/scripts/swiss.py | 4 +- examples/interactive_installation.py | 4 +- mypy.ini | 14 -- pyproject.toml | 1 + 22 files changed, 454 insertions(+), 429 deletions(-) delete mode 100644 mypy.ini (limited to 'archinstall/lib/disk') diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 8689570f..e0db6f06 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -15,4 +15,4 @@ jobs: # one day this will be enabled # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --config-file mypy.ini + run: mypy --config-file pyproject.toml diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 3d0768a5..29b70b7a 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -233,7 +233,8 @@ def post_process_arguments(arguments): log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) if arguments.get('plugin', None): - load_plugin(arguments['plugin']) + path = arguments['plugin'] + load_plugin(path) load_config() diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index ba325cda..8f92cf3b 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -269,13 +269,13 @@ class DeviceHandler(object): # partition will be encrypted if enc_conf is not None and part_mod in enc_conf.partitions: self._perform_enc_formatting( - part_mod.real_dev_path, + part_mod.safe_dev_path, part_mod.mapper_name, part_mod.fs_type, enc_conf ) else: - self._perform_formatting(part_mod.fs_type, part_mod.real_dev_path) + self._perform_formatting(part_mod.fs_type, part_mod.safe_dev_path) def _perform_partitioning( self, @@ -287,11 +287,11 @@ class DeviceHandler(object): # when we require a delete and the partition to be (re)created # already exists then we have to delete it first if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: - log(f'Delete existing partition: {part_mod.real_dev_path}', level=logging.INFO) - part_info = self.find_partition(part_mod.real_dev_path) + log(f'Delete existing partition: {part_mod.safe_dev_path}', level=logging.INFO) + part_info = self.find_partition(part_mod.safe_dev_path) if not part_info: - raise DiskError(f'No partition for dev path found: {part_mod.real_dev_path}') + raise DiskError(f'No partition for dev path found: {part_mod.safe_dev_path}') disk.deletePartition(part_info.partition) disk.commit() @@ -375,7 +375,7 @@ class DeviceHandler(object): part_mod: PartitionModification, enc_conf: Optional['DiskEncryption'] = None ): - log(f'Creating subvolumes: {part_mod.real_dev_path}', level=logging.INFO) + log(f'Creating subvolumes: {part_mod.safe_dev_path}', level=logging.INFO) luks_handler = None @@ -385,7 +385,7 @@ class DeviceHandler(object): raise ValueError('No device path specified for modification') luks_handler = self.unlock_luks2_dev( - part_mod.real_dev_path, + part_mod.safe_dev_path, part_mod.mapper_name, enc_conf.encryption_password ) @@ -395,7 +395,7 @@ class DeviceHandler(object): self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) else: - self.mount(part_mod.real_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) for sub_vol in part_mod.btrfs_subvols: log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG) @@ -419,7 +419,7 @@ class DeviceHandler(object): self.umount(luks_handler.mapper_dev) luks_handler.lock() else: - self.umount(part_mod.real_dev_path) + self.umount(part_mod.safe_dev_path) def unlock_luks2_dev(self, dev_path: Path, mapper_name: str, enc_password: str) -> Luks2: luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password) diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 0270a4dd..987a1e8a 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -603,7 +603,7 @@ class PartitionModification: return '' @property - def real_dev_path(self) -> Path: + def safe_dev_path(self) -> Path: if self.dev_path is None: raise ValueError('Device path was not set') return self.dev_path diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 436be4d4..2a53b551 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -2,7 +2,8 @@ from __future__ import annotations import getpass import logging -from typing import List +from pathlib import Path +from typing import List, Optional from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes @@ -36,12 +37,12 @@ class Fido2: # to prevent continous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: - ret = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') if not ret: log('Unable to retrieve fido2 devices', level=logging.ERROR) return [] - fido_devices = clear_vt100_escape_codes(ret) + fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore manufacturer_pos = 0 product_pos = 0 @@ -58,7 +59,7 @@ class Fido2: product = line[product_pos:] devices.append( - Fido2Device(path, manufacturer, product) + Fido2Device(Path(path), manufacturer, product) ) cls._loaded = True diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 57f13288..997b7d67 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -19,9 +19,15 @@ import pathlib from datetime import datetime, date from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING +from .exceptions import RequirementError, SysCallError +from .output import log +from .storage import storage + + if TYPE_CHECKING: from .installer import Installer + if sys.platform == 'linux': from select import epoll, EPOLLIN, EPOLLHUP else: @@ -53,30 +59,15 @@ else: except OSError: return [] -from .exceptions import RequirementError, SysCallError -from .output import log -from .storage import storage def gen_uid(entropy_length :int = 256) -> str: return hashlib.sha512(os.urandom(entropy_length)).hexdigest() + def generate_password(length :int = 64) -> str: haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) -def multisplit(s :str, splitters :List[str]) -> str: - s = [s, ] - for key in splitters: - ns = [] - for obj in s: - x = obj.split(key) - for index, part in enumerate(x): - if len(part): - ns.append(part) - if index < len(x) - 1: - ns.append(key) - s = ns - return s def locate_binary(name :str) -> str: for PATH in os.environ['PATH'].split(':'): @@ -88,20 +79,20 @@ def locate_binary(name :str) -> str: raise RequirementError(f"Binary {name} does not exist.") -def clear_vt100_escape_codes(data :Union[bytes, str]): + +def clear_vt100_escape_codes(data :Union[bytes, str]) -> Union[bytes, str]: # https://stackoverflow.com/a/43627833/929999 if type(data) == bytes: - vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8') - else: + byte_vt100_escape_regex = bytes(r'\x1B\[[?0-9;]*[a-zA-Z]', 'UTF-8') + data = re.sub(byte_vt100_escape_regex, b'', data) + elif type(data) == str: vt100_escape_regex = r'\x1B\[[?0-9;]*[a-zA-Z]' - - for match in re.findall(vt100_escape_regex, data, re.IGNORECASE): - data = data.replace(match, '' if type(data) == str else b'') + data = re.sub(vt100_escape_regex, '', data) + else: + raise ValueError(f'Unsupported data type: {type(data)}') return data -def json_dumps(*args :str, **kwargs :str) -> str: - return json.dumps(*args, **{**kwargs, 'cls': JSON}) class JsonEncoder: @staticmethod @@ -245,10 +236,12 @@ class SysCommandWorker: def __iter__(self, *args :str, **kwargs :Dict[str, Any]) -> Iterator[bytes]: for line in self._trace_log[self._trace_log_pos:self._trace_log.rfind(b'\n')].split(b'\n'): if line: + escaped_line: bytes = line + if self.remove_vt100_escape_codes_from_lines: - line = clear_vt100_escape_codes(line) + escaped_line = clear_vt100_escape_codes(line) # type: ignore - yield line + b'\n' + yield escaped_line + b'\n' self._trace_log_pos = self._trace_log.rfind(b'\n') @@ -279,7 +272,11 @@ class SysCommandWorker: log(args[1], level=logging.DEBUG, fg='red') if self.exit_code != 0: - raise SysCallError(f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {self._trace_log[-500:]}", self.exit_code, worker=self) + raise SysCallError( + f"{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self._trace_log[-500:])}", + self.exit_code, + worker=self + ) def is_alive(self) -> bool: self.poll() @@ -328,7 +325,7 @@ class SysCommandWorker: change_perm = True with peak_logfile.open("a") as peek_output_log: - peek_output_log.write(output) + peek_output_log.write(str(output)) if change_perm: os.chmod(str(peak_logfile), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) @@ -497,7 +494,7 @@ class SysCommand: clears any printed output if ``.peek_output=True``. """ if self.session: - return self.session + return True with SysCommandWorker( self.cmd, diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 9660ea95..3759725f 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -2,7 +2,7 @@ import os import logging from functools import partial from pathlib import Path -from typing import Iterator, Optional, Union +from typing import Iterator, Optional, Dict from .general import SysCommand from .networking import list_interfaces, enrich_iface_types @@ -61,15 +61,15 @@ AVAILABLE_GFX_DRIVERS = { "VMware / VirtualBox (open-source)": ["mesa", "xf86-video-vmware"], } -CPUINFO = Path("/proc/cpuinfo") -MEMINFO = Path("/proc/meminfo") - def cpuinfo() -> Iterator[dict[str, str]]: - """Yields information about the CPUs of the system.""" - cpu = {} + """ + Yields information about the CPUs of the system + """ + cpu_info_path = Path("/proc/cpuinfo") + cpu: Dict[str, str] = {} - with CPUINFO.open() as file: + with cpu_info_path.open() as file: for line in file: if not (line := line.strip()): yield cpu @@ -80,24 +80,31 @@ def cpuinfo() -> Iterator[dict[str, str]]: cpu[key.strip()] = value.strip() -def meminfo(key: Optional[str] = None) -> Union[dict[str, int], Optional[int]]: - """Returns a dict with memory info if called with no args +def all_meminfo() -> Dict[str, int]: + """ + Returns a dict with memory info if called with no args or the value of the given key of said dict. """ - with MEMINFO.open() as file: - mem_info = { - (columns := line.strip().split())[0].rstrip(':'): int(columns[1]) - for line in file - } + mem_info_path = Path("/proc/meminfo") + mem_info: Dict[str, int] = {} - if key is None: - return mem_info + with mem_info_path.open() as file: + for line in file: + key, value = line.strip().split(':') + num = value.split()[0] + mem_info[key] = int(num) + + return mem_info - return mem_info.get(key) + +def meminfo_for_key(key: str) -> int: + info = all_meminfo() + return info[key] def has_wifi() -> bool: - return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values() + ifaces = list(list_interfaces().values()) + return 'WIRELESS' in enrich_iface_types(ifaces).values() def has_cpu_vendor(vendor_id: str) -> bool: @@ -160,15 +167,15 @@ def product_name() -> Optional[str]: def mem_available() -> Optional[int]: - return meminfo('MemAvailable') + return meminfo_for_key('MemAvailable') def mem_free() -> Optional[int]: - return meminfo('MemFree') + return meminfo_for_key('MemFree') def mem_total() -> Optional[int]: - return meminfo('MemTotal') + return meminfo_for_key('MemTotal') def virtualization() -> Optional[str]: @@ -182,9 +189,9 @@ def virtualization() -> Optional[str]: def is_vm() -> bool: try: - return b"none" not in b"".join(SysCommand("systemd-detect-virt")).lower() + result = SysCommand("systemd-detect-virt") + return b"none" not in b"".join(result).lower() except SysCallError as error: log(f"System is not running in a VM: {error}", level=logging.DEBUG) - return None -# TODO: Add more identifiers + return False diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ddbcc2f2..b6eaa797 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -7,7 +7,7 @@ import shutil import subprocess import time from pathlib import Path -from typing import Any, Iterator, List, Mapping, Optional, TYPE_CHECKING, Union, Dict +from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, Iterable from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError @@ -36,32 +36,6 @@ __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "l __accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"] -class InstallationFile: - def __init__(self, installation :'Installer', filename :str, owner :str, mode :str = "w"): - self.installation = installation - self.filename = filename - self.owner = owner - self.mode = mode - self.fh = None - - def __enter__(self) -> 'InstallationFile': - self.fh = open(self.filename, self.mode) - return self - - def __exit__(self, *args :str) -> None: - self.fh.close() - self.installation.chown(self.owner, self.filename) - - def write(self, data: Union[str, bytes]) -> int: - return self.fh.write(data) - - def read(self, *args) -> Union[str, bytes]: - return self.fh.read(*args) - -# def poll(self, *args) -> bool: -# return self.fh.poll(*args) - - def accessibility_tools_in_use() -> bool: return os.system('systemctl is-active --quiet espeakup.service') == 0 @@ -106,15 +80,17 @@ class Installer: self.kernels = kernels self._disk_config = disk_config - self._disk_encryption = disk_encryption - if self._disk_encryption is None: + if disk_encryption is None: self._disk_encryption = disk.DiskEncryption(disk.EncryptionType.NoEncryption) + else: + self._disk_encryption = disk_encryption + + self.target: Path = target - self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) - self.helper_flags = {'base': False, 'bootloader': False} + self.helper_flags: Dict[str, Any] = {'base': False, 'bootloader': None} self.base_packages = base_packages for kernel in self.kernels: @@ -124,31 +100,33 @@ class Installer: if accessibility_tools_in_use(): self.base_packages.extend(__accessibility_packages__) - self.post_base_install = [] + self.post_base_install: List[Callable] = [] # TODO: Figure out which one of these two we'll use.. But currently we're mixing them.. storage['session'] = self storage['installation_session'] = self - self.MODULES = [] - self.BINARIES = [] - self.FILES = [] + self.modules: List[str] = [] + self._binaries: List[str] = [] + self._files: List[str] = [] + # systemd, sd-vconsole and sd-encrypt will be replaced by udev, keymap and encrypt # if HSM is not used to encrypt the root volume. Check mkinitcpio() function for that override. - self.HOOKS = ["base", "systemd", "autodetect", "keyboard", "sd-vconsole", "modconf", "block", "filesystems", "fsck"] - self.KERNEL_PARAMS = [] - self.FSTAB_ENTRIES = [] + self._hooks: List[str] = [ + "base", "systemd", "autodetect", "keyboard", + "sd-vconsole", "modconf", "block", "filesystems", "fsck" + ] + self._kernel_params: List[str] = [] + self._fstab_entries: List[str] = [] self._zram_enabled = False - def __enter__(self, *args: str, **kwargs: str) -> 'Installer': + def __enter__(self) -> 'Installer': return self - def __exit__(self, *args :str, **kwargs :str) -> bool: - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - - if len(args) >= 2 and args[1]: - self.log(args[1], level=logging.ERROR, fg='red') + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + log(exc_val, fg='red', level=logging.ERROR) self.sync_log_to_install_medium() @@ -156,7 +134,7 @@ class Installer: # and then reboot, and a identical log file will be found in the ISO medium anyway. print(_("[!] A log file has been created here: {}").format(os.path.join(storage['LOG_PATH'], storage['LOG_FILE']))) print(_(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")) - raise args[1] + raise exc_val if not (missing_steps := self.post_install_check()): self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) @@ -164,6 +142,7 @@ class Installer: return True else: self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING) + for step in missing_steps: self.log(f' - {step}', fg='red', level=logging.WARNING) @@ -247,31 +226,32 @@ class Installer: luks_handlers = {} for part_mod in partitions: - luks_handler = disk.device_handler.unlock_luks2_dev( - part_mod.dev_path, - part_mod.mapper_name, - self._disk_encryption.encryption_password - ) - luks_handlers[part_mod] = luks_handler + if part_mod.mapper_name and part_mod.dev_path: + luks_handler = disk.device_handler.unlock_luks2_dev( + part_mod.dev_path, + part_mod.mapper_name, + self._disk_encryption.encryption_password + ) + luks_handlers[part_mod] = luks_handler return luks_handlers def _mount_partition(self, part_mod: disk.PartitionModification): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint is not None: + if part_mod.mountpoint and part_mod.dev_path: target = self.target / part_mod.relative_mountpoint disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) - if part_mod.fs_type == disk.FilesystemType.Btrfs: + if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint is not None: + if part_mod.mountpoint and luks_handler.mapper_dev: target = self.target / part_mod.relative_mountpoint disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) - if part_mod.fs_type == disk.FilesystemType.Btrfs: + if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]): @@ -346,15 +326,15 @@ class Installer: SysCommand(f'chmod 0600 {self.target}{file}') SysCommand(f'mkswap {self.target}{file}') - self.FSTAB_ENTRIES.append(f'{file} none swap defaults 0 0') + self._fstab_entries.append(f'{file} none swap defaults 0 0') if enable_resume: resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode('UTF-8').strip() resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode('UTF-8').split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() - self.HOOKS.append('resume') - self.KERNEL_PARAMS.append(f'resume=UUID={resume_uuid}') - self.KERNEL_PARAMS.append(f'resume_offset={resume_offset}') + self._hooks.append('resume') + self._kernel_params.append(f'resume=UUID={resume_uuid}') + self._kernel_params.append(f'resume_offset={resume_offset}') def post_install_check(self, *args :str, **kwargs :str) -> List[str]: return [step for step, flag in self.helper_flags.items() if flag is False] @@ -411,7 +391,7 @@ class Installer: else: pacman_conf.write(line) - def pacstrap(self, *packages: Union[str, List[str]], **kwargs :str) -> bool: + def _pacstrap(self, packages: Union[str, List[str]]) -> bool: if type(packages[0]) in (list, tuple): packages = packages[0] @@ -430,9 +410,9 @@ class Installer: if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): - return self.pacstrap(*packages, **kwargs) + return self._pacstrap(packages) - raise RequirementError(f'Could not sync mirrors: {error}', level=logging.ERROR, fg="red") + raise RequirementError(f'Could not sync mirrors: {error}') try: SysCommand(f'/usr/bin/pacstrap -C /etc/pacman.conf -K {self.target} {" ".join(packages)} --noconfirm', peek_output=True) @@ -442,40 +422,44 @@ class Installer: if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): - return self.pacstrap(*packages, **kwargs) + return self._pacstrap(packages) raise RequirementError("Pacstrap failed. See /var/log/archinstall/install.log or above message for error details.") - def set_mirrors(self, mirrors :Mapping[str, Iterator[str]]) -> None: + def set_mirrors(self, mirrors: Dict[str, Iterable[str]]): for plugin in plugins.values(): if hasattr(plugin, 'on_mirrors'): if result := plugin.on_mirrors(mirrors): mirrors = result - return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') + destination = f'{self.target}/etc/pacman.d/mirrorlist' + use_mirrors(mirrors, destination=destination) def genfstab(self, flags :str = '-pU'): self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) try: - fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}') + gen_fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}').decode() except SysCallError as error: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {error}') - with open(f"{self.target}/etc/fstab", 'a') as fstab_fh: - fstab_fh.write(fstab.decode()) + if not gen_fstab: + raise RequirementError(f'Genrating fstab returned empty value') + + with open(f"{self.target}/etc/fstab", 'a') as fp: + fp.write(gen_fstab) if not os.path.isfile(f'{self.target}/etc/fstab'): - raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {fstab}') + raise RequirementError(f'Could not create fstab file') for plugin in plugins.values(): if hasattr(plugin, 'on_genfstab'): if plugin.on_genfstab(self) is True: break - with open(f"{self.target}/etc/fstab", 'a') as fstab_fh: - for entry in self.FSTAB_ENTRIES: - fstab_fh.write(f'{entry}\n') + with open(f"{self.target}/etc/fstab", 'a') as fp: + for entry in self._fstab_entries: + fp.write(f'{entry}\n') for mod in self._disk_config.device_modifications: for part_mod in mod.partitions: @@ -583,7 +567,7 @@ class Installer: # fstrim is owned by util-linux, a dependency of both base and systemd. self.enable_service("fstrim.timer") - def enable_service(self, *services: Union[str, List[str]]) -> None: + def enable_service(self, services: Union[str, List[str]]) -> None: if type(services[0]) in (list, tuple): services = services[0] @@ -611,19 +595,7 @@ class Installer: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) def configure_nic(self, network_config: NetworkConfiguration) -> None: - from .systemd import Networkd - - if network_config.dhcp: - conf = Networkd(Match={"Name": network_config.iface}, Network={"DHCP": "yes"}) - else: - network = {"Address": network_config.ip} - if network_config.gateway: - network["Gateway"] = network_config.gateway - if network_config.dns: - dns = network_config.dns - network["DNS"] = dns if isinstance(dns, list) else [dns] - - conf = Networkd(Match={"Name": network_config.iface}, Network=network) + conf = network_config.as_systemd_config() for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): @@ -663,7 +635,7 @@ class Installer: # Otherwise, we can go ahead and add the required package # and enable it's service: else: - self.pacstrap('iwd') + self._pacstrap('iwd') self.enable_service('iwd') for psk in psk_files: @@ -682,12 +654,12 @@ class Installer: if self.helper_flags.get('base', False) is False: def post_install_enable_networkd_resolved(*args :str, **kwargs :str): - self.enable_service('systemd-networkd', 'systemd-resolved') + self.enable_service(['systemd-networkd', 'systemd-resolved']) self.post_base_install.append(post_install_enable_networkd_resolved) # Otherwise, we can go ahead and enable the services else: - self.enable_service('systemd-networkd', 'systemd-resolved') + self.enable_service(['systemd-networkd', 'systemd-resolved']) return True @@ -704,9 +676,9 @@ class Installer: fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: - mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n") - mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") - mkinit.write(f"FILES=({' '.join(self.FILES)})\n") + mkinit.write(f"MODULES=({' '.join(self.modules)})\n") + mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n") + mkinit.write(f"FILES=({' '.join(self._files)})\n") if not self._disk_encryption.hsm_device: # For now, if we don't use HSM we revert to the old @@ -714,9 +686,9 @@ class Installer: # This is purely for stability reasons, we're going away from this. # * systemd -> udev # * sd-vconsole -> keymap - self.HOOKS = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self.HOOKS] + self._hooks = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self._hooks] - mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n") + mkinit.write(f"HOOKS=({' '.join(self._hooks)})\n") try: SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}') @@ -736,25 +708,25 @@ class Installer: if (pkg := part.fs_type.installation_pkg) is not None: self.base_packages.append(pkg) if (module := part.fs_type.installation_module) is not None: - self.MODULES.append(module) + self.modules.append(module) if (binary := part.fs_type.installation_binary) is not None: - self.BINARIES.append(binary) + self._binaries.append(binary) # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: - if 'fsck' in self.HOOKS: - self.HOOKS.remove('fsck') + if 'fsck' in self._hooks: + self._hooks.remove('fsck') if part in self._disk_encryption.partitions: if self._disk_encryption.hsm_device: # Required bby mkinitcpio to add support for fido2-device options - self.pacstrap('libfido2') + self._pacstrap('libfido2') - if 'sd-encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt') + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') else: - if 'encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') if not has_uefi(): self.base_packages.append('grub') @@ -786,7 +758,7 @@ class Installer: else: self.log("The testing flag is not set. This system will be installed without testing repositories enabled.") - self.pacstrap(self.base_packages) + self._pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True # This handles making sure that the repositories we enabled persist on the installed system @@ -826,7 +798,7 @@ class Installer: def setup_swap(self, kind :str = 'zram'): if kind == 'zram': self.log(f"Setting up swap on zram") - self.pacstrap('zram-generator') + self._pacstrap('zram-generator') # We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813 # zram_example_location = '/usr/share/doc/zram-generator/zram-generator.conf.example' @@ -853,7 +825,7 @@ class Installer: return None def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): - self.pacstrap('efibootmgr') + self._pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError @@ -919,7 +891,7 @@ class Installer: # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. - options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self.KERNEL_PARAMS)}\n' + options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self._kernel_params)}\n' for sub_vol in root_partition.btrfs_subvols: if sub_vol.is_root(): @@ -958,7 +930,7 @@ class Installer: boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification ): - self.pacstrap('grub') # no need? + self._pacstrap('grub') # no need? _file = "/etc/default/grub" @@ -977,7 +949,7 @@ class Installer: log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO) if has_uefi(): - self.pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + self._pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) @@ -987,8 +959,20 @@ class Installer: except SysCallError as error: raise DiskError(f"Could not install GRUB to {self.target}/boot: {error}") else: + device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) + + if not device: + raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}') + try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=i386-pc --recheck {boot_partition.parent}', peek_output=True) + cmd = f'/usr/bin/arch-chroot' \ + f' {self.target}' \ + f' grub-install' \ + f' --debug' \ + f' --target=i386-pc' \ + f' --recheck {device.device_info.path}' + + SysCommand(cmd, peek_output=True) except SysCallError as error: raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}") @@ -1004,7 +988,7 @@ class Installer: boot_partition: disk.PartitionModification, root_partition: disk.PartitionModification ): - self.pacstrap('efibootmgr') + self._pacstrap('efibootmgr') if not has_uefi(): raise HardwareIncompatibilityError @@ -1038,17 +1022,30 @@ class Installer: # 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) log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) - kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') + kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') else: log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) - kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self.KERNEL_PARAMS)}') + kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') + + device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) - device = disk.device_handler.get_device_by_partition_path(boot_partition.dev_path) - SysCommand(f'efibootmgr --disk {device.path} --part {device.path} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose') + if not device: + raise ValueError(f'Unable to find block device: {boot_partition.safe_dev_path}') + + cmd = f'efibootmgr ' \ + f'--disk {device.device_info.path} ' \ + f'--part {boot_partition.safe_dev_path} ' \ + f'--create ' \ + f'--label "{label}" ' \ + f'--loader {loader} ' \ + f'--unicode \'{" ".join(kernel_parameters)}\' ' \ + f'--verbose' + + SysCommand(cmd) self.helper_flags['bootloader'] = "efistub" - def add_bootloader(self, bootloader: Bootloader) -> bool: + def add_bootloader(self, bootloader: Bootloader): """ Adds a bootloader to the installation instance. Archinstall supports one of three types: @@ -1056,8 +1053,7 @@ class Installer: * grub * efistub (beta) - :param bootloader: Can be one of the three strings - 'systemd-bootctl', 'grub' or 'efistub' (beta) + :param bootloader: Type of bootloader to be added """ for plugin in plugins.values(): @@ -1089,8 +1085,8 @@ class Installer: case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root_partition) - def add_additional_packages(self, *packages: Union[str, List[str]]) -> bool: - return self.pacstrap(*packages) + def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: + return self._pacstrap(packages) def _enable_users(self, service: str, users: List[User]): for user in users: @@ -1201,9 +1197,6 @@ class Installer: except SysCallError: return False - def create_file(self, filename :str, owner :Optional[str] = None) -> InstallationFile: - return InstallationFile(self, filename, owner) - def set_keyboard_language(self, language: str) -> bool: log(f"Setting keyboard language to {language}", level=logging.INFO) if len(language.strip()): diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 53816655..e44d65a4 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -482,9 +482,9 @@ class AbstractMenu: if item in self._menus_to_enable(): yield item - def _select_archinstall_language(self, preset_value: Language) -> Language: + def _select_archinstall_language(self, preset: Language) -> Language: from ..user_interaction.general_conf import select_archinstall_language - language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) + language = select_archinstall_language(self.translation_handler.translated_languages, preset) self._translation_handler.activate(language) return language diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 44ac33a6..12dbf1f5 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -3,7 +3,7 @@ from enum import Enum, auto from os import system from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable -from simple_term_menu import TerminalMenu +from simple_term_menu import TerminalMenu # type: ignore from ..exceptions import RequirementError from ..output import log @@ -29,11 +29,11 @@ class MenuSelection: @property def single_value(self) -> Any: - return self.value + return self.value # type: ignore @property def multi_value(self) -> List[Any]: - return self.value + return self.value # type: ignore class Menu(TerminalMenu): @@ -67,7 +67,7 @@ class Menu(TerminalMenu): preview_command: Optional[Callable] = None, preview_size: float = 0.0, preview_title: str = 'Info', - header: Union[List[str],str] = None, + header: Union[List[str], str] = [], allow_reset: bool = False, allow_reset_warning_msg: Optional[str] = None, clear_screen: bool = True, @@ -141,8 +141,6 @@ class Menu(TerminalMenu): log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError("Menu() requires an iterable as option.") - self._default_str = str(_('(default)')) - if isinstance(p_options,dict): options = list(p_options.keys()) else: @@ -193,8 +191,7 @@ class Menu(TerminalMenu): if default_option: # if a default value was specified we move that one # to the top of the list and mark it as default as well - default = f'{default_option} {self._default_str}' - self._menu_options = [default] + [o for o in self._menu_options if default_option != o] + self._menu_options = [self._default_menu_value] + [o for o in self._menu_options if default_option != o] if display_back_option and not multi and skip: skip_empty_entries = True @@ -204,7 +201,18 @@ class Menu(TerminalMenu): skip_empty_entries = True self._menu_options += [''] - self._preselection(preset_values,cursor_index) + preset_list: Optional[List[str]] = None + + if preset_values and isinstance(preset_values, str): + preset_list = [preset_values] + + calc_cursor_idx = self._determine_cursor_pos(preset_list, cursor_index) + + # when we're not in multi selection mode we don't care about + # passing the pre-selection list to the menu as the position + # of the cursor is the one determining the pre-selection + if not self._multi: + preset_values = None cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") @@ -217,8 +225,8 @@ class Menu(TerminalMenu): menu_cursor_style=main_menu_cursor_style, menu_highlight_style=main_menu_style, multi_select=multi, - preselected_entries=self.preset_values, - cursor_index=self.cursor_index, + preselected_entries=preset_values, + cursor_index=calc_cursor_idx, preview_command=lambda x: self._show_preview(preview_command, x), preview_size=preview_size, preview_title=preview_title, @@ -231,12 +239,17 @@ class Menu(TerminalMenu): skip_empty_entries=skip_empty_entries ) + @property + def _default_menu_value(self) -> str: + default_str = str(_('(default)')) + return f'{self._default_option} {default_str}' + def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]: if selection == self.back(): return None if preview_command: - if self._default_option is not None and f'{self._default_option} {self._default_str}' == selection: + if self._default_option is not None and self._default_menu_value == selection: selection = self._default_option return preview_command(selection) @@ -249,7 +262,7 @@ class Menu(TerminalMenu): return MenuSelection(type_=MenuSelectionType.Reset) def check_default(elem): - if self._default_option is not None and f'{self._default_option} {self._default_str}' in elem: + if self._default_option is not None and self._default_menu_value in elem: return self._default_option else: return elem @@ -297,31 +310,44 @@ class Menu(TerminalMenu): pos = self._menu_entries.index(value) self.set_cursor_pos(pos) - def _preselection(self,preset_values :Union[str, List[str]] = [], cursor_index : Optional[int] = None): - def from_preset_to_cursor(): - if preset_values: - # if the value is not extant return 0 as cursor index + def _determine_cursor_pos( + self, + preset: Optional[List[str]] = None, + cursor_index: Optional[int] = None + ) -> Optional[int]: + """ + The priority order to determine the cursor position is: + 1. A static cursor position was provided + 2. Preset values have been provided so the cursor will be + positioned on those + 3. A default value for a selection is given so the cursor + will be placed on such + """ + if cursor_index: + return cursor_index + + if preset: + indexes = [] + + for p in preset: try: - if isinstance(preset_values,str): - self.cursor_index = self._menu_options.index(self.preset_values) - else: # should return an error, but this is smoother - self.cursor_index = self._menu_options.index(self.preset_values[0]) - except ValueError: - self.cursor_index = 0 - - self.cursor_index = cursor_index - if not preset_values: - self.preset_values = None - return - - self.preset_values = preset_values + # the options of the table selection menu + # are already escaped so we have to escape + # the preset values as well for the comparison + if '|' in p: + p = p.replace('|', '\\|') + + idx = self._menu_options.index(p) + indexes.append(idx) + except (IndexError, ValueError): + log(f'Error finding index of {p}: {self._menu_options}', level=logging.DEBUG) + + if len(indexes) == 0: + indexes.append(0) + + return indexes[0] + if self._default_option: - if isinstance(preset_values,str) and self._default_option == preset_values: - self.preset_values = f"{preset_values} {self._default_str}" - elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values: - idx = preset_values.index(self._default_option) - self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}" - if cursor_index is None or not self._multi: - from_preset_to_cursor() - if not self._multi: # Not supported by the infraestructure - self.preset_values = None + return self._menu_options.index(self._default_menu_value) + + return None diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 4bae6d8b..15d0fd6b 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -3,12 +3,22 @@ import pathlib import urllib.error import urllib.request from typing import Union, Iterable, Dict, Any, List +from dataclasses import dataclass from .general import SysCommand from .output import log from .storage import storage -def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes: + +@dataclass +class CustomMirror: + url: str + signcheck: str + signoptions: str + name: str + + +def sort_mirrorlist(raw_data :bytes, sort_order: List[str] = ['https', 'http']) -> bytes: """ This function can sort /etc/pacman.d/mirrorlist according to the mirror's URL prefix. By default places HTTPS before HTTP but it also @@ -28,8 +38,9 @@ def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes: from server url definitions (commented or uncommented). """ comments_and_whitespaces = b"" + sort_order += ['Unknown'] + categories: Dict[str, List] = {key: [] for key in sort_order} - categories = {key: [] for key in sort_order + ["Unknown"]} for line in raw_data.split(b"\n"): if line[0:2] in (b'##', b''): comments_and_whitespaces += line + b'\n' @@ -82,18 +93,18 @@ def filter_mirrors_by_region(regions :str, return new_list.decode('UTF-8') -def add_custom_mirrors(mirrors: List[str], *args :str, **kwargs :str) -> bool: +def add_custom_mirrors(mirrors: List[CustomMirror]) -> bool: """ This will append custom mirror definitions in pacman.conf - :param mirrors: A list of mirror data according to: `{'url': 'http://url.com', 'signcheck': 'Optional', 'signoptions': 'TrustAll', 'name': 'testmirror'}` - :type mirrors: dict + :param mirrors: A list of custom mirrors + :type mirrors: List[CustomMirror] """ with open('/etc/pacman.conf', 'a') as pacman: for mirror in mirrors: - pacman.write(f"[{mirror['name']}]\n") - pacman.write(f"SigLevel = {mirror['signcheck']} {mirror['signoptions']}\n") - pacman.write(f"Server = {mirror['url']}\n") + pacman.write(f"[{mirror.name}]\n") + pacman.write(f"SigLevel = {mirror.signcheck} {mirror.signoptions}\n") + pacman.write(f"Server = {mirror.url}\n") return True @@ -123,7 +134,7 @@ def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool: def use_mirrors( regions: Dict[str, Iterable[str]], destination: str = '/etc/pacman.d/mirrorlist' -) -> None: +): log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) with open(destination, 'w') as mirrorlist: for region, mirrors in regions.items(): @@ -146,7 +157,7 @@ def re_rank_mirrors( def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: - regions = {} + regions: Dict[str, Dict[str, Any]] = {} if storage['arguments']['offline']: with pathlib.Path('/etc/pacman.d/mirrorlist').open('rb') as fh: @@ -170,18 +181,19 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: if len(line.strip()) == 0: continue - line = line.decode('UTF-8').strip('\n').strip('\r') - if line[:3] == '## ': - region = line[3:] - elif line[:10] == '#Server = ': + clean_line = line.decode('UTF-8').strip('\n').strip('\r') + + if clean_line[:3] == '## ': + region = clean_line[3:] + elif clean_line[:10] == '#Server = ': regions.setdefault(region, {}) - url = line.lstrip('#Server = ') + url = clean_line.lstrip('#Server = ') regions[region][url] = True - elif line.startswith('Server = '): + elif clean_line.startswith('Server = '): regions.setdefault(region, {}) - url = line.lstrip('Server = ') + url = clean_line.lstrip('Server = ') regions[region][url] = True return regions diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index b7ab690d..66230e24 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -1,8 +1,9 @@ from __future__ import annotations -from dataclasses import dataclass +import logging +from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING +from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple from ..output import log from ..storage import storage @@ -24,7 +25,7 @@ class NetworkConfiguration: ip: Optional[str] = None dhcp: bool = True gateway: Optional[str] = None - dns: Union[None, List[str]] = None + dns: List[str] = field(default_factory=list) def __str__(self): if self.is_iso(): @@ -53,6 +54,33 @@ class NetworkConfiguration: return data + def as_systemd_config(self) -> str: + match: List[Tuple[str, str]] = [] + network: List[Tuple[str, str]] = [] + + if self.iface: + match.append(('Name', self.iface)) + + if self.dhcp: + network.append(('DHCP', 'yes')) + else: + if self.ip: + network.append(('Address', self.ip)) + if self.gateway: + network.append(('Gateway', self.gateway)) + for dns in self.dns: + network.append(('DNS', dns)) + + config = {'Match': match, 'Network': network} + + config_str = '' + for top, entries in config.items(): + config_str += f'[{top}]\n' + config_str += '\n'.join([f'{k}={v}' for k, v in entries]) + config_str += '\n\n' + + return config_str + def json(self) -> Dict: # for json serialization when calling json.dumps(...) on this class return self.__dict__ @@ -90,41 +118,14 @@ class NetworkConfigurationHandler: # Perform a copy of the config if self._configuration.is_iso(): installation.copy_iso_network_config( - enable_services=True) # Sources the ISO network configuration to the install medium. + enable_services=True # Sources the ISO network configuration to the install medium. + ) elif self._configuration.is_network_manager(): installation.add_additional_packages(["networkmanager"]) if (profile := storage['arguments'].get('profile_config')) and profile.is_desktop_type_profile: installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') - def _backwards_compability_config(self, config: Union[str,Dict[str, str]]) -> Union[List[NetworkConfiguration], NetworkConfiguration, None]: - def get(config: Dict[str, str], key: str) -> List[str]: - if (value := config.get(key, None)) is not None: - return [value] - return [] - - if isinstance(config, str): # is a ISO network - return NetworkConfiguration(NicType.ISO) - elif config.get('NetworkManager'): # is a network manager configuration - return NetworkConfiguration(NicType.NM) - elif 'ip' in config: - return [NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - ip=config.get('ip'), - gateway=config.get('gateway', ''), - dns=get(config, 'dns'), - dhcp=False - )] - elif 'nic' in config: - return [NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - dhcp=True - )] - else: # not recognized - return None - def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: configurations = [] @@ -145,13 +146,17 @@ class NetworkConfigurationHandler: log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') exit(1) + dns = manual_config.get('dns', []) + if not isinstance(dns, list): + dns = [dns] + configurations.append( NetworkConfiguration( NicType.MANUAL, iface=iface, ip=ip, gateway=manual_config.get('gateway', ''), - dns=manual_config.get('dns', []), + dns=dns, dhcp=False ) ) @@ -176,8 +181,5 @@ class NetworkConfigurationHandler: self._configuration = NetworkConfiguration(type_) else: # manual configuration settings self._configuration = self._parse_manual_config([config]) - else: # old style definitions - network_config = self._backwards_compability_config(config) - if network_config: - return network_config - return None + else: + log(f'Unable to parse network configuration: {config}', level=logging.DEBUG) diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index 0ff63610..b1ece04f 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -3,77 +3,86 @@ import importlib import logging import os import sys -import pathlib import urllib.parse import urllib.request from importlib import metadata +from pathlib import Path from typing import Optional, List -from types import ModuleType from .output import log from .storage import storage plugins = {} + # 1: List archinstall.plugin definitions # 2: Load the plugin entrypoint # 3: Initiate the plugin and store it as .name in plugins for plugin_definition in metadata.entry_points().select(group='archinstall.plugin'): plugin_entrypoint = plugin_definition.load() + try: plugins[plugin_definition.name] = plugin_entrypoint() except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR) -# The following functions and core are support structures for load_plugin() -def localize_path(profile_path :str) -> str: - if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): - converted_path = f"/tmp/{os.path.basename(profile_path).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py" +def localize_path(path: Path) -> Path: + """ + Support structures for load_plugin() + """ + url = urllib.parse.urlparse(str(path)) + + if url.scheme and url.scheme in ('https', 'http'): + converted_path = Path(f'/tmp/{path.stem}_{hashlib.md5(os.urandom(12)).hexdigest()}.py') with open(converted_path, "w") as temp_file: temp_file.write(urllib.request.urlopen(url.geturl()).read().decode('utf-8')) return converted_path else: - return profile_path + return path -def import_via_path(path :str, namespace :Optional[str] = None) -> ModuleType: +def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: if not namespace: namespace = os.path.basename(path) if namespace == '__init__.py': - path = pathlib.PurePath(path) namespace = path.parent.name try: spec = importlib.util.spec_from_file_location(namespace, path) - imported = importlib.util.module_from_spec(spec) - sys.modules[namespace] = imported - spec.loader.exec_module(sys.modules[namespace]) + if spec and spec.loader: + imported = importlib.util.module_from_spec(spec) + sys.modules[namespace] = imported + spec.loader.exec_module(sys.modules[namespace]) return namespace except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR) try: - del(sys.modules[namespace]) # noqa: E275 - except: + del sys.modules[namespace] + except Exception: pass -def find_nth(haystack :List[str], needle :str, n :int) -> int: - start = haystack.find(needle) - while start >= 0 and n > 1: - start = haystack.find(needle, start + len(needle)) - n -= 1 - return start + return namespace + + +def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: + indices = [idx for idx, elem in enumerate(haystack) if elem == needle] + if n <= len(indices): + return indices[n - 1] + return None + -def load_plugin(path :str) -> ModuleType: - parsed_url = urllib.parse.urlparse(path) - log(f"Loading plugin {parsed_url}.", fg="gray", level=logging.INFO) +def load_plugin(path: Path): + namespace: Optional[str] = None + parsed_url = urllib.parse.urlparse(str(path)) + log(f"Loading plugin from url {parsed_url}.", level=logging.INFO) # The Profile was not a direct match on a remote URL if not parsed_url.scheme: @@ -81,9 +90,10 @@ def load_plugin(path :str) -> ModuleType: if os.path.isfile(path): namespace = import_via_path(path) elif parsed_url.scheme in ('https', 'http'): - namespace = import_via_path(localize_path(path)) + localized = localize_path(path) + namespace = import_via_path(localized) - if namespace in sys.modules: + if namespace and namespace in sys.modules: # Version dependency via __archinstall__version__ variable (if present) in the plugin # Any errors in version inconsistency will be handled through normal error handling if not defined. if hasattr(sys.modules[namespace], '__archinstall__version__'): @@ -99,7 +109,7 @@ def load_plugin(path :str) -> ModuleType: plugins[namespace] = sys.modules[namespace].Plugin() log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) except Exception as err: - log(err, level=logging.ERROR) + log(f'Error: {err}', level=logging.ERROR) log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) else: log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index a8b5cc22..824849c3 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -194,23 +194,23 @@ class ProfileHandler: install_session.add_additional_packages(f"{kernel}-headers") # I've had kernel regen fail if it wasn't installed before nvidia-dkms - install_session.add_additional_packages("dkms xorg-server xorg-xinit nvidia-dkms") + install_session.add_additional_packages(['dkms', 'xorg-server', 'xorg-xinit', 'nvidia-dkms']) return elif 'amdgpu' in driver_pkgs: # The order of these two are important if amdgpu is installed #808 - if 'amdgpu' in install_session.MODULES: - install_session.MODULES.remove('amdgpu') - install_session.MODULES.append('amdgpu') + if 'amdgpu' in install_session.modules: + install_session.modules.remove('amdgpu') + install_session.modules.append('amdgpu') - if 'radeon' in install_session.MODULES: - install_session.MODULES.remove('radeon') - install_session.MODULES.append('radeon') + if 'radeon' in install_session.modules: + install_session.modules.remove('radeon') + install_session.modules.append('radeon') install_session.add_additional_packages(additional_pkg) except Exception as err: log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") # Prep didn't run, so there's no driver to install - install_session.add_additional_packages("xorg-server xorg-xinit") + install_session.add_additional_packages(['xorg-server', 'xorg-xinit']) def install_profile_config(self, install_session: 'Installer', profile_config: ProfileConfiguration): profile = profile_config.profile diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py index 64ffcae4..6ccbc5f6 100644 --- a/archinstall/lib/systemd.py +++ b/archinstall/lib/systemd.py @@ -1,6 +1,6 @@ import logging import time -from typing import Iterator +from typing import Iterator, Optional from .exceptions import SysCallError from .general import SysCommand, SysCommandWorker, locate_binary from .installer import Installer @@ -8,51 +8,11 @@ from .output import log from .storage import storage -class Ini: - def __init__(self, *args :str, **kwargs :str): - """ - Limited INI handler for now. - Supports multiple keywords through dictionary list items. - """ - self.kwargs = kwargs - - def __str__(self) -> str: - result = '' - first_row_done = False - for top_level in self.kwargs: - if first_row_done: - result += f"\n[{top_level}]\n" - else: - result += f"[{top_level}]\n" - first_row_done = True - - for key, val in self.kwargs[top_level].items(): - if type(val) == list: - for item in val: - result += f"{key}={item}\n" - else: - result += f"{key}={val}\n" - - return result - - -class Systemd(Ini): - """ - Placeholder class to do systemd specific setups. - """ - - -class Networkd(Systemd): - """ - Placeholder class to do systemd-network specific setups. - """ - - class Boot: def __init__(self, installation: Installer): self.instance = installation self.container_name = 'archinstall' - self.session = None + self.session: Optional[SysCommandWorker] = None self.ready = False def __enter__(self) -> 'Boot': @@ -63,17 +23,18 @@ class Boot: self.session = existing_session.session self.ready = existing_session.ready else: + # '-P' or --console=pipe could help us not having to do a bunch + # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. self.session = SysCommandWorker([ '/usr/bin/systemd-nspawn', - '-D', self.instance.target, + '-D', str(self.instance.target), '--timezone=off', '-b', '--no-pager', '--machine', self.container_name ]) - # '-P' or --console=pipe could help us not having to do a bunch of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. - if not self.ready: + if not self.ready and self.session: while self.session.is_alive(): if b' login:' in self.session: self.ready = True @@ -91,25 +52,31 @@ class Boot: log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red") shutdown = None - shutdown_exit_code = -1 + shutdown_exit_code: Optional[int] = -1 try: shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') except SysCallError as error: shutdown_exit_code = error.exit_code - while self.session.is_alive(): - time.sleep(0.25) + if self.session: + while self.session.is_alive(): + time.sleep(0.25) - if shutdown: + if shutdown and shutdown.exit_code: shutdown_exit_code = shutdown.exit_code - if self.session.exit_code == 0 or shutdown_exit_code == 0: + if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): storage['active_boot'] = None else: - raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {self.session.exit_code}/{shutdown_exit_code}", exit_code=next(filter(bool, [self.session.exit_code, shutdown_exit_code]))) + session_exit_code = self.session.exit_code if self.session else -1 + + raise SysCallError( + f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", + exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) + ) - def __iter__(self) -> Iterator[str]: + def __iter__(self) -> Iterator[bytes]: if self.session: for value in self.session: yield value diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index 7a6bb358..9722dc4d 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging import pathlib from typing import List, Any, Optional, Dict, TYPE_CHECKING -from typing import Union from ..locale_helpers import list_keyboard_languages, list_timezones from ..menu import MenuSelectionType, Menu, TextInput @@ -29,13 +28,18 @@ def ask_ntp(preset: bool = True) -> bool: return False if choice.value == Menu.no() else True -def ask_hostname(preset: str = None) -> str: +def ask_hostname(preset: str = '') -> str: while True: - hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip() + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + if hostname: return hostname -def ask_for_a_timezone(preset: str = None) -> str: + +def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: timezones = list_timezones() default = 'UTC' @@ -48,10 +52,12 @@ def ask_for_a_timezone(preset: str = None) -> str: match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + + return None -def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = None) -> Union[str, None]: +def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: no_audio = str(_('No audio server')) choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] default = 'pipewire' if desktop else no_audio @@ -60,10 +66,12 @@ def ask_for_audio_selection(desktop: bool = True, preset: Union[str, None] = Non match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + return None -def select_language(preset_value: str = None) -> str: + +def select_language(preset: Optional[str] = None) -> Optional[str]: """ Asks the user to select a language Usually this is combined with :ref:`archinstall.list_keyboard_languages`. @@ -75,17 +83,18 @@ def select_language(preset_value: str = None) -> str: # sort alphabetically and then by length sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - selected_lang = Menu( + choice = Menu( _('Select keyboard layout'), sorted_kb_lang, - preset_values=preset_value, + preset_values=preset, sort=False ).run() - if selected_lang.value is None: - return preset_value + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value - return selected_lang.value + return None def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: @@ -100,8 +109,10 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: preselected = None else: preselected = list(preset_values.keys()) + mirrors = list_mirrors() - selected_mirror = Menu( + + choice = Menu( _('Select one of the regions to download packages from'), list(mirrors.keys()), preset_values=preselected, @@ -109,13 +120,18 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: allow_reset=True ).run() - match selected_mirror.type_: - case MenuSelectionType.Reset: return {} - case MenuSelectionType.Skip: return preset_values - case _: return {selected: mirrors[selected] for selected in selected_mirror.value} + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} -def select_archinstall_language(languages: List[Language], preset_value: Language) -> Language: +def select_archinstall_language(languages: List[Language], preset: Language) -> Language: # these are the displayed language names which can either be # the english name of a language or, if present, the # name of the language in its own language @@ -128,15 +144,15 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag choice = Menu( title, list(options.keys()), - default_option=preset_value.display_name, + default_option=preset.display_name, preview_size=0.5 ).run() match choice.type_: - case MenuSelectionType.Skip: - return preset_value - case MenuSelectionType.Selection: - return options[choice.value] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return options[choice.single_value] + + raise ValueError('Language selection not handled') def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: @@ -223,4 +239,6 @@ def select_additional_repositories(preset: List[str]) -> List[str]: match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.single_value + + return [] diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py index 88aec64e..cdc3423a 100644 --- a/archinstall/lib/user_interaction/locale_conf.py +++ b/archinstall/lib/user_interaction/locale_conf.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Optional from ..locale_helpers import list_locales from ..menu import Menu, MenuSelectionType @@ -9,33 +9,37 @@ if TYPE_CHECKING: _: Any -def select_locale_lang(preset: str = None) -> str: +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: locales = list_locales() locale_lang = set([locale.split()[0] for locale in locales]) - selected_locale = Menu( + choice = Menu( _('Choose which locale language to use'), list(locale_lang), sort=True, preset_values=preset ).run() - match selected_locale.type_: - case MenuSelectionType.Selection: return selected_locale.value + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value case MenuSelectionType.Skip: return preset + return None -def select_locale_enc(preset: str = None) -> str: + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: locales = list_locales() locale_enc = set([locale.split()[1] for locale in locales]) - selected_locale = Menu( + choice = Menu( _('Choose which locale encoding to use'), list(locale_enc), sort=True, preset_values=preset ).run() - match selected_locale.type_: - case MenuSelectionType.Selection: return selected_locale.value + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value case MenuSelectionType.Skip: return preset + + return None diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index d9c5837c..48d141b7 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -223,7 +223,7 @@ def perform_installation(mountpoint: Path): # If the user provided a list of services to be enabled, pass the list to the enable_service function. # Note that while it's called enable_service, it can actually take a list of services and iterate it. if archinstall.arguments.get('services', None): - installation.enable_service(*archinstall.arguments['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index e2ee6fcb..34e4c022 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -239,7 +239,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): handler.config_installer(installation) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + installation.add_additional_packages(archinstall.arguments.get('packages', [])) if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -278,7 +278,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # If the user provided a list of services to be enabled, pass the list to the enable_service function. # Note that while it's called enable_service, it can actually take a list of services and iterate it. if archinstall.arguments.get('services', None): - installation.enable_service(*archinstall.arguments['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index a78b1712..f72f110b 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -147,7 +147,7 @@ def perform_installation(mountpoint: Path): handler.config_installer(installation) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': - installation.add_additional_packages(archinstall.arguments.get('packages', None)) + installation.add_additional_packages(archinstall.arguments.get('packages', [])) if users := archinstall.arguments.get('!users', None): installation.create_users(users) @@ -186,7 +186,7 @@ def perform_installation(mountpoint: Path): # If the user provided a list of services to be enabled, pass the list to the enable_service function. # Note that while it's called enable_service, it can actually take a list of services and iterate it. if archinstall.arguments.get('services', None): - installation.enable_service(*archinstall.arguments['services']) + installation.enable_service(archinstall.arguments.get('services', [])) # If the user provided custom commands to be run post-installation, execute them now. if archinstall.arguments.get('custom-commands', None): diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index a08b2d88..00000000 --- a/mypy.ini +++ /dev/null @@ -1,14 +0,0 @@ -[mypy] -python_version = 3.10 -follow_imports = silent -exclude = (?x)(^archinstall/lib/disk/btrfs/btrfssubvolumeinfo\.py$ - | ^archinstall/lib/general\.py$ - | ^archinstall/lib/hardware\.py$ - | ^archinstall/lib/menu/menu\.py$ - | ^archinstall/lib/mirrors\.py$ - | ^archinstall/lib/plugins\.py$ - | ^archinstall/lib/installer\.py$ - | ^archinstall/lib/systemd\.py$ - | ^archinstall/lib/user_interaction/general_conf\.py$ - | ^archinstall/lib/user_interaction/locale_conf\.py$) -files = archinstall/ diff --git a/pyproject.toml b/pyproject.toml index 557418cc..f837ebdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ packages = ["archinstall"] [tool.mypy] python_version = "3.10" +files = "archinstall/" exclude = "tests" [tool.bandit] -- cgit v1.2.3-54-g00ecf From adceed22ad3d8b6aa1e6d1aee56ae0c9a0a751aa Mon Sep 17 00:00:00 2001 From: Daemon Coder <11915375+codefiles@users.noreply.github.com> Date: Thu, 4 May 2023 03:45:43 -0400 Subject: Fix logic errors in `_fetch_lsblk_info()` (#1754) --- archinstall/lib/disk/device_model.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 987a1e8a..8e26b1d7 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -974,32 +974,33 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = if retry == 0: retry = 1 - result = None - - for i in range(retry): + for retry_attempt in range(retry): try: result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') + break except SysCallError as error: # Get the output minus the message/info from lsblk if it returns a non-zero exit code. if error.worker: err = error.worker.decode('UTF-8') log(f'Error calling lsblk: {err}', level=logging.DEBUG) - time.sleep(1) else: raise error - if result and result.exit_code == 0: - try: - if decoded := result.decode('utf-8'): - block_devices = json.loads(decoded) - blockdevices = block_devices['blockdevices'] - return [LsblkInfo.from_json(device) for device in blockdevices] - except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) - raise err + if retry_attempt == retry - 1: + raise error - raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + time.sleep(1) + + try: + if decoded := result.decode('utf-8'): + block_devices = json.loads(decoded) + blockdevices = block_devices['blockdevices'] + return [LsblkInfo.from_json(device) for device in blockdevices] + except json.decoder.JSONDecodeError as err: + log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + raise err + raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: if infos := _fetch_lsblk_info(dev_path): -- cgit v1.2.3-54-g00ecf From fd83f073f3e84feb1388ef739c1096f7d4a741de Mon Sep 17 00:00:00 2001 From: Daemon Coder <11915375+codefiles@users.noreply.github.com> Date: Thu, 4 May 2023 04:42:37 -0400 Subject: Update `SysCommand()` calls in remaining files (#1707) --- archinstall/lib/disk/device_handler.py | 50 ++++++++++++++++------------------ archinstall/lib/locale_helpers.py | 8 ++++-- archinstall/lib/luks.py | 48 ++++++++++++++------------------ archinstall/lib/mirrors.py | 6 ++-- archinstall/lib/networking.py | 23 +++++++++------- 5 files changed, 66 insertions(+), 69 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 8f92cf3b..d4ad9075 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -139,19 +139,18 @@ class DeviceHandler(object): log(f'Failed to read btrfs subvolume information: {err}', level=logging.DEBUG) return subvol_infos - if result.exit_code == 0: - try: - if decoded := result.decode('utf-8'): - # ID 256 gen 16 top level 5 path @ - for line in decoded.splitlines(): - # expected output format: - # ID 257 gen 8 top level 5 path @home - name = Path(line.split(' ')[-1]) - sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) - subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) - except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) - raise err + try: + if decoded := result.decode('utf-8'): + # ID 256 gen 16 top level 5 path @ + for line in decoded.splitlines(): + # expected output format: + # ID 257 gen 8 top level 5 path @home + name = Path(line.split(' ')[-1]) + sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) + subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) + except json.decoder.JSONDecodeError as err: + log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + raise err if not lsblk_info.mountpoint: self.umount(dev_path) @@ -206,9 +205,7 @@ class DeviceHandler(object): log(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') try: - if (handle := SysCommand(f"/usr/bin/{command} {options_str} {path}")).exit_code != 0: - mkfs_error = handle.decode() - raise DiskError(f'Could not format {path} with {fs_type.value}: {mkfs_error}') + SysCommand(f"/usr/bin/{command} {options_str} {path}") except SysCallError as error: msg = f'Could not format {path} with {fs_type.value}: {error.message}' log(msg, fg='red') @@ -408,12 +405,16 @@ class DeviceHandler(object): SysCommand(f"btrfs subvolume create {subvol_path}") if sub_vol.nodatacow: - if (result := SysCommand(f'chattr +C {subvol_path}')).exit_code != 0: - raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {result.decode()}') + try: + SysCommand(f'chattr +C {subvol_path}') + except SysCallError as error: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {error}') if sub_vol.compress: - if (result := SysCommand(f'chattr +c {subvol_path}')).exit_code != 0: - raise DiskError(f'Could not set compress attribute at {subvol_path}: {result}') + try: + SysCommand(f'chattr +c {subvol_path}') + except SysCallError as error: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {error}') if luks_handler is not None and luks_handler.mapper_dev is not None: self.umount(luks_handler.mapper_dev) @@ -518,9 +519,7 @@ class DeviceHandler(object): log(f'Mounting {dev_path}: command', level=logging.DEBUG) try: - result = SysCommand(command) - if result.exit_code != 0: - raise DiskError(f'Could not mount {dev_path}: {command}\n{result.decode()}') + SysCommand(command) except SysCallError as err: raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}') @@ -575,10 +574,7 @@ class DeviceHandler(object): try: log(f'Calling partprobe: {command}', level=logging.DEBUG) - result = SysCommand(command) - - if result.exit_code != 0: - log(f'"{command}" returned a failure: {result.decode()}', level=logging.DEBUG) + SysCommand(command) except SysCallError as error: log(f'"{command}" failed to run: {error}', level=logging.DEBUG) diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py index d1fb4562..efb0365f 100644 --- a/archinstall/lib/locale_helpers.py +++ b/archinstall/lib/locale_helpers.py @@ -1,7 +1,7 @@ import logging from typing import Iterator, List, Callable, Optional -from .exceptions import ServiceException +from .exceptions import ServiceException, SysCallError from .general import SysCommand from .output import log from .storage import storage @@ -161,8 +161,10 @@ def set_keyboard_language(locale :str) -> bool: log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR) return False - if (output := SysCommand(f'localectl set-keymap {locale}')).exit_code != 0: - raise ServiceException(f"Unable to set locale '{locale}' for console: {output}") + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as error: + raise ServiceException(f"Unable to set locale '{locale}' for console: {error}") return True diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index fc531a06..53a5e8d2 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -88,30 +88,28 @@ class Luks2: 'luksFormat', str(self.luks_dev_path), ]) - try: - # Retry formatting the volume because archinstall can some times be too quick - # which generates a "Device /dev/sdX does not exist or access denied." between - # setting up partitions and us trying to encrypt it. - cmd_handle = None - for i in range(storage['DISK_RETRY_ATTEMPTS']): - if (cmd_handle := SysCommand(cryptsetup_args)).exit_code != 0: - time.sleep(storage['DISK_TIMEOUTS']) - else: - break + # Retry formatting the volume because archinstall can some times be too quick + # which generates a "Device /dev/sdX does not exist or access denied." between + # setting up partitions and us trying to encrypt it. + for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS']): + try: + SysCommand(cryptsetup_args) + break + except SysCallError as error: + time.sleep(storage['DISK_TIMEOUTS']) - if cmd_handle is not None and cmd_handle.exit_code != 0: - output = str(b''.join(cmd_handle)) - raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {output}') - except SysCallError as err: - if err.exit_code == 1: - log(f'luks2 partition currently in use: {self.luks_dev_path}') - log('Attempting to unmount, crypt-close and trying encryption again') + if retry_attempt != storage['DISK_RETRY_ATTEMPTS'] - 1: + continue - self.lock() - # Then try again to set up the crypt-device - SysCommand(cryptsetup_args) - else: - raise err + if error.exit_code == 1: + log(f'luks2 partition currently in use: {self.luks_dev_path}') + log('Attempting to unmount, crypt-close and trying encryption again') + + self.lock() + # Then try again to set up the crypt-device + SysCommand(cryptsetup_args) + else: + raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {error}') return key_file @@ -119,11 +117,7 @@ class Luks2: command = f'/usr/bin/cryptsetup luksUUID {self.luks_dev_path}' try: - result = SysCommand(command) - if result.exit_code != 0: - raise DiskError(f'Unable to get UUID for Luks device: {result.decode()}') - - return result.decode() # type: ignore + return SysCommand(command).decode().strip() # type: ignore except SysCallError as err: log(f'Unable to get UUID for Luks device: {self.luks_dev_path}', level=logging.INFO) raise err diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 15d0fd6b..c6c5c8e4 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from .general import SysCommand from .output import log +from .exceptions import SysCallError from .storage import storage @@ -148,8 +149,9 @@ def re_rank_mirrors( src: str = '/etc/pacman.d/mirrorlist', dst: str = '/etc/pacman.d/mirrorlist', ) -> bool: - cmd = SysCommand(f"/usr/bin/rankmirrors -n {top} {src}") - if cmd.exit_code != 0: + try: + cmd = SysCommand(f"/usr/bin/rankmirrors -n {top} {src}") + except SysCallError: return False with open(dst, 'w') as f: f.write(str(cmd)) diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index 3516aac4..b858daaf 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -38,11 +38,11 @@ def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: def check_mirror_reachable() -> bool: log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO) try: - if run_pacman("-Sy").exit_code == 0: - return True - elif os.geteuid() != 0: - log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") + run_pacman("-Sy") + return True except SysCallError as err: + if os.geteuid() != 0: + log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") log(f'exit_code: {err.exit_code}, Error: {err.message}', level=logging.DEBUG) return False @@ -50,11 +50,12 @@ def check_mirror_reachable() -> bool: def update_keyring() -> bool: log("Updating archlinux-keyring ...", level=logging.INFO) - if run_pacman("-Sy --noconfirm archlinux-keyring").exit_code == 0: + try: + run_pacman("-Sy --noconfirm archlinux-keyring") return True - - elif os.geteuid() != 0: - log("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.", level=logging.ERROR, fg="red") + except SysCallError: + if os.geteuid() != 0: + log("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.", level=logging.ERROR, fg="red") return False @@ -84,8 +85,10 @@ def wireless_scan(interface :str) -> None: if interfaces[interface] != 'WIRELESS': raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") - if not (output := SysCommand(f"iwctl station {interface} scan")).exit_code == 0: - raise SystemError(f"Could not scan for wireless networks: {output}") + try: + SysCommand(f"iwctl station {interface} scan") + except SysCallError as error: + raise SystemError(f"Could not scan for wireless networks: {error}") if '_WIFI' not in storage: storage['_WIFI'] = {} -- cgit v1.2.3-54-g00ecf From 1330c68084580a3ed42a799c3f846cb78a520269 Mon Sep 17 00:00:00 2001 From: Daemon Coder <11915375+codefiles@users.noreply.github.com> Date: Fri, 5 May 2023 01:47:35 -0400 Subject: Fix `LsblkInfo` size field (#1787) --- archinstall/lib/disk/device_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8e26b1d7..d57347b7 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -851,7 +851,7 @@ class LsblkInfo: name: str = '' path: Path = Path() pkname: str = '' - size: Size = Size(0, Unit.B) + size: Size = field(default_factory=lambda: Size(0, Unit.B)) log_sec: int = 0 pttype: str = '' ptuuid: str = '' -- cgit v1.2.3-54-g00ecf From 525c94bf36edcc47b9af4d025785ec9681e3aa37 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 7 May 2023 16:39:25 +1000 Subject: Fix 1793 (#1794) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index d4ad9075..13bde77a 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -10,7 +10,7 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING from parted import ( # type: ignore Disk, Geometry, FileSystem, PartitionException, DiskLabelException, - getAllDevices, freshDisk, Partition, + getAllDevices, freshDisk, Partition, Device ) from .device_model import ( @@ -103,7 +103,8 @@ class DeviceHandler(object): def get_device_by_partition_path(self, partition_path: Path) -> Optional[BDevice]: partition = self.find_partition(partition_path) if partition: - return partition.disk.device + device: Device = partition.disk.device + return self.get_device(Path(device.path)) return None def find_partition(self, path: Path) -> Optional[_PartitionInfo]: -- cgit v1.2.3-54-g00ecf From 89cefb9a1c7d4c4968e7d8645149078e601c9d1c Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 12 May 2023 02:30:09 +1000 Subject: Cleanup imports and unused code (#1801) * Cleanup imports and unused code * Update build check * Keep deprecation exception * Simplify logging --------- Co-authored-by: Daniel Girtler --- .github/workflows/python-build.yml | 5 +- README.md | 2 +- archinstall/__init__.py | 85 +++-- archinstall/default_profiles/desktop.py | 4 +- archinstall/default_profiles/server.py | 9 +- archinstall/lib/boot.py | 111 ++++++ archinstall/lib/configuration.py | 114 +++++- archinstall/lib/disk/device_handler.py | 99 +++--- archinstall/lib/disk/device_model.py | 50 +-- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/lib/disk/fido.py | 11 +- archinstall/lib/disk/filesystem.py | 11 +- archinstall/lib/disk/partitioning_menu.py | 9 +- archinstall/lib/exceptions.py | 23 +- archinstall/lib/general.py | 88 ++--- archinstall/lib/global_menu.py | 36 +- archinstall/lib/hardware.py | 274 +++++++------- archinstall/lib/installer.py | 225 ++++++------ archinstall/lib/interactions/__init__.py | 20 ++ archinstall/lib/interactions/disk_conf.py | 393 +++++++++++++++++++++ archinstall/lib/interactions/general_conf.py | 243 +++++++++++++ archinstall/lib/interactions/locale_conf.py | 43 +++ archinstall/lib/interactions/manage_users_conf.py | 106 ++++++ archinstall/lib/interactions/network_conf.py | 172 +++++++++ archinstall/lib/interactions/system_conf.py | 117 ++++++ archinstall/lib/interactions/utils.py | 34 ++ archinstall/lib/locale.py | 68 ++++ archinstall/lib/locale_helpers.py | 176 --------- archinstall/lib/luks.py | 31 +- archinstall/lib/menu/abstract_menu.py | 9 +- archinstall/lib/menu/menu.py | 27 +- archinstall/lib/mirrors.py | 7 +- archinstall/lib/models/bootloader.py | 9 +- archinstall/lib/models/network_configuration.py | 14 +- archinstall/lib/networking.py | 53 +-- archinstall/lib/output.py | 154 +++++--- archinstall/lib/pacman.py | 7 +- archinstall/lib/plugins.py | 43 ++- archinstall/lib/profile/profile_menu.py | 2 +- archinstall/lib/profile/profiles_handler.py | 21 +- archinstall/lib/services.py | 11 - archinstall/lib/storage.py | 4 +- archinstall/lib/systemd.py | 110 ------ archinstall/lib/translationhandler.py | 14 +- archinstall/lib/user_interaction/__init__.py | 10 - archinstall/lib/user_interaction/disk_conf.py | 391 -------------------- archinstall/lib/user_interaction/general_conf.py | 244 ------------- archinstall/lib/user_interaction/locale_conf.py | 45 --- .../lib/user_interaction/manage_users_conf.py | 106 ------ archinstall/lib/user_interaction/network_conf.py | 173 --------- archinstall/lib/user_interaction/save_conf.py | 113 ------ archinstall/lib/user_interaction/system_conf.py | 117 ------ archinstall/lib/user_interaction/utils.py | 34 -- archinstall/lib/utils/util.py | 4 +- archinstall/scripts/guided.py | 35 +- archinstall/scripts/minimal.py | 30 +- archinstall/scripts/only_hd.py | 29 +- archinstall/scripts/swiss.py | 50 +-- archinstall/scripts/unattended.py | 9 +- examples/full_automated_installation.py | 11 +- examples/interactive_installation.py | 29 +- examples/mac_address_installation.py | 8 +- examples/minimal_installation.py | 21 +- examples/only_hd_installation.py | 6 +- pyproject.toml | 1 + 65 files changed, 2124 insertions(+), 2388 deletions(-) create mode 100644 archinstall/lib/boot.py create mode 100644 archinstall/lib/interactions/__init__.py create mode 100644 archinstall/lib/interactions/disk_conf.py create mode 100644 archinstall/lib/interactions/general_conf.py create mode 100644 archinstall/lib/interactions/locale_conf.py create mode 100644 archinstall/lib/interactions/manage_users_conf.py create mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/system_conf.py create mode 100644 archinstall/lib/interactions/utils.py create mode 100644 archinstall/lib/locale.py delete mode 100644 archinstall/lib/locale_helpers.py delete mode 100644 archinstall/lib/services.py delete mode 100644 archinstall/lib/systemd.py delete mode 100644 archinstall/lib/user_interaction/__init__.py delete mode 100644 archinstall/lib/user_interaction/disk_conf.py delete mode 100644 archinstall/lib/user_interaction/general_conf.py delete mode 100644 archinstall/lib/user_interaction/locale_conf.py delete mode 100644 archinstall/lib/user_interaction/manage_users_conf.py delete mode 100644 archinstall/lib/user_interaction/network_conf.py delete mode 100644 archinstall/lib/user_interaction/save_conf.py delete mode 100644 archinstall/lib/user_interaction/system_conf.py delete mode 100644 archinstall/lib/user_interaction/utils.py (limited to 'archinstall/lib/disk') diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index f98ce160..950ff8f4 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -36,7 +36,10 @@ jobs: - name: Run archinstall run: | python -V - archinstall -v + archinstall --script guided -v + archinstall --script swiss -v + archinstall --script only_hd -v + archinstall --script minimal -v - uses: actions/upload-artifact@v3 with: name: archinstall diff --git a/README.md b/README.md index 720bd487..15646170 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ from archinstall.default_profiles.minimal import MinimalProfile from archinstall.lib.disk.device_model import FilesystemType from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu from archinstall.lib.disk.filesystem import FilesystemHandler -from archinstall.lib.user_interaction.disk_conf import select_disk_config +from archinstall.lib.interactions.disk_conf import select_disk_config fs_type = FilesystemType('ext4') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 6f67d20f..992bd9fa 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,42 +1,75 @@ """Arch Linux installer - guided, templates etc.""" import importlib +import os from argparse import ArgumentParser, Namespace +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Union from .lib import disk from .lib import menu from .lib import models from .lib import packages - -from .lib.exceptions import * -from .lib.general import * -from .lib.hardware import * -from .lib.installer import __packages__, Installer, accessibility_tools_in_use -from .lib.locale_helpers import * -from .lib.luks import * -from .lib.mirrors import * -from .lib.networking import * -from .lib.output import * -from archinstall.lib.profile.profiles_handler import ProfileHandler, profile_handler -from .lib.profile.profile_menu import ProfileConfiguration -from .lib.services import * -from .lib.storage import * -from .lib.systemd import * -from .lib.user_interaction import * +from .lib import exceptions +from .lib import luks +from .lib import locale +from .lib import mirrors +from .lib import networking +from .lib import profile +from .lib import interactions +from . import default_profiles + +from .lib.hardware import SysInfo, AVAILABLE_GFX_DRIVERS +from .lib.installer import Installer, accessibility_tools_in_use +from .lib.output import ( + FormattedOutput, log, error, + check_log_permissions, debug, warn, info +) +from .lib.storage import storage from .lib.global_menu import GlobalMenu -from .lib.translationhandler import TranslationHandler, DeferredTranslation -from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony -from .lib.configuration import * +from .lib.boot import Boot +from .lib.translationhandler import TranslationHandler, Language, DeferredTranslation +from .lib.plugins import plugins, load_plugin +from .lib.configuration import ConfigurationOutput +from .lib.general import ( + generate_password, locate_binary, clear_vt100_escape_codes, + JsonEncoder, JSON, UNSAFE_JSON, SysCommandWorker, SysCommand, + run_custom_user_commands, json_stream_to_structure, secret +) + + +if TYPE_CHECKING: + _: Any -parser = ArgumentParser() __version__ = "2.5.6" storage['__version__'] = __version__ + # add the custome _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() +check_log_permissions() + +# Log various information about hardware before starting the installation. This might assist in troubleshooting +debug(f"Hardware model detected: {SysInfo.sys_vendor()} {SysInfo.product_name()}; UEFI mode: {SysInfo.has_uefi()}") +debug(f"Processor model detected: {SysInfo.cpu_model()}") +debug(f"Memory statistics: {SysInfo.mem_available()} available out of {SysInfo.mem_total()} total installed") +debug(f"Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_vm()}") +debug(f"Graphics devices detected: {SysInfo._graphics_devices().keys()}") + +# For support reasons, we'll log the disk layout pre installation to match against post-installation layout +debug(f"Disk states before installing: {disk.disk_layouts()}") + + +if os.getuid() != 0: + print(_("Archinstall requires root privileges to run. See --help for more.")) + exit(1) + + +parser = ArgumentParser() + def define_arguments(): """ @@ -61,7 +94,7 @@ def define_arguments(): parser.add_argument("--plugin", nargs="?", type=str) -def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :bool = False) -> dict: +def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, err :bool = False) -> dict: """We accept arguments not defined to the parser. (arguments "ad hoc"). Internally argparse return to us a list of words so we have to parse its contents, manually. We accept following individual syntax for each argument @@ -105,14 +138,14 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro config[last_key] = [config[last_key],element] else: config[last_key].append(element) - elif error: + elif err: raise ValueError(f"Entry {element} is not related to any argument") else: print(f" We ignore the entry {element} as it isn't related to any argument") return config -def cleanup_empty_args(args: Union[Namespace, dict]) -> dict: +def cleanup_empty_args(args: Union[Namespace, Dict]) -> Dict: """ Takes arguments (dictionary or argparse Namespace) and removes any None values. This ensures clean mergers during dict.update(args) @@ -190,14 +223,14 @@ def load_config(): arguments['disk_config'] = disk.DiskLayoutConfiguration.parse_arg(disk_config) if profile_config := arguments.get('profile_config', None): - arguments['profile_config'] = ProfileConfiguration.parse_arg(profile_config) + arguments['profile_config'] = profile.ProfileConfiguration.parse_arg(profile_config) if arguments.get('mirror-region', None) is not None: if type(arguments.get('mirror-region', None)) is dict: arguments['mirror-region'] = arguments.get('mirror-region', None) else: selected_region = arguments.get('mirror-region', None) - arguments['mirror-region'] = {selected_region: list_mirrors()[selected_region]} + arguments['mirror-region'] = {selected_region: mirrors.list_mirrors()[selected_region]} if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) @@ -230,7 +263,7 @@ def post_process_arguments(arguments): storage['MOUNT_POINT'] = Path(mountpoint) if arguments.get('debug', False): - log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) + warn(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!") if arguments.get('plugin', None): path = arguments['plugin'] diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index 2351bd08..9d92f822 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -1,7 +1,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Dict from archinstall.lib import menu -from archinstall.lib.output import log +from archinstall.lib.output import info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, GreeterType @@ -79,7 +79,7 @@ class DesktopProfile(Profile): install_session.add_additional_packages(self.packages) for profile in self._current_selection: - log(f'Installing profile {profile.name}...') + info(f'Installing profile {profile.name}...') install_session.add_additional_packages(profile.packages) install_session.enable_service(profile.services) diff --git a/archinstall/default_profiles/server.py b/archinstall/default_profiles/server.py index e240b3ef..ab758975 100644 --- a/archinstall/default_profiles/server.py +++ b/archinstall/default_profiles/server.py @@ -1,7 +1,6 @@ -import logging from typing import Any, TYPE_CHECKING, List -from archinstall.lib.output import log +from archinstall.lib.output import info from archinstall.lib.menu import MenuSelectionType from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.default_profiles.profile import ProfileType, Profile, SelectResult, TProfile @@ -46,12 +45,12 @@ class ServerProfile(Profile): def install(self, install_session: 'Installer'): server_info = self.current_selection_names() details = ', '.join(server_info) - log(f'Now installing the selected servers: {details}', level=logging.INFO) + info(f'Now installing the selected servers: {details}') for server in self._current_selection: - log(f'Installing {server.name}...', level=logging.INFO) + info(f'Installing {server.name}...') install_session.add_additional_packages(server.packages) install_session.enable_service(server.services) server.install(install_session) - log('If your selections included multiple servers with the same port, you may have to reconfigure them.', fg="yellow", level=logging.INFO) + info('If your selections included multiple servers with the same port, you may have to reconfigure them.') diff --git a/archinstall/lib/boot.py b/archinstall/lib/boot.py new file mode 100644 index 00000000..62c50df3 --- /dev/null +++ b/archinstall/lib/boot.py @@ -0,0 +1,111 @@ +import time +from typing import Iterator, Optional +from .exceptions import SysCallError +from .general import SysCommand, SysCommandWorker, locate_binary +from .installer import Installer +from .output import error +from .storage import storage + + +class Boot: + def __init__(self, installation: Installer): + self.instance = installation + self.container_name = 'archinstall' + self.session: Optional[SysCommandWorker] = None + self.ready = False + + def __enter__(self) -> 'Boot': + if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: + raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") + + if existing_session: + self.session = existing_session.session + self.ready = existing_session.ready + else: + # '-P' or --console=pipe could help us not having to do a bunch + # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. + self.session = SysCommandWorker([ + '/usr/bin/systemd-nspawn', + '-D', str(self.instance.target), + '--timezone=off', + '-b', + '--no-pager', + '--machine', self.container_name + ]) + + if not self.ready and self.session: + while self.session.is_alive(): + if b' login:' in self.session: + self.ready = True + break + + storage['active_boot'] = self + return self + + def __exit__(self, *args :str, **kwargs :str) -> None: + # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. + # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager + + if len(args) >= 2 and args[1]: + error( + args[1], + f"The error above occurred in a temporary boot-up of the installation {self.instance}" + ) + + shutdown = None + shutdown_exit_code: Optional[int] = -1 + + try: + shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') + except SysCallError as err: + shutdown_exit_code = err.exit_code + + if self.session: + while self.session.is_alive(): + time.sleep(0.25) + + if shutdown and shutdown.exit_code: + shutdown_exit_code = shutdown.exit_code + + if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): + storage['active_boot'] = None + else: + session_exit_code = self.session.exit_code if self.session else -1 + + raise SysCallError( + f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", + exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) + ) + + def __iter__(self) -> Iterator[bytes]: + if self.session: + for value in self.session: + yield value + + def __contains__(self, key: bytes) -> bool: + if self.session is None: + return False + + return key in self.session + + def is_alive(self) -> bool: + if self.session is None: + return False + + return self.session.is_alive() + + def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand: + if cmd[0][0] != '/' and cmd[0][:2] != './': + # This check is also done in SysCommand & SysCommandWorker. + # However, that check is done for `machinectl` and not for our chroot command. + # So this wrapper for SysCommand will do this additionally. + + cmd[0] = locate_binary(cmd[0]) + + return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) + + def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker: + if cmd[0][0] != '/' and cmd[0][:2] != './': + cmd[0] = locate_binary(cmd[0]) + + return SysCommandWorker(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 22c41c0d..c3af3a83 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -1,13 +1,13 @@ import os import json import stat -import logging from pathlib import Path from typing import Optional, Dict, Any, TYPE_CHECKING +from .menu import Menu, MenuSelectionType from .storage import storage -from .general import JSON, UNSAFE_JSON -from .output import log +from .general import JSON, UNSAFE_JSON, SysCommand +from .output import debug, info, warn if TYPE_CHECKING: _: Any @@ -69,18 +69,18 @@ class ConfigurationOutput: def show(self): print(_('\nThis is your chosen configuration:')) - log(" -- Chosen configuration --", level=logging.DEBUG) + debug(" -- Chosen configuration --") user_conig = self.user_config_to_json() - log(user_conig, level=logging.INFO) + info(user_conig) print() def _is_valid_path(self, dest_path: Path) -> bool: if (not dest_path.exists()) or not (dest_path.is_dir()): - log( - 'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()), - fg="yellow" + warn( + f'Destination directory {dest_path.resolve()} does not exist or is not a directory\n.', + 'Configuration files can not be saved' ) return False return True @@ -111,3 +111,101 @@ class ConfigurationOutput: if self._is_valid_path(dest_path): self.save_user_config(dest_path) self.save_user_creds(dest_path) + + +def save_config(config: Dict): + def preview(selection: str): + if options['user_config'] == selection: + serialized = config_output.user_config_to_json() + return f'{config_output.user_configuration_file}\n{serialized}' + elif options['user_creds'] == selection: + if maybe_serial := config_output.user_credentials_to_json(): + return f'{config_output.user_credentials_file}\n{maybe_serial}' + else: + return str(_('No configuration')) + elif options['all'] == selection: + output = f'{config_output.user_configuration_file}\n' + if config_output.user_credentials_to_json(): + output += f'{config_output.user_credentials_file}\n' + return output[:-1] + return None + + config_output = ConfigurationOutput(config) + + options = { + 'user_config': str(_('Save user configuration')), + 'user_creds': str(_('Save user credentials')), + 'disk_layout': str(_('Save disk layout')), + 'all': str(_('Save all')) + } + + choice = Menu( + _('Choose which configuration to save'), + list(options.values()), + sort=False, + skip=True, + preview_size=0.75, + preview_command=preview + ).run() + + if choice.type_ == MenuSelectionType.Skip: + return + + save_config_value = choice.single_value + saving_key = [k for k, v in options.items() if v == save_config_value][0] + + dirs_to_exclude = [ + '/bin', + '/dev', + '/lib', + '/lib64', + '/lost+found', + '/opt', + '/proc', + '/run', + '/sbin', + '/srv', + '/sys', + '/usr', + '/var', + ] + + debug('Ignore configuration option folders: ' + ','.join(dirs_to_exclude)) + info(_('Finding possible directories to save configuration files ...')) + + find_exclude = '-path ' + ' -prune -o -path '.join(dirs_to_exclude) + ' -prune ' + file_picker_command = f'find / {find_exclude} -o -type d -print0' + + directories = SysCommand(file_picker_command).decode() + + if directories is None: + raise ValueError('Failed to retrieve possible configuration directories') + + possible_save_dirs = list(filter(None, directories.split('\x00'))) + + selection = Menu( + _('Select directory (or directories) for saving configuration files'), + possible_save_dirs, + multi=True, + skip=True, + allow_reset=False, + ).run() + + match selection.type_: + case MenuSelectionType.Skip: + return + + save_dirs = selection.multi_value + + debug(f'Saving {saving_key} configuration files to {save_dirs}') + + if save_dirs is not None: + for save_dir_str in save_dirs: + save_dir = Path(save_dir_str) + if options['user_config'] == save_config_value: + config_output.save_user_config(save_dir) + elif options['user_creds'] == save_config_value: + config_output.save_user_creds(save_dir) + elif options['all'] == save_config_value: + config_output.save_user_config(save_dir) + config_output.save_user_creds(save_dir) diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 13bde77a..4341c53c 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import logging import os import time from pathlib import Path @@ -24,7 +23,7 @@ from .device_model import ( from ..exceptions import DiskError, UnknownFilesystemFormat from ..general import SysCommand, SysCallError, JSON from ..luks import Luks2 -from ..output import log +from ..output import debug, error, info, warn from ..utils.util import is_subpath if TYPE_CHECKING: @@ -48,11 +47,11 @@ class DeviceHandler(object): for device in getAllDevices(): try: disk = Disk(device) - except DiskLabelException as error: - if 'unrecognised disk label' in getattr(error, 'message', str(error)): + except DiskLabelException as err: + if 'unrecognised disk label' in getattr(error, 'message', str(err)): disk = freshDisk(device, PartitionTable.GPT.value) else: - log(f'Unable to get disk from device: {device}', level=logging.DEBUG) + debug(f'Unable to get disk from device: {device}') continue device_info = _DeviceInfo.from_disk(disk) @@ -93,7 +92,7 @@ class DeviceHandler(object): return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None return None except ValueError: - log(f'Could not determine the filesystem: {partition.fileSystem}', level=logging.DEBUG) + debug(f'Could not determine the filesystem: {partition.fileSystem}') return None @@ -137,7 +136,7 @@ class DeviceHandler(object): try: result = SysCommand(f'btrfs subvolume list {mountpoint}') except SysCallError as err: - log(f'Failed to read btrfs subvolume information: {err}', level=logging.DEBUG) + debug(f'Failed to read btrfs subvolume information: {err}') return subvol_infos try: @@ -150,7 +149,7 @@ class DeviceHandler(object): sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + error(f"Could not decode lsblk JSON: {result}") raise err if not lsblk_info.mountpoint: @@ -203,14 +202,14 @@ class DeviceHandler(object): options += additional_parted_options options_str = ' '.join(options) - log(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + info(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') try: SysCommand(f"/usr/bin/{command} {options_str} {path}") - except SysCallError as error: - msg = f'Could not format {path} with {fs_type.value}: {error.message}' - log(msg, fg='red') - raise DiskError(msg) from error + except SysCallError as err: + msg = f'Could not format {path} with {fs_type.value}: {err.message}' + error(msg) + raise DiskError(msg) from err def _perform_enc_formatting( self, @@ -227,16 +226,16 @@ class DeviceHandler(object): key_file = luks_handler.encrypt() - log(f'Unlocking luks2 device: {dev_path}', level=logging.DEBUG) + debug(f'Unlocking luks2 device: {dev_path}') luks_handler.unlock(key_file=key_file) if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') - log(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}', level=logging.INFO) + info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}') self._perform_formatting(fs_type, luks_handler.mapper_dev) - log(f'luks2 locking device: {dev_path}', level=logging.INFO) + info(f'luks2 locking device: {dev_path}') luks_handler.lock() def format( @@ -285,7 +284,7 @@ class DeviceHandler(object): # when we require a delete and the partition to be (re)created # already exists then we have to delete it first if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]: - log(f'Delete existing partition: {part_mod.safe_dev_path}', level=logging.INFO) + info(f'Delete existing partition: {part_mod.safe_dev_path}') part_info = self.find_partition(part_mod.safe_dev_path) if not part_info: @@ -325,9 +324,9 @@ class DeviceHandler(object): for flag in part_mod.flags: partition.setFlag(flag.value) - log(f'\tType: {part_mod.type.value}', level=logging.DEBUG) - log(f'\tFilesystem: {part_mod.fs_type.value}', level=logging.DEBUG) - log(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length', level=logging.DEBUG) + debug(f'\tType: {part_mod.type.value}') + debug(f'\tFilesystem: {part_mod.fs_type.value}') + debug(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length') try: disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint) @@ -339,41 +338,41 @@ class DeviceHandler(object): # the partition has a real path now as it was created part_mod.dev_path = Path(partition.path) - info = self._fetch_partuuid(part_mod.dev_path) + lsblk_info = self._fetch_partuuid(part_mod.dev_path) - part_mod.partuuid = info.partuuid - part_mod.uuid = info.uuid + part_mod.partuuid = lsblk_info.partuuid + part_mod.uuid = lsblk_info.uuid except PartitionException as ex: raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex def _fetch_partuuid(self, path: Path) -> LsblkInfo: attempts = 3 - info: Optional[LsblkInfo] = None + lsblk_info: Optional[LsblkInfo] = None self.partprobe(path) for attempt_nr in range(attempts): time.sleep(attempt_nr + 1) - info = get_lsblk_info(path) + lsblk_info = get_lsblk_info(path) - if info.partuuid: + if lsblk_info.partuuid: break self.partprobe(path) - if not info or not info.partuuid: - log(f'Unable to determine new partition uuid: {path}\n{info}', level=logging.DEBUG) + if not lsblk_info or not lsblk_info.partuuid: + debug(f'Unable to determine new partition uuid: {path}\n{lsblk_info}') raise DiskError(f'Unable to determine new partition uuid: {path}') - log(f'partuuid found: {info.json()}', level=logging.DEBUG) + debug(f'partuuid found: {lsblk_info.json()}') - return info + return lsblk_info def create_btrfs_volumes( self, part_mod: PartitionModification, enc_conf: Optional['DiskEncryption'] = None ): - log(f'Creating subvolumes: {part_mod.safe_dev_path}', level=logging.INFO) + info(f'Creating subvolumes: {part_mod.safe_dev_path}') luks_handler = None @@ -396,7 +395,7 @@ class DeviceHandler(object): self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) for sub_vol in part_mod.btrfs_subvols: - log(f'Creating subvolume: {sub_vol.name}', level=logging.DEBUG) + debug(f'Creating subvolume: {sub_vol.name}') if luks_handler is not None: subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name @@ -408,14 +407,14 @@ class DeviceHandler(object): if sub_vol.nodatacow: try: SysCommand(f'chattr +C {subvol_path}') - except SysCallError as error: - raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {error}') + except SysCallError as err: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') if sub_vol.compress: try: SysCommand(f'chattr +c {subvol_path}') - except SysCallError as error: - raise DiskError(f'Could not set compress attribute at {subvol_path}: {error}') + except SysCallError as err: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') if luks_handler is not None and luks_handler.mapper_dev is not None: self.umount(luks_handler.mapper_dev) @@ -435,12 +434,12 @@ class DeviceHandler(object): return luks_handler def _umount_all_existing(self, modification: DeviceModification): - log(f'Unmounting all partitions: {modification.device_path}', level=logging.INFO) + info(f'Unmounting all partitions: {modification.device_path}') existing_partitions = self._devices[modification.device_path].partition_infos for partition in existing_partitions: - log(f'Unmounting: {partition.path}', level=logging.DEBUG) + debug(f'Unmounting: {partition.path}') # un-mount for existing encrypted partitions if partition.fs_type == FilesystemType.Crypto_luks: @@ -472,10 +471,10 @@ class DeviceHandler(object): part_table = partition_table.value if partition_table else None disk = freshDisk(modification.device.disk.device, part_table) else: - log(f'Use existing device: {modification.device_path}') + info(f'Use existing device: {modification.device_path}') disk = modification.device.disk - log(f'Creating partitions: {modification.device_path}') + info(f'Creating partitions: {modification.device_path}') # TODO sort by delete first @@ -507,7 +506,7 @@ class DeviceHandler(object): lsblk_info = get_lsblk_info(dev_path) if target_mountpoint in lsblk_info.mountpoints: - log(f'Device already mounted at {target_mountpoint}') + info(f'Device already mounted at {target_mountpoint}') return str_options = ','.join(options) @@ -517,7 +516,7 @@ class DeviceHandler(object): command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' - log(f'Mounting {dev_path}: command', level=logging.DEBUG) + debug(f'Mounting {dev_path}: command') try: SysCommand(command) @@ -536,10 +535,10 @@ class DeviceHandler(object): raise ex if len(lsblk_info.mountpoints) > 0: - log(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}', level=logging.DEBUG) + debug(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}') for mountpoint in lsblk_info.mountpoints: - log(f'Unmounting mountpoint: {mountpoint}', level=logging.DEBUG) + debug(f'Unmounting mountpoint: {mountpoint}') command = 'umount' @@ -574,10 +573,10 @@ class DeviceHandler(object): command = 'partprobe' try: - log(f'Calling partprobe: {command}', level=logging.DEBUG) + debug(f'Calling partprobe: {command}') SysCommand(command) - except SysCallError as error: - log(f'"{command}" failed to run: {error}', level=logging.DEBUG) + except SysCallError as err: + error(f'"{command}" failed to run: {err}') def _wipe(self, dev_path: Path): """ @@ -594,7 +593,7 @@ class DeviceHandler(object): This is not intended to be secure, but rather to ensure that auto-discovery tools don't recognize anything here. """ - log(f'Wiping partitions and metadata: {block_device.device_info.path}') + info(f'Wiping partitions and metadata: {block_device.device_info.path}') for partition in block_device.partition_infos: self._wipe(partition.path) @@ -609,8 +608,8 @@ def disk_layouts() -> str: lsblk_info = get_all_lsblk_info() return json.dumps(lsblk_info, indent=4, sort_keys=True, cls=JSON) except SysCallError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not return disk layouts: {err}") return '' except json.decoder.JSONDecodeError as err: - log(f"Could not return disk layouts: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not return disk layouts: {err}") return '' diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index d57347b7..36dd0c4f 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -2,7 +2,6 @@ from __future__ import annotations import dataclasses import json -import logging import math import time import uuid @@ -18,7 +17,7 @@ from parted import Disk, Geometry, Partition from ..exceptions import DiskError, SysCallError from ..general import SysCommand -from ..output import log +from ..output import debug, error from ..storage import storage if TYPE_CHECKING: @@ -282,7 +281,7 @@ class _PartitionInfo: btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) def as_json(self) -> Dict[str, Any]: - info = { + part_info = { 'Name': self.name, 'Type': self.type.value, 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), @@ -293,9 +292,9 @@ class _PartitionInfo: } if self.btrfs_subvol_infos: - info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' + part_info['Btrfs vol.'] = f'{len(self.btrfs_subvol_infos)} subvolumes' - return info + return part_info @classmethod def from_partition( @@ -392,7 +391,7 @@ class SubvolumeModification: mods = [] for entry in subvol_args: if not entry.get('name', None) or not entry.get('mountpoint', None): - log(f'Subvolume arg is missing name: {entry}', level=logging.DEBUG) + debug(f'Subvolume arg is missing name: {entry}') continue mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None @@ -705,7 +704,7 @@ class PartitionModification: """ Called for displaying data in table format """ - info = { + part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, @@ -718,9 +717,9 @@ class PartitionModification: } if self.btrfs_subvols: - info['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' + part_mod['Btrfs vol.'] = f'{len(self.btrfs_subvols)} subvolumes' - return info + return part_mod @dataclass @@ -916,36 +915,36 @@ class LsblkInfo: @classmethod def from_json(cls, blockdevice: Dict[str, Any]) -> LsblkInfo: - info = cls() + lsblk_info = cls() for f in cls.fields(): lsblk_field = _clean_field(f, CleanType.Blockdevice) data_field = _clean_field(f, CleanType.Dataclass) val: Any = None - if isinstance(getattr(info, data_field), Path): + if isinstance(getattr(lsblk_info, data_field), Path): val = Path(blockdevice[lsblk_field]) - elif isinstance(getattr(info, data_field), Size): + elif isinstance(getattr(lsblk_info, data_field), Size): val = Size(blockdevice[lsblk_field], Unit.B) else: val = blockdevice[lsblk_field] - setattr(info, data_field, val) + setattr(lsblk_info, data_field, val) - info.children = [LsblkInfo.from_json(child) for child in blockdevice.get('children', [])] + lsblk_info.children = [LsblkInfo.from_json(child) for child in blockdevice.get('children', [])] # sometimes lsblk returns 'mountpoints': [null] - info.mountpoints = [Path(mnt) for mnt in info.mountpoints if mnt] + lsblk_info.mountpoints = [Path(mnt) for mnt in lsblk_info.mountpoints if mnt] fs_roots = [] - for r in info.fsroots: + for r in lsblk_info.fsroots: if r: path = Path(r) # store the fsroot entries without the leading / fs_roots.append(path.relative_to(path.anchor)) - info.fsroots = fs_roots + lsblk_info.fsroots = fs_roots - return info + return lsblk_info class CleanType(Enum): @@ -978,16 +977,16 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = try: result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') break - except SysCallError as error: + except SysCallError as err: # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - if error.worker: - err = error.worker.decode('UTF-8') - log(f'Error calling lsblk: {err}', level=logging.DEBUG) + if err.worker: + err_str = err.worker.decode('UTF-8') + debug(f'Error calling lsblk: {err_str}') else: - raise error + raise err if retry_attempt == retry - 1: - raise error + raise err time.sleep(1) @@ -997,11 +996,12 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = blockdevices = block_devices['blockdevices'] return [LsblkInfo.from_json(device) for device in blockdevices] except json.decoder.JSONDecodeError as err: - log(f"Could not decode lsblk JSON: {result}", fg="red", level=logging.ERROR) + error(f"Could not decode lsblk JSON: {result}") raise err raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: if infos := _fetch_lsblk_info(dev_path): return infos[0] diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 285270fb..8c64e65e 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -13,7 +13,7 @@ from ..menu import ( MenuSelectionType, TableMenu ) -from ..user_interaction.utils import get_password +from ..interactions.utils import get_password from ..menu import Menu from ..general import secret from .fido import Fido2Device, Fido2 diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 2a53b551..97c38d84 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -1,13 +1,12 @@ from __future__ import annotations import getpass -import logging from pathlib import Path from typing import List, Optional from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes -from ..output import log +from ..output import error, info class Fido2: @@ -39,7 +38,7 @@ class Fido2: if not cls._loaded or reload: ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') if not ret: - log('Unable to retrieve fido2 devices', level=logging.ERROR) + error('Unable to retrieve fido2 devices') return [] fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore @@ -88,8 +87,4 @@ class Fido2: worker.write(bytes(getpass.getpass(" "), 'UTF-8')) pin_inputted = True - log( - f"You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds.", - level=logging.INFO, - fg="yellow" - ) + info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds') diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 6ea99340..dc99afce 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import signal import sys import time @@ -8,8 +7,8 @@ from typing import Any, Optional, TYPE_CHECKING from .device_model import DiskLayoutConfiguration, DiskLayoutType, PartitionTable, FilesystemType, DiskEncryption from .device_handler import device_handler -from ..hardware import has_uefi -from ..output import log +from ..hardware import SysInfo +from ..output import debug from ..menu import Menu if TYPE_CHECKING: @@ -27,13 +26,13 @@ class FilesystemHandler: def perform_filesystem_operations(self, show_countdown: bool = True): if self._disk_config.config_type == DiskLayoutType.Pre_mount: - log('Disk layout configuration is set to pre-mount, not performing any operations', level=logging.DEBUG) + debug('Disk layout configuration is set to pre-mount, not performing any operations') return device_mods = list(filter(lambda x: len(x.partitions) > 0, self._disk_config.device_modifications)) if not device_mods: - log('No modifications required', level=logging.DEBUG) + debug('No modifications required') return device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods]) @@ -48,7 +47,7 @@ class FilesystemHandler: # Setup the blockdevice, filesystem (and optionally encryption). # Once that's done, we'll hand over to perform_installation() partition_table = PartitionTable.GPT - if has_uefi() is False: + if SysInfo.has_uefi() is False: partition_table = PartitionTable.MBR for mod in device_mods: diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 686e8c29..89cf6293 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -1,13 +1,12 @@ from __future__ import annotations -import logging from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus from ..menu import Menu, ListManager, MenuSelection, TextInput -from ..output import FormattedOutput, log +from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu if TYPE_CHECKING: @@ -229,7 +228,7 @@ class PartitioningList(ListManager): if not start_sector or self._validate_sector(start_sector): break - log(f'Invalid start sector entered: {start_sector}', fg='red', level=logging.INFO) + warn(f'Invalid start sector entered: {start_sector}') if not start_sector: start_sector = str(largest_free_area.start) @@ -245,7 +244,7 @@ class PartitioningList(ListManager): if not end_value or self._validate_sector(start_sector, end_value): break - log(f'Invalid end sector entered: {start_sector}', fg='red', level=logging.INFO) + warn(f'Invalid end sector entered: {start_sector}') # override the default value with the user value if end_value: @@ -300,7 +299,7 @@ class PartitioningList(ListManager): if choice.value == Menu.no(): return [] - from ..user_interaction.disk_conf import suggest_single_disk_layout + from ..interactions.disk_conf import suggest_single_disk_layout device_modification = suggest_single_disk_layout(self._device) return device_modification.partitions diff --git a/archinstall/lib/exceptions.py b/archinstall/lib/exceptions.py index a66e4e04..53458d2c 100644 --- a/archinstall/lib/exceptions.py +++ b/archinstall/lib/exceptions.py @@ -3,6 +3,7 @@ from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from .general import SysCommandWorker + class RequirementError(BaseException): pass @@ -15,10 +16,6 @@ class UnknownFilesystemFormat(BaseException): pass -class ProfileError(BaseException): - pass - - class SysCallError(BaseException): def __init__(self, message :str, exit_code :Optional[int] = None, worker :Optional['SysCommandWorker'] = None) -> None: super(SysCallError, self).__init__(message) @@ -27,22 +24,10 @@ class SysCallError(BaseException): self.worker = worker -class PermissionError(BaseException): - pass - - -class ProfileNotFound(BaseException): - pass - - class HardwareIncompatibilityError(BaseException): pass -class UserError(BaseException): - pass - - class ServiceException(BaseException): pass @@ -51,9 +36,5 @@ class PackageError(BaseException): pass -class TranslationError(BaseException): - pass - - class Deprecated(BaseException): - pass \ No newline at end of file + pass diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 997b7d67..777ee90e 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -1,8 +1,6 @@ from __future__ import annotations -import hashlib import json -import logging import os import secrets import shlex @@ -18,9 +16,10 @@ import urllib.error import pathlib from datetime import datetime, date from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING +from select import epoll, EPOLLIN, EPOLLHUP from .exceptions import RequirementError, SysCallError -from .output import log +from .output import debug, error, info from .storage import storage @@ -28,42 +27,6 @@ if TYPE_CHECKING: from .installer import Installer -if sys.platform == 'linux': - from select import epoll, EPOLLIN, EPOLLHUP -else: - import select - EPOLLIN = 0 - EPOLLHUP = 0 - - class epoll(): - """ #!if windows - Create a epoll() implementation that simulates the epoll() behavior. - This so that the rest of the code doesn't need to worry weither we're using select() or epoll(). - """ - def __init__(self) -> None: - self.sockets: Dict[str, Any] = {} - self.monitoring: Dict[int, Any] = {} - - def unregister(self, fileno :int, *args :List[Any], **kwargs :Dict[str, Any]) -> None: - try: - del(self.monitoring[fileno]) # noqa: E275 - except: - pass - - def register(self, fileno :int, *args :int, **kwargs :Dict[str, Any]) -> None: - self.monitoring[fileno] = True - - def poll(self, timeout: float = 0.05, *args :str, **kwargs :Dict[str, Any]) -> List[Any]: - try: - return [[fileno, 1] for fileno in select.select(list(self.monitoring.keys()), [], [], timeout)[0]] - except OSError: - return [] - - -def gen_uid(entropy_length :int = 256) -> str: - return hashlib.sha512(os.urandom(entropy_length)).hexdigest() - - def generate_password(length :int = 64) -> str: haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) @@ -156,6 +119,7 @@ class JsonEncoder: else: return JsonEncoder._encode(obj) + class JSON(json.JSONEncoder, json.JSONDecoder): """ A safe JSON encoder that will omit private information in dicts (starting with !) @@ -166,6 +130,7 @@ class JSON(json.JSONEncoder, json.JSONDecoder): def encode(self, obj :Any) -> Any: return super(JSON, self).encode(self._encode(obj)) + class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder): """ UNSAFE_JSON will call/encode and keep private information in dicts (starting with !) @@ -269,7 +234,7 @@ class SysCommandWorker: sys.stdout.flush() if len(args) >= 2 and args[1]: - log(args[1], level=logging.DEBUG, fg='red') + debug(args[1]) if self.exit_code != 0: raise SysCallError( @@ -350,7 +315,7 @@ class SysCommandWorker: self.ended = time.time() break - if self.ended or (got_output is False and pid_exists(self.pid) is False): + if self.ended or (got_output is False and _pid_exists(self.pid) is False): self.ended = time.time() try: wait_status = os.waitpid(self.pid, 0)[1] @@ -396,15 +361,15 @@ class SysCommandWorker: pass except Exception as e: exception_type = type(e).__name__ - log(f"Unexpected {exception_type} occurred in {self.cmd}: {e}", level=logging.ERROR) + error(f"Unexpected {exception_type} occurred in {self.cmd}: {e}") raise e os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars}) if storage['arguments'].get('debug'): - log(f"Executing: {self.cmd}", level=logging.DEBUG) + debug(f"Executing: {self.cmd}") except FileNotFoundError: - log(f"{self.cmd[0]} does not exist.", level=logging.ERROR, fg="red") + error(f"{self.cmd[0]} does not exist.") self.exit_code = 1 return False else: @@ -455,7 +420,7 @@ class SysCommand: # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') + error(args[1]) def __iter__(self, *args :List[Any], **kwargs :Dict[str, Any]) -> Iterator[bytes]: if self.session: @@ -535,22 +500,7 @@ class SysCommand: return None -def prerequisite_check() -> bool: - """ - This function is used as a safety check before - continuing with an installation. - - Could be anything from checking that /boot is big enough - to check if nvidia hardware exists when nvidia driver was chosen. - """ - - return True - -def reboot(): - SysCommand("/usr/bin/reboot") - - -def pid_exists(pid: int) -> bool: +def _pid_exists(pid: int) -> bool: try: return any(subprocess.check_output(['/usr/bin/ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip()) except subprocess.CalledProcessError: @@ -559,7 +509,7 @@ def pid_exists(pid: int) -> bool: def run_custom_user_commands(commands :List[str], installation :Installer) -> None: for index, command in enumerate(commands): - log(f'Executing custom command "{command}" ...', level=logging.INFO) + info(f'Executing custom command "{command}" ...') with open(f"{installation.target}/var/tmp/user-command.{index}.sh", "w") as temp_script: temp_script.write(command) @@ -568,6 +518,7 @@ def run_custom_user_commands(commands :List[str], installation :Installer) -> No os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh") + def json_stream_to_structure(configuration_identifier : str, stream :str, target :dict) -> bool : """ Function to load a stream (file (as name) or valid JSON string into an existing dictionary @@ -582,16 +533,16 @@ def json_stream_to_structure(configuration_identifier : str, stream :str, target try: with urllib.request.urlopen(urllib.request.Request(stream, headers={'User-Agent': 'ArchInstall'})) as response: target.update(json.loads(response.read())) - except urllib.error.HTTPError as error: - log(f"Could not load {configuration_identifier} via {parsed_url} due to: {error}", level=logging.ERROR, fg="red") + except urllib.error.HTTPError as err: + error(f"Could not load {configuration_identifier} via {parsed_url} due to: {err}") return False else: if pathlib.Path(stream).exists(): try: with pathlib.Path(stream).open() as fh: target.update(json.load(fh)) - except Exception as error: - log(f"{configuration_identifier} = {stream} does not contain a valid JSON format: {error}", level=logging.ERROR, fg="red") + except Exception as err: + error(f"{configuration_identifier} = {stream} does not contain a valid JSON format: {err}") return False else: # NOTE: This is a rudimentary check if what we're trying parse is a dict structure. @@ -600,14 +551,15 @@ def json_stream_to_structure(configuration_identifier : str, stream :str, target try: target.update(json.loads(stream)) except Exception as e: - log(f" {configuration_identifier} Contains an invalid JSON format : {e}",level=logging.ERROR, fg="red") + error(f"{configuration_identifier} Contains an invalid JSON format: {e}") return False else: - log(f" {configuration_identifier} is neither a file nor is a JSON string:",level=logging.ERROR, fg="red") + error(f"{configuration_identifier} is neither a file nor is a JSON string") return False return True + def secret(x :str): """ return * with len equal to to the input string """ return '*' * len(x) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index a969d93f..13595132 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -11,24 +11,24 @@ from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .storage import storage -from .user_interaction import add_number_of_parrallel_downloads -from .user_interaction import ask_additional_packages_to_install -from .user_interaction import ask_for_additional_users -from .user_interaction import ask_for_audio_selection -from .user_interaction import ask_for_bootloader -from .user_interaction import ask_for_swap -from .user_interaction import ask_hostname -from .user_interaction import ask_ntp -from .user_interaction import ask_to_configure_network -from .user_interaction import get_password, ask_for_a_timezone -from .user_interaction import select_additional_repositories -from .user_interaction import select_kernel -from .user_interaction import select_language -from .user_interaction import select_locale_enc -from .user_interaction import select_locale_lang -from .user_interaction import select_mirror_regions -from .user_interaction.disk_conf import select_disk_config -from .user_interaction.save_conf import save_config +from .configuration import save_config +from .interactions import add_number_of_parrallel_downloads +from .interactions import ask_additional_packages_to_install +from .interactions import ask_for_additional_users +from .interactions import ask_for_audio_selection +from .interactions import ask_for_bootloader +from .interactions import ask_for_swap +from .interactions import ask_hostname +from .interactions import ask_to_configure_network +from .interactions import get_password, ask_for_a_timezone +from .interactions import select_additional_repositories +from .interactions import select_kernel +from .interactions import select_language +from .interactions import select_locale_enc +from .interactions import select_locale_lang +from .interactions import select_mirror_regions +from .interactions import ask_ntp +from .interactions.disk_conf import select_disk_config if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 3759725f..b95301f9 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,27 +1,12 @@ import os -import logging -from functools import partial +from functools import cached_property from pathlib import Path -from typing import Iterator, Optional, Dict +from typing import Optional, Dict from .general import SysCommand from .networking import list_interfaces, enrich_iface_types from .exceptions import SysCallError -from .output import log - -__packages__ = [ - "mesa", - "xf86-video-amdgpu", - "xf86-video-ati", - "xf86-video-nouveau", - "xf86-video-vmware", - "libva-mesa-driver", - "libva-intel-driver", - "intel-media-driver", - "vulkan-radeon", - "vulkan-intel", - "nvidia", -] +from .output import debug AVAILABLE_GFX_DRIVERS = { # Sub-dicts are layer-2 options to be selected @@ -62,136 +47,125 @@ AVAILABLE_GFX_DRIVERS = { } -def cpuinfo() -> Iterator[dict[str, str]]: - """ - Yields information about the CPUs of the system - """ - cpu_info_path = Path("/proc/cpuinfo") - cpu: Dict[str, str] = {} +class _SysInfo: + def __init__(self): + pass - with cpu_info_path.open() as file: - for line in file: - if not (line := line.strip()): - yield cpu - cpu = {} - continue - - key, value = line.split(":", maxsplit=1) - cpu[key.strip()] = value.strip() - - -def all_meminfo() -> Dict[str, int]: - """ - Returns a dict with memory info if called with no args - or the value of the given key of said dict. - """ - mem_info_path = Path("/proc/meminfo") - mem_info: Dict[str, int] = {} - - with mem_info_path.open() as file: - for line in file: - key, value = line.strip().split(':') - num = value.split()[0] - mem_info[key] = int(num) - - return mem_info - - -def meminfo_for_key(key: str) -> int: - info = all_meminfo() - return info[key] - - -def has_wifi() -> bool: - ifaces = list(list_interfaces().values()) - return 'WIRELESS' in enrich_iface_types(ifaces).values() - - -def has_cpu_vendor(vendor_id: str) -> bool: - return any(cpu.get("vendor_id") == vendor_id for cpu in cpuinfo()) - - -has_amd_cpu = partial(has_cpu_vendor, "AuthenticAMD") - - -has_intel_cpu = partial(has_cpu_vendor, "GenuineIntel") - - -def has_uefi() -> bool: - return os.path.isdir('/sys/firmware/efi') - - -def graphics_devices() -> dict: - cards = {} - for line in SysCommand("lspci"): - if b' VGA ' in line or b' 3D ' in line: - _, identifier = line.split(b': ', 1) - cards[identifier.strip().decode('UTF-8')] = line - return cards - - -def has_nvidia_graphics() -> bool: - return any('nvidia' in x.lower() for x in graphics_devices()) - - -def has_amd_graphics() -> bool: - return any('amd' in x.lower() for x in graphics_devices()) - - -def has_intel_graphics() -> bool: - return any('intel' in x.lower() for x in graphics_devices()) - - -def cpu_vendor() -> Optional[str]: - for cpu in cpuinfo(): - return cpu.get("vendor_id") - - return None - - -def cpu_model() -> Optional[str]: - for cpu in cpuinfo(): - return cpu.get("model name") - - return None - - -def sys_vendor() -> Optional[str]: - with open(f"/sys/devices/virtual/dmi/id/sys_vendor") as vendor: - return vendor.read().strip() - - -def product_name() -> Optional[str]: - with open(f"/sys/devices/virtual/dmi/id/product_name") as product: - return product.read().strip() - - -def mem_available() -> Optional[int]: - return meminfo_for_key('MemAvailable') - - -def mem_free() -> Optional[int]: - return meminfo_for_key('MemFree') - - -def mem_total() -> Optional[int]: - return meminfo_for_key('MemTotal') - - -def virtualization() -> Optional[str]: - try: - return str(SysCommand("systemd-detect-virt")).strip('\r\n') - except SysCallError as error: - log(f"Could not detect virtual system: {error}", level=logging.DEBUG) - - return None - - -def is_vm() -> bool: - try: - result = SysCommand("systemd-detect-virt") - return b"none" not in b"".join(result).lower() - except SysCallError as error: - log(f"System is not running in a VM: {error}", level=logging.DEBUG) - - return False + @cached_property + def cpu_info(self) -> Dict[str, str]: + """ + Returns system cpu information + """ + cpu_info_path = Path("/proc/cpuinfo") + cpu: Dict[str, str] = {} + + with cpu_info_path.open() as file: + for line in file: + if line := line.strip(): + key, value = line.split(":", maxsplit=1) + cpu[key.strip()] = value.strip() + + return cpu + + @cached_property + def mem_info(self) -> Dict[str, int]: + """ + Returns system memory information + """ + mem_info_path = Path("/proc/meminfo") + mem_info: Dict[str, int] = {} + + with mem_info_path.open() as file: + for line in file: + key, value = line.strip().split(':') + num = value.split()[0] + mem_info[key] = int(num) + + return mem_info + + def mem_info_by_key(self, key: str) -> int: + return self.mem_info[key] + + +_sys_info = _SysInfo() + + +class SysInfo: + @staticmethod + def has_wifi() -> bool: + ifaces = list(list_interfaces().values()) + return 'WIRELESS' in enrich_iface_types(ifaces).values() + + @staticmethod + def has_uefi() -> bool: + return os.path.isdir('/sys/firmware/efi') + + @staticmethod + def _graphics_devices() -> Dict[str, str]: + cards: Dict[str, str] = {} + for line in SysCommand("lspci"): + if b' VGA ' in line or b' 3D ' in line: + _, identifier = line.split(b': ', 1) + cards[identifier.strip().decode('UTF-8')] = str(line) + return cards + + @staticmethod + def has_nvidia_graphics() -> bool: + return any('nvidia' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def has_amd_graphics() -> bool: + return any('amd' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def has_intel_graphics() -> bool: + return any('intel' in x.lower() for x in SysInfo._graphics_devices()) + + @staticmethod + def cpu_vendor() -> Optional[str]: + return _sys_info.cpu_info.get('vendor_id', None) + + @staticmethod + def cpu_model() -> Optional[str]: + return _sys_info.cpu_info.get('model name', None) + + @staticmethod + def sys_vendor() -> str: + with open(f"/sys/devices/virtual/dmi/id/sys_vendor") as vendor: + return vendor.read().strip() + + @staticmethod + def product_name() -> str: + with open(f"/sys/devices/virtual/dmi/id/product_name") as product: + return product.read().strip() + + @staticmethod + def mem_available() -> int: + return _sys_info.mem_info_by_key('MemAvailable') + + @staticmethod + def mem_free() -> int: + return _sys_info.mem_info_by_key('MemFree') + + @staticmethod + def mem_total() -> int: + return _sys_info.mem_info_by_key('MemTotal') + + @staticmethod + def virtualization() -> Optional[str]: + try: + return str(SysCommand("systemd-detect-virt")).strip('\r\n') + except SysCallError as err: + debug(f"Could not detect virtual system: {err}") + + return None + + @staticmethod + def is_vm() -> bool: + try: + result = SysCommand("systemd-detect-virt") + return b"none" not in b"".join(result).lower() + except SysCallError as err: + debug(f"System is not running in a VM: {err}") + + return False diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 726ff3d0..3c427ab2 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,5 +1,4 @@ import glob -import logging import os import re import shlex @@ -12,17 +11,16 @@ from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable, It from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand -from .hardware import has_uefi, is_vm, cpu_vendor -from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout +from .hardware import SysInfo +from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors from .models.bootloader import Bootloader from .models.network_configuration import NetworkConfiguration from .models.users import User -from .output import log +from .output import log, error, info, warn, debug from .pacman import run_pacman from .plugins import plugins -from .services import service_state from .storage import storage if TYPE_CHECKING: @@ -41,28 +39,6 @@ def accessibility_tools_in_use() -> bool: class Installer: - """ - `Installer()` is the wrapper for most basic installation steps. - It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. - - :param partition: Requires a partition as the first argument, this is - so that the installer can mount to `mountpoint` and strap packages there. - :type partition: class:`archinstall.Partition` - - :param boot_partition: There's two reasons for needing a boot partition argument, - The first being so that `mkinitcpio` can place the `vmlinuz` kernel at the right place - during the `pacstrap` or `linux` and the base packages for a minimal installation. - The second being when :py:func:`~archinstall.Installer.add_bootloader` is called, - A `boot_partition` must be known to the installer before this is called. - :type boot_partition: class:`archinstall.Partition` - - :param profile: A profile to install, this is optional and can be called later manually. - This just simplifies the process by not having to call :py:func:`~archinstall.Installer.install_profile` later on. - :type profile: str, optional - - :param hostname: The given /etc/hostname for the machine. - :type hostname: str, optional - """ def __init__( self, target: Path, @@ -71,6 +47,10 @@ class Installer: base_packages: List[str] = [], kernels: Optional[List[str]] = None ): + """ + `Installer()` is the wrapper for most basic installation steps. + It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. + """ if not base_packages: base_packages = __packages__[:3] @@ -126,7 +106,7 @@ class Installer: def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: - log(exc_val, fg='red', level=logging.ERROR) + error(exc_val) self.sync_log_to_install_medium() @@ -137,48 +117,41 @@ class Installer: raise exc_val if not (missing_steps := self.post_install_check()): - self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) + log('Installation completed without any errors. You may now reboot.', fg='green') self.sync_log_to_install_medium() return True else: - self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING) + warn('Some required steps were not successfully installed/configured before leaving the installer:') for step in missing_steps: - self.log(f' - {step}', fg='red', level=logging.WARNING) + warn(f' - {step}') - self.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=logging.WARNING) - self.log("Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=logging.WARNING) + warn(f"Detailed error logs can be found at: {storage['LOG_PATH']}") + warn("Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues") self.sync_log_to_install_medium() return False - def log(self, *args :str, level :int = logging.DEBUG, **kwargs :str): - """ - installer.log() wraps output.log() mainly to set a default log-level for this install session. - Any manual override can be done per log() call. - """ - log(*args, level=level, **kwargs) - def _verify_service_stop(self): """ Certain services might be running that affects the system during installation. One such service is "reflector.service" which updates /etc/pacman.d/mirrorlist We need to wait for it before we continue since we opted in to use a custom mirror/region. """ - log('Waiting for time sync (systemd-timesyncd.service) to complete.', level=logging.INFO) + info('Waiting for time sync (systemd-timesyncd.service) to complete.') while SysCommand('timedatectl show --property=NTPSynchronized --value').decode().rstrip() != 'yes': time.sleep(1) - log('Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) - while service_state('reflector') not in ('dead', 'failed', 'exited'): + info('Waiting for automatic mirror selection (reflector) to complete.') + while self._service_state('reflector') not in ('dead', 'failed', 'exited'): time.sleep(1) - log('Waiting pacman-init.service to complete.', level=logging.INFO) - while service_state('pacman-init') not in ('dead', 'failed', 'exited'): + info('Waiting pacman-init.service to complete.') + while self._service_state('pacman-init') not in ('dead', 'failed', 'exited'): time.sleep(1) - log('Waiting Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.', level=logging.INFO) - while service_state('archlinux-keyring-wkd-sync') not in ('dead', 'failed', 'exited'): + info('Waiting Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.') + while self._service_state('archlinux-keyring-wkd-sync') not in ('dead', 'failed', 'exited'): time.sleep(1) def _verify_boot_part(self): @@ -204,7 +177,7 @@ class Installer: self._verify_service_stop() def mount_ordered_layout(self): - log('Mounting partitions in order', level=logging.INFO) + info('Mounting partitions in order') for mod in self._disk_config.device_modifications: # partitions have to mounted in the right order on btrfs the mountpoint will @@ -275,7 +248,7 @@ class Installer: ) if gen_enc_file and not part_mod.is_root(): - log(f'Creating key-file: {part_mod.dev_path}', level=logging.INFO) + info(f'Creating key-file: {part_mod.dev_path}') luks_handler.create_keyfile(self.target) if part_mod.is_root() and not gen_enc_file: @@ -384,25 +357,25 @@ class Installer: if (result := plugin.on_pacstrap(packages)): packages = result - self.log(f'Installing packages: {packages}', level=logging.INFO) + info(f'Installing packages: {packages}') # TODO: We technically only need to run the -Syy once. try: run_pacman('-Syy', default_cmd='/usr/bin/pacman') - except SysCallError as error: - self.log(f'Could not sync a new package database: {error}', level=logging.ERROR, fg="red") + except SysCallError as err: + error(f'Could not sync a new package database: {err}') if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): return self._pacstrap(packages) - raise RequirementError(f'Could not sync mirrors: {error}') + raise RequirementError(f'Could not sync mirrors: {err}') try: SysCommand(f'/usr/bin/pacstrap -C /etc/pacman.conf -K {self.target} {" ".join(packages)} --noconfirm', peek_output=True) return True - except SysCallError as error: - self.log(f'Could not strap in packages: {error}', level=logging.ERROR, fg="red") + except SysCallError as err: + error(f'Could not strap in packages: {err}') if storage['arguments'].get('silent', False) is False: if input('Would you like to re-try this download? (Y/n): ').lower().strip() in ('', 'y'): @@ -420,12 +393,12 @@ class Installer: use_mirrors(mirrors, destination=destination) def genfstab(self, flags :str = '-pU'): - self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) + info(f"Updating {self.target}/etc/fstab") try: gen_fstab = SysCommand(f'/usr/bin/genfstab {flags} {self.target}').decode() - except SysCallError as error: - raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {error}') + except SysCallError as err: + raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') if not gen_fstab: raise RequirementError(f'Genrating fstab returned empty value') @@ -530,24 +503,20 @@ class Installer: return True else: - self.log( - f"Time zone {zone} does not exist, continuing with system default.", - level=logging.WARNING, - fg='red' - ) + warn(f'Time zone {zone} does not exist, continuing with system default') return False def activate_time_syncronization(self) -> None: - self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO) + info('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers') self.enable_service('systemd-timesyncd') def enable_espeakup(self) -> None: - self.log('Enabling espeakup.service for speech synthesis (accessibility).', level=logging.INFO) + info('Enabling espeakup.service for speech synthesis (accessibility)') self.enable_service('espeakup') def enable_periodic_trim(self) -> None: - self.log("Enabling periodic TRIM") + info("Enabling periodic TRIM") # fstrim is owned by util-linux, a dependency of both base and systemd. self.enable_service("fstrim.timer") @@ -556,12 +525,12 @@ class Installer: services = [services] for service in services: - self.log(f'Enabling service {service}', level=logging.INFO) + info(f'Enabling service {service}') try: self.arch_chroot(f'systemctl enable {service}') - except SysCallError as error: - raise ServiceException(f"Unable to start service {service}: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to start service {service}: {err}") for plugin in plugins.values(): if hasattr(plugin, 'on_service'): @@ -713,11 +682,11 @@ class Installer: if 'encrypt' not in self._hooks: self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') - if not has_uefi(): + if not SysInfo.has_uefi(): self.base_packages.append('grub') - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": self.base_packages.append("amd-ucode") if (ucode := Path(f"{self.target}/boot/amd-ucode.img")).exists(): @@ -727,21 +696,21 @@ class Installer: if (ucode := Path(f"{self.target}/boot/intel-ucode.img")).exists(): ucode.unlink() else: - self.log(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't install any ucode.", level=logging.DEBUG) + debug(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't install any ucode") # Determine whether to enable multilib/testing repositories before running pacstrap if testing flag is set. # This action takes place on the host system as pacstrap copies over package repository lists. if multilib: - self.log("The multilib flag is set. This system will be installed with the multilib repository enabled.") + info("The multilib flag is set. This system will be installed with the multilib repository enabled.") self.enable_multilib_repository() else: - self.log("The multilib flag is not set. This system will be installed without multilib repositories enabled.") + info("The multilib flag is not set. This system will be installed without multilib repositories enabled.") if testing: - self.log("The testing flag is set. This system will be installed with testing repositories enabled.") + info("The testing flag is set. This system will be installed with testing repositories enabled.") self.enable_testing_repositories(multilib) else: - self.log("The testing flag is not set. This system will be installed without testing repositories enabled.") + info("The testing flag is not set. This system will be installed without testing repositories enabled.") self._pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True @@ -773,7 +742,7 @@ class Installer: # Run registered post-install hooks for function in self.post_base_install: - self.log(f"Running post-installation hook: {function}", level=logging.INFO) + info(f"Running post-installation hook: {function}") function(self) for plugin in plugins.values(): @@ -782,7 +751,7 @@ class Installer: def setup_swap(self, kind :str = 'zram'): if kind == 'zram': - self.log(f"Setting up swap on zram") + info(f"Setting up swap on zram") self._pacstrap('zram-generator') # We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813 @@ -812,7 +781,7 @@ class Installer: def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): self._pacstrap('efibootmgr') - if not has_uefi(): + if not SysInfo.has_uefi(): raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config @@ -862,16 +831,18 @@ class Installer: entry.write(f'# Created on: {self.init_time}\n') entry.write(f'title Arch Linux ({kernel}{variant})\n') entry.write(f"linux /vmlinuz-{kernel}\n") - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": entry.write("initrd /amd-ucode.img\n") elif vendor == "GenuineIntel": entry.write("initrd /intel-ucode.img\n") else: - self.log( - f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to systemd-boot config.", - level=logging.DEBUG) + debug( + f"Unknown CPU vendor '{vendor}' detected.", + "Archinstall won't add any ucode to systemd-boot config.", + ) + entry.write(f"initrd /initramfs-{kernel}{variant}.img\n") # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. @@ -890,7 +861,7 @@ class Installer: if root_partition.fs_type.is_crypto(): # 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) - log('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}') kernel_options = f"options" @@ -905,7 +876,7 @@ class Installer: entry.write(f'{kernel_options} root=/dev/mapper/luksdev {options_entry}') else: - log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') entry.write(f'options root=PARTUUID={root_partition.partuuid} {options_entry}') self.helper_flags['bootloader'] = 'systemd' @@ -920,7 +891,7 @@ class Installer: _file = "/etc/default/grub" if root_partition.fs_type.is_crypto(): - log(f"Using UUID {root_partition.uuid} as encrypted root identifier", level=logging.DEBUG) + debug(f"Using UUID {root_partition.uuid} as encrypted root identifier") cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_partition.uuid}:cryptlvm rootfstype={root_partition.fs_type.value}\"/'" enable_cryptdisk = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'" @@ -931,9 +902,9 @@ class Installer: SysCommand(f"/usr/bin/arch-chroot {self.target} {cmd_line_linux} {_file}") - log(f"GRUB boot partition: {boot_partition.dev_path}", level=logging.INFO) + info(f"GRUB boot partition: {boot_partition.dev_path}") - if has_uefi(): + if SysInfo.has_uefi(): self._pacstrap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: @@ -941,8 +912,8 @@ class Installer: except SysCallError: try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) - except SysCallError as error: - raise DiskError(f"Could not install GRUB to {self.target}/boot: {error}") + except SysCallError as err: + raise DiskError(f"Could not install GRUB to {self.target}/boot: {err}") else: device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) @@ -958,13 +929,13 @@ class Installer: f' --recheck {device.device_info.path}' SysCommand(cmd, peek_output=True) - except SysCallError as error: - raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {error}") + except SysCallError as err: + raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {err}") try: SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o /boot/grub/grub.cfg') - except SysCallError as error: - raise DiskError(f"Could not configure GRUB: {error}") + except SysCallError as err: + raise DiskError(f"Could not configure GRUB: {err}") self.helper_flags['bootloader'] = "grub" @@ -975,7 +946,7 @@ class Installer: ): self._pacstrap('efibootmgr') - if not has_uefi(): + if not SysInfo.has_uefi(): raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config @@ -989,14 +960,14 @@ class Installer: kernel_parameters = [] - if not is_vm(): - vendor = cpu_vendor() + if not SysInfo.is_vm(): + vendor = SysInfo.cpu_vendor() if vendor == "AuthenticAMD": kernel_parameters.append("initrd=\\amd-ucode.img") elif vendor == "GenuineIntel": kernel_parameters.append("initrd=\\intel-ucode.img") else: - self.log(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to firmware boot entry.", level=logging.DEBUG) + debug(f"Unknown CPU vendor '{vendor}' detected. Archinstall won't add any ucode to firmware boot entry.") kernel_parameters.append(f"initrd=\\initramfs-{kernel}.img") @@ -1006,10 +977,10 @@ class Installer: if root_partition.fs_type.is_crypto(): # 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) - log(f'Identifying root partition by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') else: - log(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}', level=logging.DEBUG) + debug(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}') kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) @@ -1060,7 +1031,7 @@ class Installer: if root_partition is None: raise ValueError(f'Could not detect root at mountpoint {self.target}') - self.log(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}', level=logging.INFO) + info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') match bootloader: case Bootloader.Systemd: @@ -1078,7 +1049,7 @@ class Installer: self.arch_chroot(f'systemctl enable --user {service}', run_as=user.username) def enable_sudo(self, entity: str, group :bool = False): - self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) + info(f'Enabling sudo permissions for {entity}') sudoers_dir = f"{self.target}/etc/sudoers.d" @@ -1127,11 +1098,11 @@ class Installer: handled_by_plugin = result if not handled_by_plugin: - self.log(f'Creating user {user}', level=logging.INFO) + info(f'Creating user {user}') try: SysCommand(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}') - except SysCallError as error: - raise SystemError(f"Could not create user inside installation: {error}") + except SysCallError as err: + raise SystemError(f"Could not create user inside installation: {err}") for plugin in plugins.values(): if hasattr(plugin, 'on_user_created'): @@ -1149,7 +1120,7 @@ class Installer: self.helper_flags['user'] = True def user_set_pw(self, user :str, password :str) -> bool: - self.log(f'Setting password for {user}', level=logging.INFO) + info(f'Setting password for {user}') if user == 'root': # This means the root account isn't locked/disabled with * in /etc/passwd @@ -1166,7 +1137,7 @@ class Installer: return False def user_set_shell(self, user :str, shell :str) -> bool: - self.log(f'Setting shell for {user} to {shell}', level=logging.INFO) + info(f'Setting shell for {user} to {shell}') try: SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"") @@ -1183,49 +1154,59 @@ class Installer: return False def set_keyboard_language(self, language: str) -> bool: - log(f"Setting keyboard language to {language}", level=logging.INFO) + info(f"Setting keyboard language to {language}") + if len(language.strip()): if not verify_keyboard_layout(language): - self.log(f"Invalid keyboard language specified: {language}", fg="red", level=logging.ERROR) + error(f"Invalid keyboard language specified: {language}") return False # In accordance with https://github.com/archlinux/archinstall/issues/107#issuecomment-841701968 # Setting an empty keymap first, allows the subsequent call to set layout for both console and x11. - from .systemd import Boot + from .boot import Boot with Boot(self) as session: os.system('/usr/bin/systemd-run --machine=archinstall --pty localectl set-keymap ""') try: session.SysCommand(["localectl", "set-keymap", language]) - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{language}' for console: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{language}' for console: {err}") - self.log(f"Keyboard language for this installation is now set to: {language}") + info(f"Keyboard language for this installation is now set to: {language}") else: - self.log('Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) + info('Keyboard language was not changed from default (no language specified)') return True def set_x11_keyboard_language(self, language: str) -> bool: - log(f"Setting x11 keyboard language to {language}", level=logging.INFO) """ A fallback function to set x11 layout specifically and separately from console layout. This isn't strictly necessary since .set_keyboard_language() does this as well. """ + info(f"Setting x11 keyboard language to {language}") + if len(language.strip()): if not verify_x11_keyboard_layout(language): - self.log(f"Invalid x11-keyboard language specified: {language}", fg="red", level=logging.ERROR) + error(f"Invalid x11-keyboard language specified: {language}") return False - from .systemd import Boot + from .boot import Boot with Boot(self) as session: session.SysCommand(["localectl", "set-x11-keymap", '""']) try: session.SysCommand(["localectl", "set-x11-keymap", language]) - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{language}' for X11: {error}") + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{language}' for X11: {err}") else: - self.log(f'X11-Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) + info(f'X11-Keyboard language was not changed from default (no language specified)') return True + + def _service_state(self, service_name: str) -> str: + if os.path.splitext(service_name)[1] != '.service': + service_name += '.service' # Just to be safe + + state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) + + return state.strip().decode('UTF-8') diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py new file mode 100644 index 00000000..b5691a10 --- /dev/null +++ b/archinstall/lib/interactions/__init__.py @@ -0,0 +1,20 @@ +from .locale_conf import select_locale_lang, select_locale_enc +from .manage_users_conf import UserList, ask_for_additional_users +from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .utils import get_password + +from .disk_conf import ( + select_devices, select_disk_config, get_default_partition_layout, + select_main_filesystem_format, suggest_single_disk_layout, + suggest_multi_disk_layout +) + +from .general_conf import ( + ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, + select_mirror_regions, select_archinstall_language, ask_additional_packages_to_install, + add_number_of_parrallel_downloads, select_additional_repositories +) + +from .system_conf import ( + select_kernel, ask_for_bootloader, select_driver, ask_for_swap +) diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py new file mode 100644 index 00000000..78e4cff4 --- /dev/null +++ b/archinstall/lib/interactions/disk_conf.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, TYPE_CHECKING +from typing import Optional, List, Tuple + +from .. import disk +from ..hardware import SysInfo +from ..menu import Menu +from ..menu import TableMenu +from ..menu.menu import MenuSelectionType +from ..output import FormattedOutput, debug +from ..utils.util import prompt_dir + +if TYPE_CHECKING: + _: Any + + +def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: + """ + Asks the user to select one or multiple devices + + :return: List of selected devices + :rtype: list + """ + + def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: + dev = disk.device_handler.get_device(selection.path) + if dev and dev.partition_infos: + return FormattedOutput.as_table(dev.partition_infos) + return None + + if preset is None: + preset = [] + + title = str(_('Select one or more devices to use and configure')) + warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) + + devices = disk.device_handler.devices + options = [d.device_info for d in devices] + preset_value = [p.device_info for p in preset] + + choice = TableMenu( + title, + data=options, + multi=True, + preset=preset_value, + preview_command=_preview_device_selection, + preview_title=str(_('Existing Partitions')), + preview_size=0.2, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: + selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_devices = [] + + for device in devices: + if device.device_info in selected_device_info: + selected_devices.append(device) + + return selected_devices + + +def get_default_partition_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_option: bool = False +) -> List[disk.DeviceModification]: + + if len(devices) == 1: + device_modification = suggest_single_disk_layout( + devices[0], + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + return [device_modification] + else: + return suggest_multi_disk_layout( + devices, + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + + +def _manual_partitioning( + preset: List[disk.DeviceModification], + devices: List[disk.BDevice] +) -> List[disk.DeviceModification]: + modifications = [] + for device in devices: + mod = next(filter(lambda x: x.device == device, preset), None) + if not mod: + mod = disk.DeviceModification(device, wipe=False) + + if partitions := disk.manual_partitioning(device, preset=mod.partitions): + mod.partitions = partitions + modifications.append(mod) + + return modifications + + +def select_disk_config( + preset: Optional[disk.DiskLayoutConfiguration] = None, + advanced_option: bool = False +) -> Optional[disk.DiskLayoutConfiguration]: + default_layout = disk.DiskLayoutType.Default.display_msg() + manual_mode = disk.DiskLayoutType.Manual.display_msg() + pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() + + options = [default_layout, manual_mode, pre_mount_mode] + preset_value = preset.config_type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select a partitioning option'), + options, + allow_reset=True, + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + if choice.single_value == pre_mount_mode: + output = "You will use whatever drive-setup is mounted at the specified directory\n" + output += "WARNING: Archinstall won't check the suitability of this setup\n" + + path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + mods = disk.device_handler.detect_pre_mounted_mods(path) + + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Pre_mount, + relative_mountpoint=path, + device_modifications=mods + ) + + preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] + + devices = select_devices(preset_devices) + + if not devices: + return None + + if choice.value == default_layout: + modifications = get_default_partition_layout(devices, advanced_option=advanced_option) + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=modifications + ) + elif choice.value == manual_mode: + preset_mods = preset.device_modifications if preset else [] + modifications = _manual_partitioning(preset_mods, devices) + + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Manual, + device_modifications=modifications + ) + + return None + + +def _boot_partition() -> disk.PartitionModification: + if SysInfo.has_uefi(): + start = disk.Size(1, disk.Unit.MiB) + size = disk.Size(512, disk.Unit.MiB) + else: + start = disk.Size(3, disk.Unit.MiB) + size = disk.Size(203, disk.Unit.MiB) + + # boot partition + return disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=size, + mountpoint=Path('/boot'), + fs_type=disk.FilesystemType.Fat32, + flags=[disk.PartitionFlag.Boot] + ) + + +def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: + options = { + 'btrfs': disk.FilesystemType.Btrfs, + 'ext4': disk.FilesystemType.Ext4, + 'xfs': disk.FilesystemType.Xfs, + 'f2fs': disk.FilesystemType.F2fs + } + + if advanced_options: + options.update({'ntfs': disk.FilesystemType.Ntfs}) + + prompt = _('Select which filesystem your main partition should use') + choice = Menu(prompt, options, skip=False, sort=False).run() + return options[choice.single_value] + + +def suggest_single_disk_layout( + device: disk.BDevice, + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False, + separate_home: Optional[bool] = None +) -> disk.DeviceModification: + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) + root_partition_size = disk.Size(20, disk.Unit.GiB) + using_subvolumes = False + using_home_partition = False + compression = False + device_size_gib = device.device_info.total_size + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_subvolumes = choice.value == Menu.yes() + + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_modification = disk.DeviceModification(device, wipe=True) + + # Used for reference: https://wiki.archlinux.org/title/partitioning + # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? + + # TODO: On BIOS, /boot partition is only needed if the drive will + # be encrypted, otherwise it is not recommended. We should probably + # add a check for whether the drive will be encrypted or not. + + # Increase the UEFI partition if UEFI is detected. + # Also re-align the start to 1MiB since we don't need the first sectors + # like we do in MBR layouts where the boot loader is installed traditionally. + + boot_partition = _boot_partition() + device_modification.add_partition(boot_partition) + + if not using_subvolumes: + if device_size_gib >= min_size_to_allow_home_part: + if separate_home is None: + prompt = str(_('Would you like to create a separate partition for /home?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_home_partition = choice.value == Menu.yes() + elif separate_home is True: + using_home_partition = True + else: + using_home_partition = False + + # root partition + start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + + # Set a size for / (/root) + if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + else: + length = min(device.device_info.total_size, root_partition_size) + + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=length, + mountpoint=Path('/') if not using_subvolumes else None, + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [], + ) + device_modification.add_partition(root_partition) + + if using_subvolumes: + # https://btrfs.wiki.kernel.org/index.php/FAQ + # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash + # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh + subvolumes = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) + ] + root_partition.btrfs_subvols = subvolumes + elif using_home_partition: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=root_partition.length, + length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + mountpoint=Path('/home'), + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [] + ) + device_modification.add_partition(home_partition) + + return device_modification + + +def suggest_multi_disk_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False +) -> List[disk.DeviceModification]: + if not devices: + return [] + + # Not really a rock solid foundation of information to stand on, but it's a start: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + min_home_partition_size = disk.Size(40, disk.Unit.GiB) + # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + compression = False + + if not filesystem_type: + filesystem_type = select_main_filesystem_format(advanced_options) + + # find proper disk for /home + possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) + home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None + + # find proper device for /root + devices_delta = {} + for device in devices: + if device is not home_device: + delta = device.device_info.total_size - desired_root_partition_size + devices_delta[device] = delta + + sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: Optional[disk.BDevice] = sorted_delta[0][0] + + if home_device is None or root_device is None: + text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') + text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) + text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) + Menu(str(text), [str(_('Continue'))], skip=False).run() + return [] + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_paths = ', '.join([str(d.device_info.path) for d in devices]) + + debug(f"Suggesting multi-disk-layout for devices: {device_paths}") + debug(f"/root: {root_device.device_info.path}") + debug(f"/home: {home_device.device_info.path}") + + root_device_modification = disk.DeviceModification(root_device, wipe=True) + home_device_modification = disk.DeviceModification(home_device, wipe=True) + + # add boot partition to the root device + boot_partition = _boot_partition() + root_device_modification.add_partition(boot_partition) + + # add root partition to the root device + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + mountpoint=Path('/'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type + ) + root_device_modification.add_partition(root_partition) + + # add home partition to home device + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(1, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + mountpoint=Path('/home'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type, + ) + home_device_modification.add_partition(home_partition) + + return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py new file mode 100644 index 00000000..5fcfa633 --- /dev/null +++ b/archinstall/lib/interactions/general_conf.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import pathlib +from typing import List, Any, Optional, Dict, TYPE_CHECKING + +from ..locale import list_keyboard_languages, list_timezones +from ..menu import MenuSelectionType, Menu, TextInput +from ..mirrors import list_mirrors +from ..output import warn +from ..packages.packages import validate_package_list +from ..storage import storage +from ..translationhandler import Language + +if TYPE_CHECKING: + _: Any + + +def ask_ntp(preset: bool = True) -> bool: + prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) + prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() + + return False if choice.value == Menu.no() else True + + +def ask_hostname(preset: str = '') -> str: + while True: + hostname = TextInput( + str(_('Desired hostname for the installation: ')), + preset + ).run().strip() + + if hostname: + return hostname + + +def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: + timezones = list_timezones() + default = 'UTC' + + choice = Menu( + _('Select a timezone'), + list(timezones), + preset_values=preset, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: + no_audio = str(_('No audio server')) + choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] + default = 'pipewire' if desktop else no_audio + + choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_language(preset: Optional[str] = None) -> Optional[str]: + """ + Asks the user to select a language + Usually this is combined with :ref:`archinstall.list_keyboard_languages`. + + :return: The language/dictionary key of the selected language + :rtype: str + """ + kb_lang = list_keyboard_languages() + # sort alphabetically and then by length + sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) + + choice = Menu( + _('Select keyboard layout'), + sorted_kb_lang, + preset_values=preset, + sort=False + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return choice.single_value + + return None + + +def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: + """ + Asks the user to select a mirror or region + Usually this is combined with :ref:`archinstall.list_mirrors`. + + :return: The dictionary information about a mirror/region. + :rtype: dict + """ + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) + + mirrors = list_mirrors() + + choice = Menu( + _('Select one of the regions to download packages from'), + list(mirrors.keys()), + preset_values=preselected, + multi=True, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return {} + case MenuSelectionType.Skip: + return preset_values + case MenuSelectionType.Selection: + return {selected: mirrors[selected] for selected in choice.multi_value} + + return {} + + +def select_archinstall_language(languages: List[Language], preset: Language) -> Language: + # these are the displayed language names which can either be + # the english name of a language or, if present, the + # name of the language in its own language + options = {lang.display_name: lang for lang in languages} + + title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' + title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' + title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' + + choice = Menu( + title, + list(options.keys()), + default_option=preset.display_name, + preview_size=0.5 + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return options[choice.single_value] + + raise ValueError('Language selection not handled') + + +def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: + # Additional packages (with some light weight error handling for invalid package names) + print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + + def read_packages(already_defined: list = []) -> list: + display = ' '.join(already_defined) + input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() + return input_packages.split() if input_packages else [] + + pre_set_packages = pre_set_packages if pre_set_packages else [] + packages = read_packages(pre_set_packages) + + if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: + while True: + if len(packages): + # Verify packages that were given + print(_("Verifying that additional packages exist (this might take a few seconds)")) + valid, invalid = validate_package_list(packages) + + if invalid: + warn(f"Some packages could not be found in the repository: {invalid}") + packages = read_packages(valid) + continue + break + + return packages + + +def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: + max_downloads = 5 + print(_(f"This option enables the number of parallel downloads that can occur during installation")) + print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) + print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) + print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) + print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) + + while True: + try: + input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) + if input_number <= 0: + input_number = 0 + elif input_number > max_downloads: + input_number = max_downloads + break + except: + print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) + + pacman_conf_path = pathlib.Path("/etc/pacman.conf") + with pacman_conf_path.open() as f: + pacman_conf = f.read().split("\n") + + with pacman_conf_path.open("w") as fwrite: + for line in pacman_conf: + if "ParallelDownloads" in line: + fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") + else: + fwrite.write(f"{line}\n") + + return input_number + + +def select_additional_repositories(preset: List[str]) -> List[str]: + """ + Allows the user to select additional repositories (multilib, and testing) if desired. + + :return: The string as a selected repository + :rtype: string + """ + + repositories = ["multilib", "testing"] + + choice = Menu( + _('Choose which optional additional repositories to enable'), + repositories, + sort=False, + multi=True, + preset_values=preset, + allow_reset=True + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.single_value + + return [] diff --git a/archinstall/lib/interactions/locale_conf.py b/archinstall/lib/interactions/locale_conf.py new file mode 100644 index 00000000..de115202 --- /dev/null +++ b/archinstall/lib/interactions/locale_conf.py @@ -0,0 +1,43 @@ +from typing import Any, TYPE_CHECKING, Optional + +from ..locale import list_locales +from ..menu import Menu, MenuSelectionType + +if TYPE_CHECKING: + _: Any + + +def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_lang = set([locale.split()[0] for locale in locales]) + + choice = Menu( + _('Choose which locale language to use'), + list(locale_lang), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None + + +def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: + locales = list_locales() + locale_enc = set([locale.split()[1] for locale in locales]) + + choice = Menu( + _('Choose which locale encoding to use'), + list(locale_enc), + sort=True, + preset_values=preset + ).run() + + match choice.type_: + case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return preset + + return None diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py new file mode 100644 index 00000000..879578da --- /dev/null +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import re +from typing import Any, Dict, TYPE_CHECKING, List, Optional + +from .utils import get_password +from ..menu import Menu, ListManager +from ..models.users import User +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class UserList(ListManager): + """ + subclass of ListManager for the managing of user accounts + """ + + def __init__(self, prompt: str, lusers: List[User]): + self._actions = [ + str(_('Add a user')), + str(_('Change password')), + str(_('Promote/Demote user')), + str(_('Delete User')) + ] + super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[User]) -> Dict[str, Any]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, user in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = user + + return display_data + + def selected_action_display(self, user: User) -> str: + return user.username + + def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: + if action == self._actions[0]: # add + new_user = self._add_user() + if new_user is not None: + # in case a user with the same username as an existing user + # was created we'll replace the existing one + data = [d for d in data if d.username != new_user.username] + data += [new_user] + elif action == self._actions[1] and entry: # change password + prompt = str(_('Password for user "{}": ').format(entry.username)) + new_password = get_password(prompt=prompt) + if new_password: + user = next(filter(lambda x: x == entry, data)) + user.password = new_password + elif action == self._actions[2] and entry: # promote/demote + user = next(filter(lambda x: x == entry, data)) + user.sudo = False if user.sudo else True + elif action == self._actions[3] and entry: # delete + data = [d for d in data if d != entry] + + return data + + def _check_for_correct_username(self, username: str) -> bool: + if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: + return True + return False + + def _add_user(self) -> Optional[User]: + prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) + + while True: + username = input(prompt).strip(' ') + if not username: + return None + if not self._check_for_correct_username(username): + error_prompt = str(_("The username you entered is invalid. Try again")) + print(error_prompt) + else: + break + + password = get_password(prompt=str(_('Password for user "{}": ').format(username))) + + if not password: + return None + + choice = Menu( + str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), + skip=False, + default_option=Menu.yes(), + clear_screen=False, + show_search_hint=False + ).run() + + sudo = True if choice.value == Menu.yes() else False + return User(username, password, sudo) + + +def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: + users = UserList(prompt, defined_users).run() + return users diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py new file mode 100644 index 00000000..18a834a1 --- /dev/null +++ b/archinstall/lib/interactions/network_conf.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + + super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, iface: NetworkConfiguration) -> str: + return iface.iface if iface.iface else '' + + def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): + if action == self._actions[0]: # add + iface_name = self._select_iface(data) + if iface_name: + iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) + iface = self._edit_iface(iface) + data += [iface] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.value + + def _edit_iface(self, edit_iface: NetworkConfiguration): + iface_name = edit_iface.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_iface.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_iface.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_iface.dns: + display_dns = ' '.join(edit_iface.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return NetworkConfiguration(NicType.MANUAL, iface=iface_name) + + +def ask_to_configure_network( + preset: Union[NetworkConfiguration, List[NetworkConfiguration]] +) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: + """ + Configure the network on the newly installed system + """ + network_options = { + 'none': str(_('No network configuration')), + 'iso_config': str(_('Copy ISO network configuration to installation')), + 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), + 'manual': str(_('Manual configuration')) + } + # for this routine it's easier to set the cursor position rather than a preset value + cursor_idx = None + + if preset and not isinstance(preset, list): + if preset.type == 'iso_config': + cursor_idx = 0 + elif preset.type == 'network_manager': + cursor_idx = 1 + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(network_options.values()), + cursor_index=cursor_idx, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + + if choice.value == network_options['none']: + return None + elif choice.value == network_options['iso_config']: + return NetworkConfiguration(NicType.ISO) + elif choice.value == network_options['network_manager']: + return NetworkConfiguration(NicType.NM) + elif choice.value == network_options['manual']: + preset_ifaces = preset if isinstance(preset, list) else [] + return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() + + return preset diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py new file mode 100644 index 00000000..bbcb5b23 --- /dev/null +++ b/archinstall/lib/interactions/system_conf.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from typing import List, Any, Dict, TYPE_CHECKING, Optional + +from ..hardware import AVAILABLE_GFX_DRIVERS, SysInfo +from ..menu import MenuSelectionType, Menu +from ..models.bootloader import Bootloader + +if TYPE_CHECKING: + _: Any + + +def select_kernel(preset: List[str] = []) -> List[str]: + """ + Asks the user to select a kernel for system. + + :return: The string as a selected kernel + :rtype: string + """ + + kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] + default_kernel = "linux" + + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), + kernels, + sort=True, + multi=True, + preset_values=preset, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Selection: return choice.value # type: ignore + + +def ask_for_bootloader(preset: Bootloader) -> Bootloader: + # when the system only supports grub + if not SysInfo.has_uefi(): + options = [Bootloader.Grub.value] + default = Bootloader.Grub.value + else: + options = Bootloader.values() + default = Bootloader.Systemd.value + + preset_value = preset.value if preset else None + + choice = Menu( + _('Choose a bootloader'), + options, + preset_values=preset_value, + sort=False, + default_option=default + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return Bootloader(choice.value) + + return preset + + +def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: + """ + Some what convoluted function, whose job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + + if not options: + options = AVAILABLE_GFX_DRIVERS + + drivers = sorted(list(options.keys())) + + if drivers: + title = '' + if SysInfo.has_amd_graphics(): + title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' + if SysInfo.has_intel_graphics(): + title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')) + if SysInfo.has_nvidia_graphics(): + title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) + + title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) + + preset = current_value if current_value else None + choice = Menu(title, drivers, preset_values=preset).run() + + if choice.type_ != MenuSelectionType.Selection: + return None + + return choice.value # type: ignore + + return current_value + + +def ask_for_swap(preset: bool = True) -> bool: + if preset: + preset_val = Menu.yes() + else: + preset_val = Menu.no() + + prompt = _('Would you like to use swap on zram?') + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset diff --git a/archinstall/lib/interactions/utils.py b/archinstall/lib/interactions/utils.py new file mode 100644 index 00000000..f6b5b2d3 --- /dev/null +++ b/archinstall/lib/interactions/utils.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import getpass +from typing import Any, Optional, TYPE_CHECKING + +from ..models import PasswordStrength +from ..output import log, error + +if TYPE_CHECKING: + _: Any + +# used for signal handler +SIG_TRIGGER = None + + +def get_password(prompt: str = '') -> Optional[str]: + if not prompt: + prompt = _("Enter a password: ") + + while password := getpass.getpass(prompt): + if len(password.strip()) <= 0: + break + + strength = PasswordStrength.strength(password) + log(f'Password strength: {strength.value}', fg=strength.color()) + + passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) + if password != passwd_verification: + error(' * Passwords did not match * ') + continue + + return password + + return None diff --git a/archinstall/lib/locale.py b/archinstall/lib/locale.py new file mode 100644 index 00000000..0a36c072 --- /dev/null +++ b/archinstall/lib/locale.py @@ -0,0 +1,68 @@ +from typing import Iterator, List + +from .exceptions import ServiceException, SysCallError +from .general import SysCommand +from .output import error + + +def list_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def list_locales() -> List[str]: + with open('/etc/locale.gen', 'r') as fp: + locales = [] + # before the list of locales begins there's an empty line with a '#' in front + # so we'll collect the localels from bottom up and halt when we're donw + entries = fp.readlines() + entries.reverse() + + for entry in entries: + text = entry.replace('#', '').strip() + if text == '': + break + locales.append(text) + + locales.reverse() + return locales + + +def list_x11_keyboard_languages() -> Iterator[str]: + for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() + + +def verify_keyboard_layout(layout :str) -> bool: + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def verify_x11_keyboard_layout(layout :str) -> bool: + for language in list_x11_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def set_keyboard_language(locale :str) -> bool: + if len(locale.strip()): + if not verify_keyboard_layout(locale): + error(f"Invalid keyboard locale specified: {locale}") + return False + + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") + + return True + + return False + + +def list_timezones() -> Iterator[str]: + for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): + yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py deleted file mode 100644 index efb0365f..00000000 --- a/archinstall/lib/locale_helpers.py +++ /dev/null @@ -1,176 +0,0 @@ -import logging -from typing import Iterator, List, Callable, Optional - -from .exceptions import ServiceException, SysCallError -from .general import SysCommand -from .output import log -from .storage import storage - - -def list_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def list_locales() -> List[str]: - with open('/etc/locale.gen', 'r') as fp: - locales = [] - # before the list of locales begins there's an empty line with a '#' in front - # so we'll collect the localels from bottom up and halt when we're donw - entries = fp.readlines() - entries.reverse() - - for entry in entries: - text = entry.replace('#', '').strip() - if text == '': - break - locales.append(text) - - locales.reverse() - return locales - -def get_locale_mode_text(mode): - if mode == 'LC_ALL': - mode_text = "general (LC_ALL)" - elif mode == "LC_CTYPE": - mode_text = "Character set" - elif mode == "LC_NUMERIC": - mode_text = "Numeric values" - elif mode == "LC_TIME": - mode_text = "Time Values" - elif mode == "LC_COLLATE": - mode_text = "sort order" - elif mode == "LC_MESSAGES": - mode_text = "text messages" - else: - mode_text = "Unassigned" - return mode_text - - -def reset_cmd_locale(): - """ sets the cmd_locale to its saved default """ - storage['CMD_LOCALE'] = storage.get('CMD_LOCALE_DEFAULT',{}) - - -def unset_cmd_locale(): - """ archinstall will use the execution environment default """ - storage['CMD_LOCALE'] = {} - - -def set_cmd_locale( - general: Optional[str] = None, - charset :str = 'C', - numbers :str = 'C', - time :str = 'C', - collate :str = 'C', - messages :str = 'C' -): - """ - Set the cmd locale. - If the parameter general is specified, it takes precedence over the rest (might as well not exist) - The rest define some specific settings above the installed default language. If anyone of this parameters is none means the installation default - """ - installed_locales = list_installed_locales() - result = {} - if general: - if general in installed_locales: - storage['CMD_LOCALE'] = {'LC_ALL':general} - else: - log(f"{get_locale_mode_text('LC_ALL')} {general} is not installed. Defaulting to C",fg="yellow",level=logging.WARNING) - return - - if numbers: - if numbers in installed_locales: - result["LC_NUMERIC"] = numbers - else: - log(f"{get_locale_mode_text('LC_NUMERIC')} {numbers} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if charset: - if charset in installed_locales: - result["LC_CTYPE"] = charset - else: - log(f"{get_locale_mode_text('LC_CTYPE')} {charset} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if time: - if time in installed_locales: - result["LC_TIME"] = time - else: - log(f"{get_locale_mode_text('LC_TIME')} {time} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if collate: - if collate in installed_locales: - result["LC_COLLATE"] = collate - else: - log(f"{get_locale_mode_text('LC_COLLATE')} {collate} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - if messages: - if messages in installed_locales: - result["LC_MESSAGES"] = messages - else: - log(f"{get_locale_mode_text('LC_MESSAGES')} {messages} is not installed. Defaulting to installation language",fg="yellow",level=logging.WARNING) - storage['CMD_LOCALE'] = result - -def host_locale_environ(func :Callable): - """ decorator when we want a function executing in the host's locale environment """ - def wrapper(*args, **kwargs): - unset_cmd_locale() - result = func(*args,**kwargs) - reset_cmd_locale() - return result - return wrapper - -def c_locale_environ(func :Callable): - """ decorator when we want a function executing in the C locale environment """ - def wrapper(*args, **kwargs): - set_cmd_locale(general='C') - result = func(*args,**kwargs) - reset_cmd_locale() - return result - return wrapper - -def list_installed_locales() -> List[str]: - lista = [] - for line in SysCommand('locale -a'): - lista.append(line.decode('UTF-8').strip()) - return lista - -def list_x11_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def verify_keyboard_layout(layout :str) -> bool: - for language in list_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def verify_x11_keyboard_layout(layout :str) -> bool: - for language in list_x11_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def search_keyboard_layout(layout :str) -> Iterator[str]: - for language in list_keyboard_languages(): - if layout.lower() in language.lower(): - yield language - - -def set_keyboard_language(locale :str) -> bool: - if len(locale.strip()): - if not verify_keyboard_layout(locale): - log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR) - return False - - try: - SysCommand(f'localectl set-keymap {locale}') - except SysCallError as error: - raise ServiceException(f"Unable to set locale '{locale}' for console: {error}") - - return True - - return False - - -def list_timezones() -> Iterator[str]: - for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 53a5e8d2..f9b09b53 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import shlex import time from dataclasses import dataclass @@ -9,7 +8,7 @@ from typing import Optional, List from . import disk from .general import SysCommand, generate_password, SysCommandWorker -from .output import log +from .output import info, debug from .exceptions import SysCallError, DiskError from .storage import storage @@ -61,7 +60,7 @@ class Luks2: iter_time: int = 10000, key_file: Optional[Path] = None ) -> Path: - log(f'Luks2 encrypting: {self.luks_dev_path}', level=logging.INFO) + info(f'Luks2 encrypting: {self.luks_dev_path}') byte_password = self._password_bytes() @@ -95,21 +94,21 @@ class Luks2: try: SysCommand(cryptsetup_args) break - except SysCallError as error: + except SysCallError as err: time.sleep(storage['DISK_TIMEOUTS']) if retry_attempt != storage['DISK_RETRY_ATTEMPTS'] - 1: continue - if error.exit_code == 1: - log(f'luks2 partition currently in use: {self.luks_dev_path}') - log('Attempting to unmount, crypt-close and trying encryption again') + if err.exit_code == 1: + info(f'luks2 partition currently in use: {self.luks_dev_path}') + info('Attempting to unmount, crypt-close and trying encryption again') self.lock() # Then try again to set up the crypt-device SysCommand(cryptsetup_args) else: - raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {error}') + raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {err}') return key_file @@ -119,7 +118,7 @@ class Luks2: try: return SysCommand(command).decode().strip() # type: ignore except SysCallError as err: - log(f'Unable to get UUID for Luks device: {self.luks_dev_path}', level=logging.INFO) + info(f'Unable to get UUID for Luks device: {self.luks_dev_path}') raise err def is_unlocked(self) -> bool: @@ -133,7 +132,7 @@ class Luks2: :param key_file: An alternative key file :type key_file: Path """ - log(f'Unlocking luks2 device: {self.luks_dev_path}', level=logging.DEBUG) + debug(f'Unlocking luks2 device: {self.luks_dev_path}') if not self.mapper_name: raise ValueError('mapper name missing') @@ -170,11 +169,11 @@ class Luks2: for child in lsblk_info.children: # Unmount the child location for mountpoint in child.mountpoints: - log(f'Unmounting {mountpoint}', level=logging.DEBUG) + debug(f'Unmounting {mountpoint}') disk.device_handler.umount(mountpoint, recursive=True) # And close it if possible. - log(f"Closing crypt device {child.name}", level=logging.DEBUG) + debug(f"Closing crypt device {child.name}") SysCommand(f"cryptsetup close {child.name}") self._mapper_dev = None @@ -194,10 +193,10 @@ class Luks2: if key_file.exists(): if not override: - log(f'Key file {key_file} already exists, keeping existing') + info(f'Key file {key_file} already exists, keeping existing') return else: - log(f'Key file {key_file} already exists, overriding') + info(f'Key file {key_file} already exists, overriding') key_file_path.mkdir(parents=True, exist_ok=True) @@ -210,7 +209,7 @@ class Luks2: self._crypttab(crypttab_path, key_file, options=["luks", "key-slot=1"]) def _add_key(self, key_file: Path): - log(f'Adding additional key-file {key_file}', level=logging.INFO) + info(f'Adding additional key-file {key_file}') command = f'/usr/bin/cryptsetup -q -v luksAddKey {self.luks_dev_path} {key_file}' worker = SysCommandWorker(command, environment_vars={'LC_ALL': 'C'}) @@ -230,7 +229,7 @@ class Luks2: key_file: Path, options: List[str] ) -> None: - log(f'Adding crypttab entry for key {key_file}', level=logging.INFO) + info(f'Adding crypttab entry for key {key_file}') with open(crypttab_path, 'a') as crypttab: opt = ','.join(options) diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index e44d65a4..2bd56374 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -1,11 +1,10 @@ from __future__ import annotations -import logging from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu, MenuSelectionType -from ..locale_helpers import set_keyboard_language -from ..output import log +from ..locale import set_keyboard_language +from ..output import error from ..translationhandler import TranslationHandler, Language if TYPE_CHECKING: @@ -211,7 +210,7 @@ class AbstractMenu: # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: skip processing when it comes from a planified exit if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') + error(args[1]) print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues") raise args[1] @@ -483,7 +482,7 @@ class AbstractMenu: yield item def _select_archinstall_language(self, preset: Language) -> Language: - from ..user_interaction.general_conf import select_archinstall_language + from ..interactions.general_conf import select_archinstall_language language = select_archinstall_language(self.translation_handler.translated_languages, preset) self._translation_handler.activate(language) return language diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index f3fdb85f..768dfe55 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -6,11 +6,8 @@ from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable from simple_term_menu import TerminalMenu # type: ignore from ..exceptions import RequirementError -from ..output import log +from ..output import debug -from collections.abc import Iterable -import sys -import logging if TYPE_CHECKING: _: Any @@ -127,33 +124,15 @@ class Menu(TerminalMenu): :param extra_bottom_space: Add an extra empty line at the end of the menu :type extra_bottom_space: bool """ - # we guarantee the inmutability of the options outside the class. - # an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case - # we recourse to make them lists before, but thru an exceptions - # this is the old code, which is not maintenable with more types - # options = copy(list(p_options) if isinstance(p_options,(type({}.keys()),type({}.values()))) else p_options) - # We check that the options are iterable. If not we abort. Else we copy them to lists - # it options is a dictionary we use the values as entries of the list - # if options is a string object, each character becomes an entry - # if options is a list, we implictily build a copy to maintain immutability - if not isinstance(p_options,Iterable): - log(f"Objects of type {type(p_options)} is not iterable, and are not supported at Menu",fg="red") - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) - raise RequirementError("Menu() requires an iterable as option.") - - if isinstance(p_options,dict): + if isinstance(p_options, Dict): options = list(p_options.keys()) else: options = list(p_options) if not options: - log(" * Menu didn't find any options to choose from * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires at least one option to proceed.') if any([o for o in options if not isinstance(o, str)]): - log(" * Menu options must be of type string * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires the options to be of type string') if sort: @@ -343,7 +322,7 @@ class Menu(TerminalMenu): idx = self._menu_options.index(self._default_menu_value) indexes.append(idx) except (IndexError, ValueError): - log(f'Error finding index of {p}: {self._menu_options}', level=logging.DEBUG) + debug(f'Error finding index of {p}: {self._menu_options}') if len(indexes) == 0: indexes.append(0) diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index c6c5c8e4..62a0b081 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,4 +1,3 @@ -import logging import pathlib import urllib.error import urllib.request @@ -6,7 +5,7 @@ from typing import Union, Iterable, Dict, Any, List from dataclasses import dataclass from .general import SysCommand -from .output import log +from .output import info, warn from .exceptions import SysCallError from .storage import storage @@ -136,7 +135,7 @@ def use_mirrors( regions: Dict[str, Iterable[str]], destination: str = '/etc/pacman.d/mirrorlist' ): - log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) + info(f'A new package mirror-list has been created: {destination}') with open(destination, 'w') as mirrorlist: for region, mirrors in regions.items(): for mirror in mirrors: @@ -170,7 +169,7 @@ def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]: try: response = urllib.request.urlopen(url) except urllib.error.URLError as err: - log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="orange") + warn(f'Could not fetch an active mirror-list: {err}') return regions mirrorlist = response.read() diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 38254c99..e21cda33 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -1,12 +1,11 @@ from __future__ import annotations -import logging import sys from enum import Enum from typing import List -from ..hardware import has_uefi -from ..output import log +from ..hardware import SysInfo +from ..output import warn class Bootloader(Enum): @@ -23,7 +22,7 @@ class Bootloader(Enum): @classmethod def get_default(cls) -> Bootloader: - if has_uefi(): + if SysInfo.has_uefi(): return Bootloader.Systemd else: return Bootloader.Grub @@ -35,6 +34,6 @@ class Bootloader(Enum): if bootloader not in cls.values(): values = ', '.join(cls.values()) - log(f'Invalid bootloader value "{bootloader}". Allowed values: {values}', level=logging.WARN) + warn(f'Invalid bootloader value "{bootloader}". Allowed values: {values}') sys.exit(1) return Bootloader(bootloader) diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index a8795fc1..93dd1c44 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -1,11 +1,10 @@ from __future__ import annotations -import logging from dataclasses import dataclass, field from enum import Enum from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple -from ..output import log +from ..output import debug from ..profile import ProfileConfiguration if TYPE_CHECKING: @@ -138,8 +137,7 @@ class NetworkConfigurationHandler: iface = manual_config.get('iface', None) if iface is None: - log(_('No iface specified for manual configuration')) - exit(1) + raise ValueError('No iface specified for manual configuration') if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): configurations.append( @@ -148,8 +146,7 @@ class NetworkConfigurationHandler: else: ip = manual_config.get('ip', '') if not ip: - log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') - exit(1) + raise ValueError('Manual nic configuration with no auto DHCP requires an IP address') dns = manual_config.get('dns', []) if not isinstance(dns, list): @@ -173,8 +170,7 @@ class NetworkConfigurationHandler: return NicType(nic_type) except ValueError: options = [e.value for e in NicType] - log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') - exit(1) + raise ValueError(f'Unknown nic type: {nic_type}. Possible values are {options}') def parse_arguments(self, config: Any): if isinstance(config, list): # new data format @@ -187,4 +183,4 @@ class NetworkConfigurationHandler: else: # manual configuration settings self._configuration = self._parse_manual_config([config]) else: - log(f'Unable to parse network configuration: {config}', level=logging.DEBUG) + debug(f'Unable to parse network configuration: {config}') diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index b858daaf..6906c320 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -1,4 +1,3 @@ -import logging import os import socket import ssl @@ -8,18 +7,16 @@ from urllib.error import URLError from urllib.parse import urlencode from urllib.request import urlopen -from .exceptions import HardwareIncompatibilityError, SysCallError -from .general import SysCommand -from .output import log +from .exceptions import SysCallError +from .output import error, info, debug from .pacman import run_pacman -from .storage import storage def get_hw_addr(ifname :str) -> str: import fcntl s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) - return ':'.join('%02x' % b for b in info[18:24]) + ret = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) + return ':'.join('%02x' % b for b in ret[18:24]) def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: @@ -36,26 +33,26 @@ def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]: def check_mirror_reachable() -> bool: - log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO) + info("Testing connectivity to the Arch Linux mirrors...") try: run_pacman("-Sy") return True except SysCallError as err: if os.geteuid() != 0: - log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") - log(f'exit_code: {err.exit_code}, Error: {err.message}', level=logging.DEBUG) + error("check_mirror_reachable() uses 'pacman -Sy' which requires root.") + debug(f'exit_code: {err.exit_code}, Error: {err.message}') return False def update_keyring() -> bool: - log("Updating archlinux-keyring ...", level=logging.INFO) + info("Updating archlinux-keyring ...") try: run_pacman("-Sy --noconfirm archlinux-keyring") return True except SysCallError: if os.geteuid() != 0: - log("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.", level=logging.ERROR, fg="red") + error("update_keyring() uses 'pacman -Sy archlinux-keyring' which requires root.") return False @@ -80,38 +77,6 @@ def enrich_iface_types(interfaces: Union[Dict[str, Any], List[str]]) -> Dict[str return result -def wireless_scan(interface :str) -> None: - interfaces = enrich_iface_types(list(list_interfaces().values())) - if interfaces[interface] != 'WIRELESS': - raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") - - try: - SysCommand(f"iwctl station {interface} scan") - except SysCallError as error: - raise SystemError(f"Could not scan for wireless networks: {error}") - - if '_WIFI' not in storage: - storage['_WIFI'] = {} - if interface not in storage['_WIFI']: - storage['_WIFI'][interface] = {} - - storage['_WIFI'][interface]['scanning'] = True - - -# TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25 -def get_wireless_networks(interface :str) -> None: - # TODO: Make this oneliner pritter to check if the interface is scanning or not. - # TODO: Rename this to list_wireless_networks() as it doesn't return anything - if '_WIFI' not in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False: - import time - - wireless_scan(interface) - time.sleep(5) - - for line in SysCommand(f"iwctl station {interface} get-networks"): - print(line) - - def fetch_data_from_url(url: str, params: Optional[Dict] = None) -> str: ssl_context = ssl.create_default_context() ssl_context.check_hostname = False diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index d65f835f..bd31b5b3 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -1,15 +1,16 @@ import logging import os import sys +from enum import Enum + from pathlib import Path from typing import Dict, Union, List, Any, Callable, Optional +from dataclasses import asdict, is_dataclass from .storage import storage -from dataclasses import asdict, is_dataclass class FormattedOutput: - @classmethod def values( cls, @@ -118,7 +119,7 @@ class FormattedOutput: class Journald: @staticmethod - def log(message :str, level :int = logging.DEBUG) -> None: + def log(message: str, level: int = logging.DEBUG) -> None: try: import systemd.journal # type: ignore except ModuleNotFoundError: @@ -134,16 +135,37 @@ class Journald: log_adapter.log(level, message) -# TODO: Replace log() for session based logging. -class SessionLogging: - def __init__(self): - pass +def check_log_permissions(): + filename = storage.get('LOG_FILE', None) + + if not filename: + return + + log_dir = storage.get('LOG_PATH', Path('./')) + absolute_logfile = log_dir / filename + + try: + log_dir.mkdir(exist_ok=True, parents=True) + with absolute_logfile.open('a') as fp: + fp.write('') + except PermissionError: + # Fallback to creating the log file in the current folder + fallback_log_file = Path('./').absolute() / filename + absolute_logfile = fallback_log_file + absolute_logfile.mkdir(exist_ok=True, parents=True) + storage['LOG_PATH'] = Path('./').absolute() + err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {fallback_log_file} instead." + warn(err_string) -# Found first reference here: https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python -# And re-used this: https://github.com/django/django/blob/master/django/core/management/color.py#L12 -def supports_color() -> bool: + +def _supports_color() -> bool: """ + Found first reference here: + https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python + And re-used this: + https://github.com/django/django/blob/master/django/core/management/color.py#L12 + Return True if the running system's terminal supports color, and False otherwise. """ @@ -154,13 +176,30 @@ def supports_color() -> bool: return supported_platform and is_a_tty -# Heavily influenced by: https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13 -# Color options here: https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i -def stylize_output(text: str, *opts :str, **kwargs) -> str: +class Font(Enum): + bold = '1' + italic = '3' + underscore = '4' + blink = '5' + reverse = '7' + conceal = '8' + + +def _stylize_output( + text: str, + fg: str, + bg: Optional[str], + reset: bool, + font: List[Font] = [], +) -> str: """ + Heavily influenced by: + https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13 + Color options here: + https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i + Adds styling to a text given a set of color arguments. """ - opt_dict = {'bold': '1', 'italic': '3', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} colors = { 'black' : '0', 'red' : '1', @@ -178,65 +217,72 @@ def stylize_output(text: str, *opts :str, **kwargs) -> str: 'darkgray' : '8;5;240', 'lightgray' : '8;5;256' } + foreground = {key: f'3{colors[key]}' for key in colors} background = {key: f'4{colors[key]}' for key in colors} - reset = '0' - code_list = [] - if text == '' and len(opts) == 1 and opts[0] == 'reset': - return '\x1b[%sm' % reset - for k, v in kwargs.items(): - if k == 'fg': - code_list.append(foreground[str(v)]) - elif k == 'bg': - code_list.append(background[str(v)]) + if text == '' and reset: + return '\x1b[%sm' % '0' + + code_list.append(foreground[str(fg)]) + + if bg: + code_list.append(background[str(bg)]) + + for o in font: + code_list.append(o.value) + + ansi = ';'.join(code_list) + + return f'\033[{ansi}m{text}\033[0m' + - for o in opts: - if o in opt_dict: - code_list.append(opt_dict[o]) +def info(*msgs: str): + log(*msgs, level=logging.INFO) - if 'noreset' not in opts: - text = '%s\x1b[%sm' % (text or '', reset) - return '%s%s' % (('\x1b[%sm' % ';'.join(code_list)), text or '') +def debug(*msgs: str): + log(*msgs, level=logging.DEBUG) -def log(*args :str, **kwargs :Union[str, int, Dict[str, Union[str, int]]]) -> None: - string = orig_string = ' '.join([str(x) for x in args]) +def error(*msgs: str): + log(*msgs, level=logging.ERROR, fg='red') + + +def warn(*msgs: str): + log(*msgs, level=logging.WARNING, fg='yellow') + + +def log( + *msgs: str, + level: int = logging.INFO, + fg: str = 'white', + bg: Optional[str] = None, + reset: bool = False, + font: List[Font] = [] +): + text = orig_string = ' '.join([str(x) for x in msgs]) # Attempt to colorize the output if supported # Insert default colors and override with **kwargs - if supports_color(): - kwargs = {'fg': 'white', **kwargs} - string = stylize_output(string, **kwargs) + if _supports_color(): + text = _stylize_output(text, fg, bg, reset, font) # If a logfile is defined in storage, # we use that one to output everything if filename := storage.get('LOG_FILE', None): - absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) + log_dir = storage.get('LOG_PATH', Path('./')) + absolute_logfile = log_dir / filename - try: - Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True) - with open(absolute_logfile, 'a') as log_file: - log_file.write("") - except PermissionError: - # Fallback to creating the log file in the current folder - err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute() / filename} instead." - absolute_logfile = Path('./').absolute() / filename - absolute_logfile.parents[0].mkdir(exist_ok=True) - absolute_logfile = str(absolute_logfile) - storage['LOG_PATH'] = './' - log(err_string, fg="red") - - with open(absolute_logfile, 'a') as log_file: - log_file.write(f"{orig_string}\n") - - Journald.log(string, level=int(str(kwargs.get('level', logging.INFO)))) + with open(absolute_logfile, 'a') as fp: + fp.write(f"{orig_string}\n") + + Journald.log(text, level=level) # Finally, print the log unless we skipped it based on level. # We use sys.stdout.write()+flush() instead of print() to try and # fix issue #94 - if kwargs.get('level', logging.INFO) != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): - sys.stdout.write(f"{string}\n") + if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): + sys.stdout.write(f"{text}\n") sys.stdout.flush() diff --git a/archinstall/lib/pacman.py b/archinstall/lib/pacman.py index 0dfd5afa..f5514f05 100644 --- a/archinstall/lib/pacman.py +++ b/archinstall/lib/pacman.py @@ -1,10 +1,9 @@ -import logging import pathlib import time from typing import TYPE_CHECKING, Any from .general import SysCommand -from .output import log +from .output import warn, error if TYPE_CHECKING: _: Any @@ -19,14 +18,14 @@ def run_pacman(args :str, default_cmd :str = 'pacman') -> SysCommand: pacman_db_lock = pathlib.Path('/var/lib/pacman/db.lck') if pacman_db_lock.exists(): - log(_('Pacman is already running, waiting maximum 10 minutes for it to terminate.'), level=logging.WARNING, fg="red") + warn(_('Pacman is already running, waiting maximum 10 minutes for it to terminate.')) started = time.time() while pacman_db_lock.exists(): time.sleep(0.25) if time.time() - started > (60 * 10): - log(_('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'), level=logging.WARNING, fg="red") + error(_('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.')) exit(1) return SysCommand(f'{default_cmd} {args}') diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index b1ece04f..4ccb0666 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -1,6 +1,5 @@ import hashlib import importlib -import logging import os import sys import urllib.parse @@ -9,7 +8,7 @@ from importlib import metadata from pathlib import Path from typing import Optional, List -from .output import log +from .output import error, info, warn from .storage import storage plugins = {} @@ -24,11 +23,13 @@ for plugin_definition in metadata.entry_points().select(group='archinstall.plugi try: plugins[plugin_definition.name] = plugin_entrypoint() except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when loading the plugin: {plugin_definition}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when loading the plugin: {plugin_definition}" + ) -def localize_path(path: Path) -> Path: +def _localize_path(path: Path) -> Path: """ Support structures for load_plugin() """ @@ -45,7 +46,7 @@ def localize_path(path: Path) -> Path: return path -def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: +def _import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str]: if not namespace: namespace = os.path.basename(path) @@ -61,8 +62,10 @@ def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str return namespace except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when loading the plugin: {path}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when loading the plugin: {path}" + ) try: del sys.modules[namespace] @@ -72,7 +75,7 @@ def import_via_path(path: Path, namespace: Optional[str] = None) -> Optional[str return namespace -def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: +def _find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: indices = [idx for idx, elem in enumerate(haystack) if elem == needle] if n <= len(indices): return indices[n - 1] @@ -82,34 +85,36 @@ def find_nth(haystack: List[str], needle: str, n: int) -> Optional[int]: def load_plugin(path: Path): namespace: Optional[str] = None parsed_url = urllib.parse.urlparse(str(path)) - log(f"Loading plugin from url {parsed_url}.", level=logging.INFO) + info(f"Loading plugin from url {parsed_url}") # The Profile was not a direct match on a remote URL if not parsed_url.scheme: # Path was not found in any known examples, check if it's an absolute path if os.path.isfile(path): - namespace = import_via_path(path) + namespace = _import_via_path(path) elif parsed_url.scheme in ('https', 'http'): - localized = localize_path(path) - namespace = import_via_path(localized) + localized = _localize_path(path) + namespace = _import_via_path(localized) if namespace and namespace in sys.modules: # Version dependency via __archinstall__version__ variable (if present) in the plugin # Any errors in version inconsistency will be handled through normal error handling if not defined. if hasattr(sys.modules[namespace], '__archinstall__version__'): - archinstall_major_and_minor_version = float(storage['__version__'][:find_nth(storage['__version__'], '.', 2)]) + archinstall_major_and_minor_version = float(storage['__version__'][:_find_nth(storage['__version__'], '.', 2)]) if sys.modules[namespace].__archinstall__version__ < archinstall_major_and_minor_version: - log(f"Plugin {sys.modules[namespace]} does not support the current Archinstall version.", fg="red", level=logging.ERROR) + error(f"Plugin {sys.modules[namespace]} does not support the current Archinstall version.") # Locate the plugin entry-point called Plugin() # This in accordance with the entry_points() from setup.cfg above if hasattr(sys.modules[namespace], 'Plugin'): try: plugins[namespace] = sys.modules[namespace].Plugin() - log(f"Plugin {plugins[namespace]} has been loaded.", fg="gray", level=logging.INFO) + info(f"Plugin {plugins[namespace]} has been loaded.") except Exception as err: - log(f'Error: {err}', level=logging.ERROR) - log(f"The above error was detected when initiating the plugin: {path}", fg="red", level=logging.ERROR) + error( + f'Error: {err}', + f"The above error was detected when initiating the plugin: {path}" + ) else: - log(f"Plugin '{path}' is missing a valid entry-point or is corrupt.", fg="yellow", level=logging.WARNING) + warn(f"Plugin '{path}' is missing a valid entry-point or is corrupt.") diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index 6462685a..213466a6 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -6,7 +6,7 @@ from archinstall.default_profiles.profile import Profile, GreeterType from .profile_model import ProfileConfiguration from ..hardware import AVAILABLE_GFX_DRIVERS from ..menu import Menu, MenuSelectionType, AbstractSubMenu, Selector -from ..user_interaction.system_conf import select_driver +from ..interactions.system_conf import select_driver if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 6ed95f8e..16fef251 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -1,7 +1,6 @@ from __future__ import annotations import importlib.util -import logging import sys from collections import Counter from functools import cached_property @@ -15,7 +14,7 @@ from .profile_model import ProfileConfiguration from ..hardware import AVAILABLE_GFX_DRIVERS from ..menu import MenuSelectionType, Menu, MenuSelection from ..networking import list_interfaces, fetch_data_from_url -from ..output import log +from ..output import error, debug, info, warn from ..storage import storage if TYPE_CHECKING: @@ -106,7 +105,7 @@ class ProfileHandler: invalid = ', '.join([k for k, v in resolved.items() if v is None]) if invalid: - log(f'No profile definition found: {invalid}') + info(f'No profile definition found: {invalid}') custom_settings = profile_config.get('custom_settings', {}) for profile in valid: @@ -216,7 +215,7 @@ class ProfileHandler: install_session.add_additional_packages(additional_pkg) except Exception as err: - log(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}", level=logging.WARNING, fg="yellow") + warn(f"Could not handle nvidia and linuz-zen specific situations during xorg installation: {err}") # Prep didn't run, so there's no driver to install install_session.add_additional_packages(['xorg-server', 'xorg-xinit']) @@ -250,7 +249,7 @@ class ProfileHandler: self.add_custom_profiles(profiles) except ValueError: err = str(_('Unable to fetch profile from specified url: {}')).format(url) - log(err, level=logging.ERROR, fg="red") + error(err) def _load_profile_class(self, module: ModuleType) -> List[Profile]: """ @@ -264,7 +263,7 @@ class ProfileHandler: if isinstance(cls_, Profile): profiles.append(cls_) except Exception: - log(f'Cannot import {module}, it does not appear to be a Profile class', level=logging.DEBUG) + debug(f'Cannot import {module}, it does not appear to be a Profile class') return profiles @@ -278,7 +277,7 @@ class ProfileHandler: if len(duplicates) > 0: err = str(_('Profiles must have unique name, but profile definitions with duplicate name found: {}')).format(duplicates[0][0]) - log(err, level=logging.ERROR, fg="red") + error(err) sys.exit(1) def _is_legacy(self, file: Path) -> bool: @@ -297,15 +296,15 @@ class ProfileHandler: Process a file for profile definitions """ if self._is_legacy(file): - log(f'Cannot import {file} because it is no longer supported, please use the new profile format') + info(f'Cannot import {file} because it is no longer supported, please use the new profile format') return [] if not file.is_file(): - log(f'Cannot find profile file {file}') + info(f'Cannot find profile file {file}') return [] name = file.name.removesuffix(file.suffix) - log(f'Importing profile: {file}', level=logging.DEBUG) + debug(f'Importing profile: {file}') try: spec = importlib.util.spec_from_file_location(name, file) @@ -315,7 +314,7 @@ class ProfileHandler: spec.loader.exec_module(imported) return self._load_profile_class(imported) except Exception as e: - log(f'Unable to parse file {file}: {e}', level=logging.ERROR) + error(f'Unable to parse file {file}: {e}') return [] diff --git a/archinstall/lib/services.py b/archinstall/lib/services.py deleted file mode 100644 index b177052b..00000000 --- a/archinstall/lib/services.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -from .general import SysCommand - - -def service_state(service_name: str) -> str: - if os.path.splitext(service_name)[1] != '.service': - service_name += '.service' # Just to be safe - - state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) - - return state.strip().decode('UTF-8') diff --git a/archinstall/lib/storage.py b/archinstall/lib/storage.py index 5a54d816..2f256e5d 100644 --- a/archinstall/lib/storage.py +++ b/archinstall/lib/storage.py @@ -11,8 +11,8 @@ from pathlib import Path storage: Dict[str, Any] = { 'PROFILE': Path(__file__).parent.parent.joinpath('default_profiles'), - 'LOG_PATH': '/var/log/archinstall', - 'LOG_FILE': 'install.log', + 'LOG_PATH': Path('/var/log/archinstall'), + 'LOG_FILE': Path('install.log'), 'MOUNT_POINT': Path('/mnt/archinstall'), 'ENC_IDENTIFIER': 'ainst', 'DISK_TIMEOUTS' : 1, # seconds diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py deleted file mode 100644 index 6ccbc5f6..00000000 --- a/archinstall/lib/systemd.py +++ /dev/null @@ -1,110 +0,0 @@ -import logging -import time -from typing import Iterator, Optional -from .exceptions import SysCallError -from .general import SysCommand, SysCommandWorker, locate_binary -from .installer import Installer -from .output import log -from .storage import storage - - -class Boot: - def __init__(self, installation: Installer): - self.instance = installation - self.container_name = 'archinstall' - self.session: Optional[SysCommandWorker] = None - self.ready = False - - def __enter__(self) -> 'Boot': - if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: - raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") - - if existing_session: - self.session = existing_session.session - self.ready = existing_session.ready - else: - # '-P' or --console=pipe could help us not having to do a bunch - # of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual. - self.session = SysCommandWorker([ - '/usr/bin/systemd-nspawn', - '-D', str(self.instance.target), - '--timezone=off', - '-b', - '--no-pager', - '--machine', self.container_name - ]) - - if not self.ready and self.session: - while self.session.is_alive(): - if b' login:' in self.session: - self.ready = True - break - - storage['active_boot'] = self - return self - - def __exit__(self, *args :str, **kwargs :str) -> None: - # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. - # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager - - if len(args) >= 2 and args[1]: - log(args[1], level=logging.ERROR, fg='red') - log(f"The error above occurred in a temporary boot-up of the installation {self.instance}", level=logging.ERROR, fg="red") - - shutdown = None - shutdown_exit_code: Optional[int] = -1 - - try: - shutdown = SysCommand(f'systemd-run --machine={self.container_name} --pty shutdown now') - except SysCallError as error: - shutdown_exit_code = error.exit_code - - if self.session: - while self.session.is_alive(): - time.sleep(0.25) - - if shutdown and shutdown.exit_code: - shutdown_exit_code = shutdown.exit_code - - if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0): - storage['active_boot'] = None - else: - session_exit_code = self.session.exit_code if self.session else -1 - - raise SysCallError( - f"Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}", - exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])) - ) - - def __iter__(self) -> Iterator[bytes]: - if self.session: - for value in self.session: - yield value - - def __contains__(self, key: bytes) -> bool: - if self.session is None: - return False - - return key in self.session - - def is_alive(self) -> bool: - if self.session is None: - return False - - return self.session.is_alive() - - def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand: - if cmd[0][0] != '/' and cmd[0][:2] != './': - # This check is also done in SysCommand & SysCommandWorker. - # However, that check is done for `machinectl` and not for our chroot command. - # So this wrapper for SysCommand will do this additionally. - - cmd[0] = locate_binary(cmd[0]) - - return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) - - def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker: - if cmd[0][0] != '/' and cmd[0][:2] != './': - cmd[0] = locate_binary(cmd[0]) - - return SysCommandWorker(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index 0d74f974..5f0f0695 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -1,14 +1,14 @@ from __future__ import annotations import json -import logging import os import gettext from dataclasses import dataclass from pathlib import Path from typing import List, Dict, Any, TYPE_CHECKING, Optional -from .exceptions import TranslationError + +from .output import error, debug if TYPE_CHECKING: _: Any @@ -80,8 +80,8 @@ class TranslationHandler: language = Language(abbr, lang, translation, percent, translated_lang) languages.append(language) - except FileNotFoundError as error: - raise TranslationError(f"Could not locate language file for '{lang}': {error}") + except FileNotFoundError as err: + raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}") return languages @@ -89,12 +89,12 @@ class TranslationHandler: """ Set the provided font as the new terminal font """ - from .general import SysCommand, log + from .general import SysCommand try: - log(f'Setting font: {font}', level=logging.DEBUG) + debug(f'Setting font: {font}') SysCommand(f'setfont {font}') except Exception: - log(f'Unable to set font {font}', level=logging.ERROR) + error(f'Unable to set font {font}') def _load_language_mappings(self) -> List[Dict[str, Any]]: """ diff --git a/archinstall/lib/user_interaction/__init__.py b/archinstall/lib/user_interaction/__init__.py deleted file mode 100644 index 5ee89de0..00000000 --- a/archinstall/lib/user_interaction/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .manage_users_conf import ask_for_additional_users -from .locale_conf import select_locale_lang, select_locale_enc -from .system_conf import select_kernel, select_driver, ask_for_bootloader, ask_for_swap -from .network_conf import ask_to_configure_network -from .general_conf import ( - ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, - select_archinstall_language, ask_additional_packages_to_install, - select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads -) -from .utils import get_password diff --git a/archinstall/lib/user_interaction/disk_conf.py b/archinstall/lib/user_interaction/disk_conf.py deleted file mode 100644 index a77e950a..00000000 --- a/archinstall/lib/user_interaction/disk_conf.py +++ /dev/null @@ -1,391 +0,0 @@ -from __future__ import annotations - -import logging -from pathlib import Path -from typing import Any, TYPE_CHECKING, Optional, List, Tuple - -from .. import disk -from ..hardware import has_uefi -from ..menu import Menu, MenuSelectionType, TableMenu -from ..output import FormattedOutput -from ..output import log -from ..utils.util import prompt_dir - -if TYPE_CHECKING: - _: Any - - -def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: - """ - Asks the user to select one or multiple devices - - :return: List of selected devices - :rtype: list - """ - - def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: - dev = disk.device_handler.get_device(selection.path) - if dev and dev.partition_infos: - return FormattedOutput.as_table(dev.partition_infos) - return None - - if preset is None: - preset = [] - - title = str(_('Select one or more devices to use and configure')) - warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) - - devices = disk.device_handler.devices - options = [d.device_info for d in devices] - preset_value = [p.device_info for p in preset] - - choice = TableMenu( - title, - data=options, - multi=True, - preset=preset_value, - preview_command=_preview_device_selection, - preview_title=str(_('Existing Partitions')), - preview_size=0.2, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: - selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore - selected_devices = [] - - for device in devices: - if device.device_info in selected_device_info: - selected_devices.append(device) - - return selected_devices - - -def get_default_partition_layout( - devices: List[disk.BDevice], - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_option: bool = False -) -> List[disk.DeviceModification]: - - if len(devices) == 1: - device_modification = suggest_single_disk_layout( - devices[0], - filesystem_type=filesystem_type, - advanced_options=advanced_option - ) - return [device_modification] - else: - return suggest_multi_disk_layout( - devices, - filesystem_type=filesystem_type, - advanced_options=advanced_option - ) - - -def _manual_partitioning( - preset: List[disk.DeviceModification], - devices: List[disk.BDevice] -) -> List[disk.DeviceModification]: - modifications = [] - for device in devices: - mod = next(filter(lambda x: x.device == device, preset), None) - if not mod: - mod = disk.DeviceModification(device, wipe=False) - - if partitions := disk.manual_partitioning(device, preset=mod.partitions): - mod.partitions = partitions - modifications.append(mod) - - return modifications - - -def select_disk_config( - preset: Optional[disk.DiskLayoutConfiguration] = None, - advanced_option: bool = False -) -> Optional[disk.DiskLayoutConfiguration]: - default_layout = disk.DiskLayoutType.Default.display_msg() - manual_mode = disk.DiskLayoutType.Manual.display_msg() - pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() - - options = [default_layout, manual_mode, pre_mount_mode] - preset_value = preset.config_type.display_msg() if preset else None - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select a partitioning option'), - options, - allow_reset=True, - allow_reset_warning_msg=warning, - sort=False, - preview_size=0.2, - preset_values=preset_value - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - case MenuSelectionType.Selection: - if choice.single_value == pre_mount_mode: - output = "You will use whatever drive-setup is mounted at the specified directory\n" - output += "WARNING: Archinstall won't check the suitability of this setup\n" - - path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) - mods = disk.device_handler.detect_pre_mounted_mods(path) - - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Pre_mount, - relative_mountpoint=path, - device_modifications=mods - ) - - preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - - devices = select_devices(preset_devices) - - if not devices: - return None - - if choice.value == default_layout: - modifications = get_default_partition_layout(devices, advanced_option=advanced_option) - if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Default, - device_modifications=modifications - ) - elif choice.value == manual_mode: - preset_mods = preset.device_modifications if preset else [] - modifications = _manual_partitioning(preset_mods, devices) - - if modifications: - return disk.DiskLayoutConfiguration( - config_type=disk.DiskLayoutType.Manual, - device_modifications=modifications - ) - - return None - - -def _boot_partition() -> disk.PartitionModification: - if has_uefi(): - start = disk.Size(1, disk.Unit.MiB) - size = disk.Size(512, disk.Unit.MiB) - else: - start = disk.Size(3, disk.Unit.MiB) - size = disk.Size(203, disk.Unit.MiB) - - # boot partition - return disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=start, - length=size, - mountpoint=Path('/boot'), - fs_type=disk.FilesystemType.Fat32, - flags=[disk.PartitionFlag.Boot] - ) - - -def ask_for_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: - options = { - 'btrfs': disk.FilesystemType.Btrfs, - 'ext4': disk.FilesystemType.Ext4, - 'xfs': disk.FilesystemType.Xfs, - 'f2fs': disk.FilesystemType.F2fs - } - - if advanced_options: - options.update({'ntfs': disk.FilesystemType.Ntfs}) - - prompt = _('Select which filesystem your main partition should use') - choice = Menu(prompt, options, skip=False, sort=False).run() - return options[choice.single_value] - - -def suggest_single_disk_layout( - device: disk.BDevice, - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_options: bool = False, - separate_home: Optional[bool] = None -) -> disk.DeviceModification: - if not filesystem_type: - filesystem_type = ask_for_main_filesystem_format(advanced_options) - - min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) - root_partition_size = disk.Size(20, disk.Unit.GiB) - using_subvolumes = False - using_home_partition = False - compression = False - device_size_gib = device.device_info.total_size - - if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_subvolumes = choice.value == Menu.yes() - - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - device_modification = disk.DeviceModification(device, wipe=True) - - # Used for reference: https://wiki.archlinux.org/title/partitioning - # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? - - # TODO: On BIOS, /boot partition is only needed if the drive will - # be encrypted, otherwise it is not recommended. We should probably - # add a check for whether the drive will be encrypted or not. - - # Increase the UEFI partition if UEFI is detected. - # Also re-align the start to 1MiB since we don't need the first sectors - # like we do in MBR layouts where the boot loader is installed traditionally. - - boot_partition = _boot_partition() - device_modification.add_partition(boot_partition) - - if not using_subvolumes: - if device_size_gib >= min_size_to_allow_home_part: - if separate_home is None: - prompt = str(_('Would you like to create a separate partition for /home?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - using_home_partition = choice.value == Menu.yes() - elif separate_home is True: - using_home_partition = True - else: - using_home_partition = False - - # root partition - start = disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB) - - # Set a size for / (/root) - if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) - else: - length = min(device.device_info.total_size, root_partition_size) - - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=start, - length=length, - mountpoint=Path('/') if not using_subvolumes else None, - fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [], - ) - device_modification.add_partition(root_partition) - - if using_subvolumes: - # https://btrfs.wiki.kernel.org/index.php/FAQ - # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash - # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh - subvolumes = [ - disk.SubvolumeModification(Path('@'), Path('/')), - disk.SubvolumeModification(Path('@home'), Path('/home')), - disk.SubvolumeModification(Path('@log'), Path('/var/log')), - disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), - disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) - ] - root_partition.btrfs_subvols = subvolumes - elif using_home_partition: - # If we don't want to use subvolumes, - # But we want to be able to re-use data between re-installs.. - # A second partition for /home would be nice if we have the space for it - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), - mountpoint=Path('/home'), - fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [] - ) - device_modification.add_partition(home_partition) - - return device_modification - - -def suggest_multi_disk_layout( - devices: List[disk.BDevice], - filesystem_type: Optional[disk.FilesystemType] = None, - advanced_options: bool = False -) -> List[disk.DeviceModification]: - if not devices: - return [] - - # Not really a rock solid foundation of information to stand on, but it's a start: - # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ - # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB) - # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(20, disk.Unit.GiB) - compression = False - - if not filesystem_type: - filesystem_type = ask_for_main_filesystem_format(advanced_options) - - # find proper disk for /home - possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) - home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None - - # find proper device for /root - devices_delta = {} - for device in devices: - if device is not home_device: - delta = device.device_info.total_size - desired_root_partition_size - devices_delta[device] = delta - - sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) - root_device: Optional[disk.BDevice] = sorted_delta[0][0] - - if home_device is None or root_device is None: - text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') - text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) - text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) - Menu(str(text), [str(_('Continue'))], skip=False).run() - return [] - - if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() - - device_paths = ', '.join([str(d.device_info.path) for d in devices]) - log(f"Suggesting multi-disk-layout for devices: {device_paths}", level=logging.DEBUG) - log(f"/root: {root_device.device_info.path}", level=logging.DEBUG) - log(f"/home: {home_device.device_info.path}", level=logging.DEBUG) - - root_device_modification = disk.DeviceModification(root_device, wipe=True) - home_device_modification = disk.DeviceModification(home_device, wipe=True) - - # add boot partition to the root device - boot_partition = _boot_partition() - root_device_modification.add_partition(boot_partition) - - # add root partition to the root device - root_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), - mountpoint=Path('/'), - mount_options=['compress=zstd'] if compression else [], - fs_type=filesystem_type - ) - root_device_modification.add_partition(root_partition) - - # add home partition to home device - home_partition = disk.PartitionModification( - status=disk.ModificationStatus.Create, - type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), - mountpoint=Path('/home'), - mount_options=['compress=zstd'] if compression else [], - fs_type=filesystem_type, - ) - home_device_modification.add_partition(home_partition) - - return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py deleted file mode 100644 index 9722dc4d..00000000 --- a/archinstall/lib/user_interaction/general_conf.py +++ /dev/null @@ -1,244 +0,0 @@ -from __future__ import annotations - -import logging -import pathlib -from typing import List, Any, Optional, Dict, TYPE_CHECKING - -from ..locale_helpers import list_keyboard_languages, list_timezones -from ..menu import MenuSelectionType, Menu, TextInput -from ..mirrors import list_mirrors -from ..output import log -from ..packages.packages import validate_package_list -from ..storage import storage -from ..translationhandler import Language - -if TYPE_CHECKING: - _: Any - - -def ask_ntp(preset: bool = True) -> bool: - prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) - prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) - if preset: - preset_val = Menu.yes() - else: - preset_val = Menu.no() - choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() - - return False if choice.value == Menu.no() else True - - -def ask_hostname(preset: str = '') -> str: - while True: - hostname = TextInput( - str(_('Desired hostname for the installation: ')), - preset - ).run().strip() - - if hostname: - return hostname - - -def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: - timezones = list_timezones() - default = 'UTC' - - choice = Menu( - _('Select a timezone'), - list(timezones), - preset_values=preset, - default_option=default - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: - no_audio = str(_('No audio server')) - choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] - default = 'pipewire' if desktop else no_audio - - choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def select_language(preset: Optional[str] = None) -> Optional[str]: - """ - Asks the user to select a language - Usually this is combined with :ref:`archinstall.list_keyboard_languages`. - - :return: The language/dictionary key of the selected language - :rtype: str - """ - kb_lang = list_keyboard_languages() - # sort alphabetically and then by length - sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - - choice = Menu( - _('Select keyboard layout'), - sorted_kb_lang, - preset_values=preset, - sort=False - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value - - return None - - -def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: - """ - Asks the user to select a mirror or region - Usually this is combined with :ref:`archinstall.list_mirrors`. - - :return: The dictionary information about a mirror/region. - :rtype: dict - """ - if preset_values is None: - preselected = None - else: - preselected = list(preset_values.keys()) - - mirrors = list_mirrors() - - choice = Menu( - _('Select one of the regions to download packages from'), - list(mirrors.keys()), - preset_values=preselected, - multi=True, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Reset: - return {} - case MenuSelectionType.Skip: - return preset_values - case MenuSelectionType.Selection: - return {selected: mirrors[selected] for selected in choice.multi_value} - - return {} - - -def select_archinstall_language(languages: List[Language], preset: Language) -> Language: - # these are the displayed language names which can either be - # the english name of a language or, if present, the - # name of the language in its own language - options = {lang.display_name: lang for lang in languages} - - title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' - title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' - title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' - - choice = Menu( - title, - list(options.keys()), - default_option=preset.display_name, - preview_size=0.5 - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return options[choice.single_value] - - raise ValueError('Language selection not handled') - - -def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: - # Additional packages (with some light weight error handling for invalid package names) - print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) - print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) - - def read_packages(already_defined: list = []) -> list: - display = ' '.join(already_defined) - input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() - return input_packages.split() if input_packages else [] - - pre_set_packages = pre_set_packages if pre_set_packages else [] - packages = read_packages(pre_set_packages) - - if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: - while True: - if len(packages): - # Verify packages that were given - print(_("Verifying that additional packages exist (this might take a few seconds)")) - valid, invalid = validate_package_list(packages) - - if invalid: - log(f"Some packages could not be found in the repository: {invalid}", level=logging.WARNING, fg='red') - packages = read_packages(valid) - continue - break - - return packages - - -def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: - max_downloads = 5 - print(_(f"This option enables the number of parallel downloads that can occur during installation")) - print(_(f"Enter the number of parallel downloads to be enabled.\n (Enter a value between 1 to {max_downloads})\nNote:")) - print(_(f" - Maximum value : {max_downloads} ( Allows {max_downloads} parallel downloads, allows {max_downloads+1} downloads at a time )")) - print(_(f" - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )")) - print(_(f" - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )")) - - while True: - try: - input_number = int(TextInput(_("[Default value: 0] > ")).run().strip() or 0) - if input_number <= 0: - input_number = 0 - elif input_number > max_downloads: - input_number = max_downloads - break - except: - print(_(f"Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]")) - - pacman_conf_path = pathlib.Path("/etc/pacman.conf") - with pacman_conf_path.open() as f: - pacman_conf = f.read().split("\n") - - with pacman_conf_path.open("w") as fwrite: - for line in pacman_conf: - if "ParallelDownloads" in line: - fwrite.write(f"ParallelDownloads = {input_number+1}\n") if not input_number == 0 else fwrite.write("#ParallelDownloads = 0\n") - else: - fwrite.write(f"{line}\n") - - return input_number - - -def select_additional_repositories(preset: List[str]) -> List[str]: - """ - Allows the user to select additional repositories (multilib, and testing) if desired. - - :return: The string as a selected repository - :rtype: string - """ - - repositories = ["multilib", "testing"] - - choice = Menu( - _('Choose which optional additional repositories to enable'), - repositories, - sort=False, - multi=True, - preset_values=preset, - allow_reset=True - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.single_value - - return [] diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py deleted file mode 100644 index cdc3423a..00000000 --- a/archinstall/lib/user_interaction/locale_conf.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -from typing import Any, TYPE_CHECKING, Optional - -from ..locale_helpers import list_locales -from ..menu import Menu, MenuSelectionType - -if TYPE_CHECKING: - _: Any - - -def select_locale_lang(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_lang = set([locale.split()[0] for locale in locales]) - - choice = Menu( - _('Choose which locale language to use'), - list(locale_lang), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None - - -def select_locale_enc(preset: Optional[str] = None) -> Optional[str]: - locales = list_locales() - locale_enc = set([locale.split()[1] for locale in locales]) - - choice = Menu( - _('Choose which locale encoding to use'), - list(locale_enc), - sort=True, - preset_values=preset - ).run() - - match choice.type_: - case MenuSelectionType.Selection: return choice.single_value - case MenuSelectionType.Skip: return preset - - return None diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py deleted file mode 100644 index 879578da..00000000 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import annotations - -import re -from typing import Any, Dict, TYPE_CHECKING, List, Optional - -from .utils import get_password -from ..menu import Menu, ListManager -from ..models.users import User -from ..output import FormattedOutput - -if TYPE_CHECKING: - _: Any - - -class UserList(ListManager): - """ - subclass of ListManager for the managing of user accounts - """ - - def __init__(self, prompt: str, lusers: List[User]): - self._actions = [ - str(_('Add a user')), - str(_('Change password')), - str(_('Promote/Demote user')), - str(_('Delete User')) - ] - super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[User]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - - def selected_action_display(self, user: User) -> str: - return user.username - - def handle_action(self, action: str, entry: Optional[User], data: List[User]) -> List[User]: - if action == self._actions[0]: # add - new_user = self._add_user() - if new_user is not None: - # in case a user with the same username as an existing user - # was created we'll replace the existing one - data = [d for d in data if d.username != new_user.username] - data += [new_user] - elif action == self._actions[1] and entry: # change password - prompt = str(_('Password for user "{}": ').format(entry.username)) - new_password = get_password(prompt=prompt) - if new_password: - user = next(filter(lambda x: x == entry, data)) - user.password = new_password - elif action == self._actions[2] and entry: # promote/demote - user = next(filter(lambda x: x == entry, data)) - user.sudo = False if user.sudo else True - elif action == self._actions[3] and entry: # delete - data = [d for d in data if d != entry] - - return data - - def _check_for_correct_username(self, username: str) -> bool: - if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: - return True - return False - - def _add_user(self) -> Optional[User]: - prompt = '\n\n' + str(_('Enter username (leave blank to skip): ')) - - while True: - username = input(prompt).strip(' ') - if not username: - return None - if not self._check_for_correct_username(username): - error_prompt = str(_("The username you entered is invalid. Try again")) - print(error_prompt) - else: - break - - password = get_password(prompt=str(_('Password for user "{}": ').format(username))) - - if not password: - return None - - choice = Menu( - str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), - skip=False, - default_option=Menu.yes(), - clear_screen=False, - show_search_hint=False - ).run() - - sudo = True if choice.value == Menu.yes() else False - return User(username, password, sudo) - - -def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]: - users = UserList(prompt, defined_users).run() - return users diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py deleted file mode 100644 index b682c1d2..00000000 --- a/archinstall/lib/user_interaction/network_conf.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import ipaddress -import logging -from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict - -from ..menu import MenuSelectionType, TextInput -from ..models.network_configuration import NetworkConfiguration, NicType - -from ..networking import list_interfaces -from ..output import log, FormattedOutput -from ..menu import ListManager, Menu - -if TYPE_CHECKING: - _: Any - - -class ManualNetworkConfig(ListManager): - """ - subclass of ListManager for the managing of network configurations - """ - - def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): - self._actions = [ - str(_('Add interface')), - str(_('Edit interface')), - str(_('Delete interface')) - ] - - super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, iface in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = iface - - return display_data - - def selected_action_display(self, iface: NetworkConfiguration) -> str: - return iface.iface if iface.iface else '' - - def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): - if action == self._actions[0]: # add - iface_name = self._select_iface(data) - if iface_name: - iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - iface = self._edit_iface(iface) - data += [iface] - elif entry: - if action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: - all_ifaces = list_interfaces().values() - existing_ifaces = [d.iface for d in data] - available = set(all_ifaces) - set(existing_ifaces) - choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - return choice.value - - def _edit_iface(self, edit_iface: NetworkConfiguration): - iface_name = edit_iface.iface - modes = ['DHCP (auto detect)', 'IP (static)'] - default_mode = 'DHCP (auto detect)' - - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() - - if mode.value == 'IP (static)': - while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) - ip = TextInput(prompt, edit_iface.ip).run().strip() - # Implemented new check for correct IP/subnet input - try: - ipaddress.ip_interface(ip) - break - except ValueError: - log("You need to enter a valid IP in IP-config mode.", level=logging.WARNING, fg='red') - - # Implemented new check for correct gateway IP address - gateway = None - - while 1: - gateway = TextInput( - _('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway - ).run().strip() - try: - if len(gateway) > 0: - ipaddress.ip_address(gateway) - break - except ValueError: - log("You need to enter a valid gateway (router) IP address.", level=logging.WARNING, fg='red') - - if edit_iface.dns: - display_dns = ' '.join(edit_iface.dns) - else: - display_dns = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() - - dns = [] - if len(dns_input): - dns = dns_input.split(' ') - - return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) - else: - # this will contain network iface names - return NetworkConfiguration(NicType.MANUAL, iface=iface_name) - - -def ask_to_configure_network( - preset: Union[NetworkConfiguration, List[NetworkConfiguration]] -) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: - """ - Configure the network on the newly installed system - """ - network_options = { - 'none': str(_('No network configuration')), - 'iso_config': str(_('Copy ISO network configuration to installation')), - 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), - 'manual': str(_('Manual configuration')) - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - - if preset and not isinstance(preset, list): - if preset.type == 'iso_config': - cursor_idx = 0 - elif preset.type == 'network_manager': - cursor_idx = 1 - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select one network interface to configure'), - list(network_options.values()), - cursor_index=cursor_idx, - sort=False, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - - if choice.value == network_options['none']: - return None - elif choice.value == network_options['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif choice.value == network_options['network_manager']: - return NetworkConfiguration(NicType.NM) - elif choice.value == network_options['manual']: - preset_ifaces = preset if isinstance(preset, list) else [] - return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() - - return preset diff --git a/archinstall/lib/user_interaction/save_conf.py b/archinstall/lib/user_interaction/save_conf.py deleted file mode 100644 index e05b9afe..00000000 --- a/archinstall/lib/user_interaction/save_conf.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -import logging - -from pathlib import Path -from typing import Any, Dict, TYPE_CHECKING - -from ..general import SysCommand -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..output import log -from ..configuration import ConfigurationOutput - -if TYPE_CHECKING: - _: Any - - -def save_config(config: Dict): - def preview(selection: str): - if options['user_config'] == selection: - serialized = config_output.user_config_to_json() - return f'{config_output.user_configuration_file}\n{serialized}' - elif options['user_creds'] == selection: - if maybe_serial := config_output.user_credentials_to_json(): - return f'{config_output.user_credentials_file}\n{maybe_serial}' - else: - return str(_('No configuration')) - elif options['all'] == selection: - output = f'{config_output.user_configuration_file}\n' - if config_output.user_credentials_to_json(): - output += f'{config_output.user_credentials_file}\n' - return output[:-1] - return None - - config_output = ConfigurationOutput(config) - - options = { - 'user_config': str(_('Save user configuration')), - 'user_creds': str(_('Save user credentials')), - 'disk_layout': str(_('Save disk layout')), - 'all': str(_('Save all')) - } - - choice = Menu( - _('Choose which configuration to save'), - list(options.values()), - sort=False, - skip=True, - preview_size=0.75, - preview_command=preview - ).run() - - if choice.type_ == MenuSelectionType.Skip: - return - - save_config_value = choice.single_value - saving_key = [k for k, v in options.items() if v == save_config_value][0] - - dirs_to_exclude = [ - '/bin', - '/dev', - '/lib', - '/lib64', - '/lost+found', - '/opt', - '/proc', - '/run', - '/sbin', - '/srv', - '/sys', - '/usr', - '/var', - ] - - log('Ignore configuration option folders: ' + ','.join(dirs_to_exclude), level=logging.DEBUG) - log(_('Finding possible directories to save configuration files ...'), level=logging.INFO) - - find_exclude = '-path ' + ' -prune -o -path '.join(dirs_to_exclude) + ' -prune ' - file_picker_command = f'find / {find_exclude} -o -type d -print0' - - directories = SysCommand(file_picker_command).decode() - - if directories is None: - raise ValueError('Failed to retrieve possible configuration directories') - - possible_save_dirs = list(filter(None, directories.split('\x00'))) - - selection = Menu( - _('Select directory (or directories) for saving configuration files'), - possible_save_dirs, - multi=True, - skip=True, - allow_reset=False, - ).run() - - match selection.type_: - case MenuSelectionType.Skip: - return - - save_dirs = selection.multi_value - - log(f'Saving {saving_key} configuration files to {save_dirs}', level=logging.DEBUG) - - if save_dirs is not None: - for save_dir_str in save_dirs: - save_dir = Path(save_dir_str) - if options['user_config'] == save_config_value: - config_output.save_user_config(save_dir) - elif options['user_creds'] == save_config_value: - config_output.save_user_creds(save_dir) - elif options['all'] == save_config_value: - config_output.save_user_config(save_dir) - config_output.save_user_creds(save_dir) diff --git a/archinstall/lib/user_interaction/system_conf.py b/archinstall/lib/user_interaction/system_conf.py deleted file mode 100644 index 3f57d0e7..00000000 --- a/archinstall/lib/user_interaction/system_conf.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations - -from typing import List, Any, Dict, TYPE_CHECKING, Optional - -from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics -from ..menu import MenuSelectionType, Menu -from ..models.bootloader import Bootloader - -if TYPE_CHECKING: - _: Any - - -def select_kernel(preset: List[str] = []) -> List[str]: - """ - Asks the user to select a kernel for system. - - :return: The string as a selected kernel - :rtype: string - """ - - kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] - default_kernel = "linux" - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), - kernels, - sort=True, - multi=True, - preset_values=preset, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value # type: ignore - - -def ask_for_bootloader(preset: Bootloader) -> Bootloader: - # when the system only supports grub - if not has_uefi(): - options = [Bootloader.Grub.value] - default = Bootloader.Grub.value - else: - options = Bootloader.values() - default = Bootloader.Systemd.value - - preset_value = preset.value if preset else None - - choice = Menu( - _('Choose a bootloader'), - options, - preset_values=preset_value, - sort=False, - default_option=default - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return Bootloader(choice.value) - - return preset - - -def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: - """ - Some what convoluted function, whose job is simple. - Select a graphics driver from a pre-defined set of popular options. - - (The template xorg is for beginner users, not advanced, and should - there for appeal to the general public first and edge cases later) - """ - - if not options: - options = AVAILABLE_GFX_DRIVERS - - drivers = sorted(list(options.keys())) - - if drivers: - title = '' - if has_amd_graphics(): - title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' - if has_intel_graphics(): - title += str(_('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')) - if has_nvidia_graphics(): - title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - - title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - - preset = current_value if current_value else None - choice = Menu(title, drivers, preset_values=preset).run() - - if choice.type_ != MenuSelectionType.Selection: - return None - - return choice.value # type: ignore - - return current_value - - -def ask_for_swap(preset: bool = True) -> bool: - if preset: - preset_val = Menu.yes() - else: - preset_val = Menu.no() - - prompt = _('Would you like to use swap on zram?') - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True - - return preset diff --git a/archinstall/lib/user_interaction/utils.py b/archinstall/lib/user_interaction/utils.py deleted file mode 100644 index 918945c0..00000000 --- a/archinstall/lib/user_interaction/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import getpass -from typing import Any, Optional, TYPE_CHECKING - -from ..models import PasswordStrength -from ..output import log - -if TYPE_CHECKING: - _: Any - -# used for signal handler -SIG_TRIGGER = None - - -def get_password(prompt: str = '') -> Optional[str]: - if not prompt: - prompt = _("Enter a password: ") - - while password := getpass.getpass(prompt): - if len(password.strip()) <= 0: - break - - strength = PasswordStrength.strength(password) - log(f'Password strength: {strength.value}', fg=strength.color()) - - passwd_verification = getpass.getpass(prompt=_('And one more time for verification: ')) - if password != passwd_verification: - log(' * Passwords did not match * ', fg='red') - continue - - return password - - return None diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index ded480ae..34716f4a 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Any, TYPE_CHECKING, Optional -from ..output import log +from ..output import info if TYPE_CHECKING: _: Any @@ -16,7 +16,7 @@ def prompt_dir(text: str, header: Optional[str] = None) -> Path: dest_path = Path(path) if dest_path.exists() and dest_path.is_dir(): return dest_path - log(_('Not a valid directory: {}').format(dest_path), fg='red') + info(_('Not a valid directory: {}').format(dest_path)) def is_subpath(first: Path, second: Path): diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 9906e0a9..37cc1cad 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,9 +1,10 @@ -import logging import os from pathlib import Path from typing import Any, TYPE_CHECKING import archinstall +from archinstall import info, debug +from archinstall import SysInfo from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -13,7 +14,7 @@ from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfigurationHandler -from archinstall.lib.output import log +from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler if TYPE_CHECKING: @@ -24,20 +25,6 @@ if archinstall.arguments.get('help'): print("See `man archinstall` for help.") exit(0) -if os.getuid() != 0: - print(_("Archinstall requires root privileges to run. See --help for more.")) - exit(1) - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) - def ask_user_questions(): """ @@ -121,7 +108,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - log('Starting installation', level=logging.INFO) + info('Starting installation') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately @@ -167,7 +154,7 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -190,13 +177,13 @@ def perform_installation(mountpoint: Path): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) @@ -231,7 +218,7 @@ def perform_installation(mountpoint: Path): installation.genfstab() - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) @@ -242,12 +229,12 @@ def perform_installation(mountpoint: Path): except: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: +if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.") exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index 0cdbdcef..704759fc 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -2,23 +2,25 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, List import archinstall -from archinstall import ConfigurationOutput, Installer, ProfileConfiguration, profile_handler +from archinstall import info +from archinstall import Installer, ConfigurationOutput from archinstall.default_profiles.minimal import MinimalProfile -from archinstall import disk -from archinstall import models -from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout +from archinstall.lib.interactions import suggest_single_disk_layout, select_devices +from archinstall.lib.models import Bootloader, User +from archinstall.lib.profile import ProfileConfiguration, profile_handler +from archinstall.lib import disk if TYPE_CHECKING: _: Any -archinstall.log("Minimal only supports:") -archinstall.log(" * Being installed to a single disk") +info("Minimal only supports:") +info(" * Being installed to a single disk") if archinstall.arguments.get('help', None): - archinstall.log(" - Optional disk encryption via --!encryption-password=") - archinstall.log(" - Optional filesystem type via --filesystem=") - archinstall.log(" - Optional systemd network via --network") + info(" - Optional disk encryption via --!encryption-password=") + info(" - Optional filesystem type via --filesystem=") + info(" - Optional systemd network via --network") def perform_installation(mountpoint: Path): @@ -35,7 +37,7 @@ def perform_installation(mountpoint: Path): # some other minor details as specified by this profile and user. if installation.minimal_installation(): installation.set_hostname('minimal-arch') - installation.add_bootloader(models.Bootloader.Systemd) + installation.add_bootloader(Bootloader.Systemd) # Optionally enable networking: if archinstall.arguments.get('network', None): @@ -46,14 +48,14 @@ def perform_installation(mountpoint: Path): profile_config = ProfileConfiguration(MinimalProfile()) profile_handler.install_profile_config(installation, profile_config) - user = models.User('devel', 'devel', False) + user = User('devel', 'devel', False) installation.create_users(user) # Once this is done, we output some useful information to the user # And the installation is complete. - archinstall.log("There are two new accounts in your installation after reboot:") - archinstall.log(" * root (password: airoot)") - archinstall.log(" * devel (password: devel)") + info("There are two new accounts in your installation after reboot:") + info(" * root (password: airoot)") + info(" * devel (password: devel)") def prompt_disk_layout(): diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index a903c5fe..d0ee1e39 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -1,22 +1,18 @@ -import logging import os from pathlib import Path import archinstall -from archinstall import Installer +from archinstall import info, debug +from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput -from archinstall import disk +from archinstall.lib import disk +from archinstall.lib.networking import check_mirror_reachable if archinstall.arguments.get('help'): print("See `man archinstall` for help.") exit(0) -if os.getuid() != 0: - print("Archinstall requires root privileges to run. See --help for more.") - exit(1) - - def ask_user_questions(): global_menu = archinstall.GlobalMenu(data_store=archinstall.arguments) @@ -59,23 +55,12 @@ def perform_installation(mountpoint: Path): target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) - - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if archinstall.arguments.get('skip-mirror-check', False) is False and archinstall.check_mirror_reachable() is False: +if archinstall.arguments.get('skip-mirror-check', False) is False and check_mirror_reachable() is False: log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 3bf847b1..a49f568d 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,18 +1,18 @@ -import logging import os from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, Dict import archinstall +from archinstall import SysInfo, info, debug from archinstall.lib.mirrors import use_mirrors -from archinstall import models -from archinstall import disk +from archinstall.lib import models +from archinstall.lib import disk +from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler -from archinstall import menu +from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu -from archinstall.lib.output import log -from archinstall import Installer +from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput from archinstall.default_profiles.applications.pipewire import PipewireProfile @@ -25,11 +25,6 @@ if archinstall.arguments.get('help'): exit(0) -if os.getuid() != 0: - print("Archinstall requires root privileges to run. See --help for more.") - exit(1) - - class ExecutionMode(Enum): Full = 'full' Lineal = 'lineal' @@ -76,7 +71,7 @@ class SetupMenu(GlobalMenu): def exit_callback(self): if self._data_store.get('mode', None): archinstall.arguments['mode'] = self._data_store['mode'] - log(f"Archinstall will execute under {archinstall.arguments['mode']} mode") + info(f"Archinstall will execute under {archinstall.arguments['mode']} mode") class SwissMainMenu(GlobalMenu): @@ -124,7 +119,7 @@ class SwissMainMenu(GlobalMenu): case ExecutionMode.Minimal: pass case _: - archinstall.log(f' Execution mode {self._execution_mode} not supported') + info(f' Execution mode {self._execution_mode} not supported') exit(1) if self._execution_mode != ExecutionMode.Lineal: @@ -219,7 +214,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -242,13 +237,13 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed.") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) @@ -283,9 +278,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.genfstab() - installation.log( - "For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", - fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str( @@ -297,23 +290,12 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): except: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) - - -# Log various information about hardware before starting the installation. This might assist in troubleshooting -archinstall.log(f"Hardware model detected: {archinstall.sys_vendor()} {archinstall.product_name()}; UEFI mode: {archinstall.has_uefi()}", level=logging.DEBUG) -archinstall.log(f"Processor model detected: {archinstall.cpu_model()}", level=logging.DEBUG) -archinstall.log(f"Memory statistics: {archinstall.mem_available()} available out of {archinstall.mem_total()} total installed", level=logging.DEBUG) -archinstall.log(f"Virtualization detected: {archinstall.virtualization()}; is VM: {archinstall.is_vm()}", level=logging.DEBUG) -archinstall.log(f"Graphics devices detected: {archinstall.graphics_devices().keys()}", level=logging.DEBUG) - -# For support reasons, we'll log the disk layout pre installation to match against post-installation layout -archinstall.log(f"Disk states before installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") -if not archinstall.check_mirror_reachable(): +if not check_mirror_reachable(): log_file = os.path.join(archinstall.storage.get('LOG_PATH', None), archinstall.storage.get('LOG_FILE', None)) - archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") + info(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'") exit(1) param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() @@ -321,7 +303,7 @@ param_mode = archinstall.arguments.get('mode', ExecutionMode.Full.value).lower() try: mode = ExecutionMode(param_mode) except KeyError: - log(f'Mode "{param_mode}" is not supported') + info(f'Mode "{param_mode}" is not supported') exit(1) if not archinstall.arguments.get('silent'): diff --git a/archinstall/scripts/unattended.py b/archinstall/scripts/unattended.py index 0a1c5160..5ae4ae3d 100644 --- a/archinstall/scripts/unattended.py +++ b/archinstall/scripts/unattended.py @@ -1,13 +1,14 @@ import time import archinstall -from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import info +from archinstall import profile -for profile in profile_handler.get_mac_addr_profiles(): +for p in profile.profile_handler.get_mac_addr_profiles(): # Tailored means it's a match for this machine # based on it's MAC address (or some other criteria # that fits the requirements for this machine specifically). - archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + info(f'Found a tailored profile for this machine called: "{p.name}"') print('Starting install in:') for i in range(10, 0, -1): @@ -15,4 +16,4 @@ for profile in profile_handler.get_mac_addr_profiles(): time.sleep(1) install_session = archinstall.storage['installation_session'] - profile.install(install_session) + p.install(install_session) diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index a169dd50..dcef731a 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -1,9 +1,10 @@ from pathlib import Path -from archinstall import Installer, ProfileConfiguration, profile_handler +from archinstall import Installer +from archinstall import profile from archinstall.default_profiles.minimal import MinimalProfile from archinstall import disk -from archinstall.lib.models import User +from archinstall import models # we're creating a new ext4 filesystem installation fs_type = disk.FilesystemType('ext4') @@ -88,8 +89,8 @@ with Installer( # Optionally, install a profile of choice. # In this case, we install a minimal profile that is empty -profile_config = ProfileConfiguration(MinimalProfile()) -profile_handler.install_profile_config(installation, profile_config) +profile_config = profile.ProfileConfiguration(MinimalProfile()) +profile.profile_handler.install_profile_config(installation, profile_config) -user = User('archinstall', 'password', True) +user = models.User('archinstall', 'password', True) installation.create_users(user) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index a27ec0f9..72595048 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,13 +1,16 @@ -import logging from pathlib import Path from typing import TYPE_CHECKING, Any import archinstall -from archinstall import log, Installer, use_mirrors, profile_handler +from archinstall import Installer +from archinstall import profile +from archinstall import SysInfo +from archinstall import mirrors from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu -from archinstall.lib.models import Bootloader, NetworkConfigurationHandler +from archinstall import models +from archinstall import info, debug if TYPE_CHECKING: _: Any @@ -84,7 +87,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - log('Starting installation', level=logging.INFO) + info('Starting installation') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately @@ -114,7 +117,7 @@ def perform_installation(mountpoint: Path): # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): - use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + mirrors.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium installation.minimal_installation( testing=enable_testing, @@ -130,7 +133,7 @@ def perform_installation(mountpoint: Path): if archinstall.arguments.get('swap'): installation.setup_swap('zram') - if archinstall.arguments.get("bootloader") == Bootloader.Grub and archinstall.has_uefi(): + if archinstall.arguments.get("bootloader") == models.Bootloader.Grub and SysInfo.has_uefi(): installation.add_additional_packages("grub") installation.add_bootloader(archinstall.arguments["bootloader"]) @@ -140,7 +143,7 @@ def perform_installation(mountpoint: Path): network_config = archinstall.arguments.get('nic', None) if network_config: - handler = NetworkConfigurationHandler(network_config) + handler = models.NetworkConfigurationHandler(network_config) handler.config_installer( installation, archinstall.arguments.get('profile_config', None) @@ -153,16 +156,16 @@ def perform_installation(mountpoint: Path): installation.create_users(users) if audio := archinstall.arguments.get('audio', None): - log(f'Installing audio server: {audio}', level=logging.INFO) + info(f'Installing audio server: {audio}') if audio == 'pipewire': PipewireProfile().install(installation) elif audio == 'pulseaudio': installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=logging.INFO) + info("No audio server will be installed.") if profile_config := archinstall.arguments.get('profile_config', None): - profile_handler.install_profile_config(installation, profile_config) + profile.profile_handler.install_profile_config(installation, profile_config) if timezone := archinstall.arguments.get('timezone', None): installation.set_timezone(timezone) @@ -194,7 +197,7 @@ def perform_installation(mountpoint: Path): installation.genfstab() - installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + info("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation") if not archinstall.arguments.get('silent'): prompt = str(_('Would you like to chroot into the newly created installation and perform post-installation configuration?')) @@ -202,10 +205,10 @@ def perform_installation(mountpoint: Path): if choice.value == menu.Menu.yes(): try: installation.drop_to_shell() - except: + except Exception: pass - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") ask_user_questions() diff --git a/examples/mac_address_installation.py b/examples/mac_address_installation.py index 0a1c5160..74a123c7 100644 --- a/examples/mac_address_installation.py +++ b/examples/mac_address_installation.py @@ -1,13 +1,13 @@ import time import archinstall -from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall import profile, info -for profile in profile_handler.get_mac_addr_profiles(): +for _profile in profile.profile_handler.get_mac_addr_profiles(): # Tailored means it's a match for this machine # based on it's MAC address (or some other criteria # that fits the requirements for this machine specifically). - archinstall.log(f'Found a tailored profile for this machine called: "{profile.name}"') + info(f'Found a tailored profile for this machine called: "{_profile.name}"') print('Starting install in:') for i in range(10, 0, -1): @@ -15,4 +15,4 @@ for profile in profile_handler.get_mac_addr_profiles(): time.sleep(1) install_session = archinstall.storage['installation_session'] - profile.install(install_session) + _profile.install(install_session) diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py index 8bd6fd55..e31adea4 100644 --- a/examples/minimal_installation.py +++ b/examples/minimal_installation.py @@ -2,11 +2,12 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, List import archinstall -from archinstall.lib import disk -from archinstall import Installer, ProfileConfiguration, profile_handler +from archinstall import disk +from archinstall import Installer +from archinstall import profile +from archinstall import models +from archinstall import interactions from archinstall.default_profiles.minimal import MinimalProfile -from archinstall.lib.models import Bootloader, User -from archinstall.lib.user_interaction.disk_conf import select_devices, suggest_single_disk_layout if TYPE_CHECKING: _: Any @@ -26,7 +27,7 @@ def perform_installation(mountpoint: Path): # some other minor details as specified by this profile and user. if installation.minimal_installation(): installation.set_hostname('minimal-arch') - installation.add_bootloader(Bootloader.Systemd) + installation.add_bootloader(models.Bootloader.Systemd) # Optionally enable networking: if archinstall.arguments.get('network', None): @@ -34,10 +35,10 @@ def perform_installation(mountpoint: Path): installation.add_additional_packages(['nano', 'wget', 'git']) - profile_config = ProfileConfiguration(MinimalProfile()) - profile_handler.install_profile_config(installation, profile_config) + profile_config = profile.ProfileConfiguration(MinimalProfile()) + profile.profile_handler.install_profile_config(installation, profile_config) - user = User('devel', 'devel', False) + user = models.User('devel', 'devel', False) installation.create_users(user) @@ -46,8 +47,8 @@ def prompt_disk_layout(): if filesystem := archinstall.arguments.get('filesystem', None): fs_type = disk.FilesystemType(filesystem) - devices = select_devices() - modifications = suggest_single_disk_layout(devices[0], filesystem_type=fs_type) + devices = interactions.select_devices() + modifications = interactions.suggest_single_disk_layout(devices[0], filesystem_type=fs_type) archinstall.arguments['disk_config'] = disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Default, diff --git a/examples/only_hd_installation.py b/examples/only_hd_installation.py index 2fc74bf0..075bde20 100644 --- a/examples/only_hd_installation.py +++ b/examples/only_hd_installation.py @@ -1,9 +1,7 @@ -import logging from pathlib import Path import archinstall -from archinstall import Installer -from archinstall.lib import disk +from archinstall import Installer, disk, debug def ask_user_questions(): @@ -48,7 +46,7 @@ def perform_installation(mountpoint: Path): target.parent.mkdir(parents=True) # For support reasons, we'll log the disk layout post installation (crash or no crash) - archinstall.log(f"Disk states after installing: {disk.disk_layouts()}", level=logging.DEBUG) + debug(f"Disk states after installing: {disk.disk_layouts()}") ask_user_questions() diff --git a/pyproject.toml b/pyproject.toml index f837ebdf..8b6ae4c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ Source = "https://github.com/archlinux/archinstall" [project.optional-dependencies] dev = [ "mypy==1.1.1", + "pre-commit==3.3.1", ] doc = ["sphinx"] -- cgit v1.2.3-54-g00ecf From 16132e6fc9d54f237f260227f99dad5b639891db Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 21 Jun 2023 17:52:06 +1000 Subject: Fix 1862 (#1884) * Fix 1862 * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 41 +++++++-- archinstall/lib/disk/partitioning_menu.py | 117 ++++++++++++++---------- archinstall/lib/mirrors.py | 2 +- archinstall/lib/models/network_configuration.py | 2 +- archinstall/lib/output.py | 20 ++-- 5 files changed, 116 insertions(+), 66 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 36dd0c4f..8e72390c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -137,6 +137,10 @@ class Unit(Enum): Percent = '%' # size in percentile + @staticmethod + def get_all_units() -> List[str]: + return [u.name for u in Unit] + @dataclass class Size: @@ -214,16 +218,25 @@ class Size: value = int(self._normalize() / target_unit.value) # type: ignore return Size(value, target_unit) + def as_text(self) -> str: + return self.format_size( + self.unit, + self.sector_size + ) + def format_size( self, target_unit: Unit, - sector_size: Optional[Size] = None + sector_size: Optional[Size] = None, + include_unit: bool = True ) -> str: if self.unit == Unit.Percent: return f'{self.value}%' else: target_size = self.convert(target_unit, sector_size) - return f'{target_size.value} {target_unit.name}' + if include_unit: + return f'{target_size.value} {target_unit.name}' + return f'{target_size.value}' def _normalize(self) -> int: """ @@ -280,7 +293,7 @@ class _PartitionInfo: mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) - def as_json(self) -> Dict[str, Any]: + def table_data(self) -> Dict[str, Any]: part_info = { 'Name': self.name, 'Type': self.type.value, @@ -343,7 +356,7 @@ class _DeviceInfo: read_only: bool dirty: bool - def as_json(self) -> Dict[str, Any]: + def table_data(self) -> Dict[str, Any]: total_free_space = sum([region.get_length(unit=Unit.MiB) for region in self.free_space_regions]) return { 'Model': self.model, @@ -440,7 +453,7 @@ class SubvolumeModification: 'nodatacow': self.nodatacow } - def as_json(self) -> Dict[str, Any]: + def table_data(self) -> Dict[str, Any]: return { 'name': str(self.name), 'mountpoint': str(self.mountpoint), @@ -465,12 +478,20 @@ class DeviceGeometry: def get_length(self, unit: Unit = Unit.sectors) -> int: return self._geometry.getLength(unit.name) - def as_json(self) -> Dict[str, Any]: + def table_data(self) -> Dict[str, Any]: + start = Size(self._geometry.start, Unit.sectors, self._sector_size) + end = Size(self._geometry.end, Unit.sectors, self._sector_size) + length = Size(self._geometry.getLength(), Unit.sectors, self._sector_size) + + start_str = f'{self._geometry.start} / {start.format_size(Unit.B, include_unit=False)}' + end_str = f'{self._geometry.end} / {end.format_size(Unit.B, include_unit=False)}' + length_str = f'{self._geometry.getLength()} / {length.format_size(Unit.B, include_unit=False)}' + return { 'Sector size': self._sector_size.value, - 'Start sector': self._geometry.start, - 'End sector': self._geometry.end, - 'Length': self._geometry.getLength() + 'Start (sector/B)': start_str, + 'End (sector/B)': end_str, + 'Length (sectors/B)': length_str } @@ -700,7 +721,7 @@ class PartitionModification: 'btrfs': [vol.__dump__() for vol in self.btrfs_subvols] } - def as_json(self) -> Dict[str, Any]: + def table_data(self) -> Dict[str, Any]: """ Called for displaying data in table format """ diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 89cf6293..4acb4e85 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -1,10 +1,11 @@ from __future__ import annotations +import re from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus + ModificationStatus, DeviceGeometry from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -192,22 +193,51 @@ class PartitioningList(ListManager): choice = Menu(prompt, options, sort=False, skip=False).run() return options[choice.single_value] - def _validate_sector(self, start_sector: str, end_sector: Optional[str] = None) -> bool: - if not start_sector.isdigit(): - return False + def _validate_value( + self, + sector_size: Size, + total_size: Size, + value: str + ) -> Optional[Size]: + match = re.match(r'([0-9]+)([a-zA-Z|%]*)', value, re.I) + + if match: + value, unit = match.groups() + + if unit == '%': + unit = Unit.Percent.name + + if unit and unit not in Unit.get_all_units(): + return None + + unit = Unit[unit] if unit else Unit.sectors + return Size(int(value), unit, sector_size, total_size) + + return None + + def _enter_size( + self, + sector_size: Size, + total_size: Size, + prompt: str, + default: Size + ) -> Size: + while True: + value = TextInput(prompt).run().strip() + + size: Optional[Size] = None - if end_sector: - if end_sector.endswith('%'): - if not end_sector[:-1].isdigit(): - return False - elif not end_sector.isdigit(): - return False - elif int(start_sector) > int(end_sector): - return False + if not value: + size = default + else: + size = self._validate_value(sector_size, total_size, value) - return True + if size: + return size - def _prompt_sectors(self) -> Tuple[Size, Size]: + warn(f'Invalid value: {value}') + + def _prompt_size(self) -> Tuple[Size, Size]: device_info = self._device.device_info text = str(_('Current free sectors on device {}:')).format(device_info.path) + '\n\n' @@ -215,54 +245,45 @@ class PartitioningList(ListManager): prompt = text + free_space_table + '\n' total_sectors = device_info.total_size.format_size(Unit.sectors, device_info.sector_size) - prompt += str(_('Total sectors: {}')).format(total_sectors) + '\n' + total_bytes = device_info.total_size.format_size(Unit.B) + + prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n' + prompt += str(_('All entered values can be suffixed with a unit: B, KB, KiB, MB, MiB...')) + '\n' + prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n' print(prompt) - largest_free_area = max(device_info.free_space_regions, key=lambda r: r.get_length()) + largest_free_area: DeviceGeometry = max(device_info.free_space_regions, key=lambda r: r.get_length()) # prompt until a valid start sector was entered - while True: - start_prompt = str(_('Enter the start sector (default: {}): ')).format(largest_free_area.start) - start_sector = TextInput(start_prompt).run().strip() - - if not start_sector or self._validate_sector(start_sector): - break - - warn(f'Invalid start sector entered: {start_sector}') + default_start = Size(largest_free_area.start, Unit.sectors, device_info.sector_size) + start_prompt = str(_('Enter start (default: sector {}): ')).format(largest_free_area.start) + start_size = self._enter_size( + device_info.sector_size, + device_info.total_size, + start_prompt, + default_start + ) - if not start_sector: - start_sector = str(largest_free_area.start) - end_sector = str(largest_free_area.end) + if start_size.value == largest_free_area.start: + end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size) else: - end_sector = '100%' + end_size = Size(100, Unit.Percent, total_size=device_info.total_size) # prompt until valid end sector was entered - while True: - end_prompt = str(_('Enter the end sector of the partition (percentage or block number, default: {}): ')).format(end_sector) - end_value = TextInput(end_prompt).run().strip() - - if not end_value or self._validate_sector(start_sector, end_value): - break - - warn(f'Invalid end sector entered: {start_sector}') - - # override the default value with the user value - if end_value: - end_sector = end_value - - start_size = Size(int(start_sector), Unit.sectors, device_info.sector_size) - - if end_sector.endswith('%'): - end_size = Size(int(end_sector[:-1]), Unit.Percent, device_info.sector_size, device_info.total_size) - else: - end_size = Size(int(end_sector), Unit.sectors, device_info.sector_size) + end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text()) + end_size = self._enter_size( + device_info.sector_size, + device_info.total_size, + end_prompt, + end_size + ) return start_size, end_size def _create_new_partition(self) -> PartitionModification: fs_type = self._prompt_partition_fs_type() - start_size, end_size = self._prompt_sectors() + start_size, end_size = self._prompt_size() length = end_size - start_size # new line for the next prompt diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 521a8e5b..74cdd0aa 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -30,7 +30,7 @@ class CustomMirror: sign_check: SignCheck sign_option: SignOption - def as_json(self) -> Dict[str, str]: + def table_data(self) -> Dict[str, str]: return { 'Name': self.name, 'Url': self.url, diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index 93dd1c44..e564b97b 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -39,7 +39,7 @@ class NetworkConfiguration: else: return 'Unknown type' - def as_json(self) -> Dict: + def table_data(self) -> Dict[str, Any]: exclude_fields = ['type'] data = {} for k, v in self.__dict__.items(): diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index d266afa8..d1c95ec5 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -11,14 +11,16 @@ from .storage import storage class FormattedOutput: + @classmethod - def values( + def _get_values( cls, o: Any, class_formatter: Optional[Union[str, Callable]] = None, filter_list: List[str] = [] ) -> Dict[str, Any]: - """ the original values returned a dataclass as dict thru the call to some specific methods + """ + the original values returned a dataclass as dict thru the call to some specific methods this version allows thru the parameter class_formatter to call a dynamicly selected formatting method. Can transmit a filter list to the class_formatter, """ @@ -33,8 +35,8 @@ class FormattedOutput: return func(filter_list) raise ValueError('Unsupported formatting call') - elif hasattr(o, 'as_json'): - return o.as_json() + elif hasattr(o, 'table_data'): + return o.table_data() elif hasattr(o, 'json'): return o.json() elif is_dataclass(o): @@ -58,7 +60,7 @@ class FormattedOutput: is for compatibility with a print statement As_table_filter can be a drop in replacement for as_table """ - raw_data = [cls.values(o, class_formatter, filter_list) for o in obj] + raw_data = [cls._get_values(o, class_formatter, filter_list) for o in obj] # determine the maximum column size column_width: Dict[str, int] = {} @@ -92,18 +94,24 @@ class FormattedOutput: for key in filter_list: width = column_width.get(key, len(key)) value = record.get(key, '') + if '!' in key: value = '*' * width - if isinstance(value,(int, float)) or (isinstance(value, str) and value.isnumeric()): + + if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()): obj_data.append(str(value).rjust(width)) else: obj_data.append(str(value).ljust(width)) + output += ' | '.join(obj_data) + '\n' return output @classmethod def as_columns(cls, entries: List[str], cols: int) -> str: + """ + Will format a list into a given number of columns + """ chunks = [] output = '' -- cgit v1.2.3-54-g00ecf From 885f89c3a15ef9d5ed8711cdb270ed5aea189705 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 21 Jun 2023 17:54:42 +1000 Subject: Rename encryption method (#1888) * Rename encryption method * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 7 +++---- archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/scripts/minimal.py | 2 +- examples/full_automated_installation.py | 2 +- examples/minimal_installation.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8e72390c..35fbd40c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -777,13 +777,12 @@ class DeviceModification: class EncryptionType(Enum): NoEncryption = "no_encryption" - Partition = "partition" + Luks = "luks" @classmethod def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: return { - # str(_('Full disk encryption')): EncryptionType.FullDiskEncryption, - str(_('Partition encryption')): EncryptionType.Partition + 'Luks': EncryptionType.Luks } @classmethod @@ -800,7 +799,7 @@ class EncryptionType(Enum): @dataclass class DiskEncryption: - encryption_type: EncryptionType = EncryptionType.Partition + encryption_type: EncryptionType = EncryptionType.Luks encryption_password: str = '' partitions: List[PartitionModification] = field(default_factory=list) hsm_device: Optional[Fido2Device] = None diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 8c64e65e..89eade2b 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -110,7 +110,7 @@ class DiskEncryptionMenu(AbstractSubMenu): def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: title = str(_('Select disk encryption option')) options = [ - EncryptionType.type_to_text(EncryptionType.Partition) + EncryptionType.type_to_text(EncryptionType.Luks) ] preset_value = EncryptionType.type_to_text(preset) diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index 704759fc..f6650ff8 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -82,7 +82,7 @@ def parse_disk_encryption(): partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, encryption_password=enc_password, partitions=partitions ) diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index dcef731a..79e85348 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -63,7 +63,7 @@ disk_config = disk.DiskLayoutConfiguration( # disk encryption configuration (Optional) disk_encryption = disk.DiskEncryption( encryption_password="enc_password", - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, partitions=[home_partition], hsm_device=None ) diff --git a/examples/minimal_installation.py b/examples/minimal_installation.py index e31adea4..c91a5d46 100644 --- a/examples/minimal_installation.py +++ b/examples/minimal_installation.py @@ -66,7 +66,7 @@ def parse_disk_encryption(): partitions += list(filter(lambda x: x.mountpoint != Path('/boot'), mod.partitions)) archinstall.arguments['disk_encryption'] = disk.DiskEncryption( - encryption_type=disk.EncryptionType.Partition, + encryption_type=disk.EncryptionType.Luks, encryption_password=enc_password, partitions=partitions ) -- cgit v1.2.3-54-g00ecf From 748f03cdb1fbd28becf2d455d7137b21934f84fe Mon Sep 17 00:00:00 2001 From: ArtikusHG <24320212+ArtikusHG@users.noreply.github.com> Date: Thu, 22 Jun 2023 20:49:08 +0000 Subject: Fix crash when libfido2 is not installed (#1893) Co-authored-by: ArtikusHG --- archinstall/lib/disk/fido.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 97c38d84..83b85d08 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -36,7 +36,11 @@ class Fido2: # to prevent continous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: - ret: Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + ret = "" + try: + ret = Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + except: + error('fido2 support is most likely not installed') if not ret: error('Unable to retrieve fido2 devices') return [] -- cgit v1.2.3-54-g00ecf From 27a474ed4a2ac8c2cc001f7bb969fee494636af4 Mon Sep 17 00:00:00 2001 From: Himadri Bhattacharjee <107522312+lavafroth@users.noreply.github.com> Date: Fri, 23 Jun 2023 07:46:30 +0000 Subject: fix: define ret as Optional[str] before reassignment in get_fido2_devices (#1894) --- archinstall/lib/disk/fido.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 83b85d08..d8c8fd3c 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -36,9 +36,9 @@ class Fido2: # to prevent continous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: - ret = "" + ret: Optional[str] = None try: - ret = Optional[str] = SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8') + ret = SysCommand("systemd-cryptenroll --fido2-device=list").decode('UTF-8') except: error('fido2 support is most likely not installed') if not ret: -- cgit v1.2.3-54-g00ecf From 57ebc42ffd64babb121c940caa3c5ff415062162 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 28 Jun 2023 21:34:54 +1000 Subject: Fix 1875 (#1880) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 41 +++++++++++++---------- archinstall/lib/disk/device_model.py | 19 ++++++++--- archinstall/lib/installer.py | 61 +++++++++++++++++----------------- pyproject.toml | 1 + 4 files changed, 69 insertions(+), 53 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 4341c53c..2c88e382 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -238,41 +238,46 @@ class DeviceHandler(object): info(f'luks2 locking device: {dev_path}') luks_handler.lock() + def _validate(self, device_mod: DeviceModification): + checks = { + # verify that all partitions have a path set (which implies that they have been created) + lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), + # crypto luks is not a valid file system type + lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError('Crypto luks cannot be set as a filesystem type'), + # file system type must be set + lambda x: x.fs_type is None: ValueError('File system type must be set for modification') + } + + for check, exc in checks.items(): + found = next(filter(check, device_mod.partitions), None) + if found is not None: + raise exc + def format( self, - modification: DeviceModification, + device_mod: DeviceModification, enc_conf: Optional['DiskEncryption'] = None ): """ Format can be given an overriding path, for instance /dev/null to test the formatting functionality and in essence the support for the given filesystem. """ - - # verify that all partitions have a path set (which implies that they have been created) - missing_path = next(filter(lambda x: x.dev_path is None, modification.partitions), None) - if missing_path is not None: - raise ValueError('When formatting, all partitions must have a path set') - - # crypto luks is not known to parted and can therefore not - # be used as a filesystem type in that sense; - invalid_fs_type = next(filter(lambda x: x.fs_type is FilesystemType.Crypto_luks, modification.partitions), None) - if invalid_fs_type is not None: - raise ValueError('Crypto luks cannot be set as a filesystem type') + self._validate(device_mod) # make sure all devices are unmounted - self._umount_all_existing(modification) + self._umount_all_existing(device_mod) - for part_mod in modification.partitions: + for part_mod in device_mod.partitions: # partition will be encrypted if enc_conf is not None and part_mod in enc_conf.partitions: self._perform_enc_formatting( part_mod.safe_dev_path, part_mod.mapper_name, - part_mod.fs_type, + part_mod.safe_fs_type, enc_conf ) else: - self._perform_formatting(part_mod.fs_type, part_mod.safe_dev_path) + self._perform_formatting(part_mod.safe_fs_type, part_mod.safe_dev_path) def _perform_partitioning( self, @@ -312,7 +317,7 @@ class DeviceHandler(object): length=length_sector.value ) - filesystem = FileSystem(type=part_mod.fs_type.value, geometry=geometry) + filesystem = FileSystem(type=part_mod.safe_fs_type.value, geometry=geometry) partition = Partition( disk=disk, @@ -325,7 +330,7 @@ class DeviceHandler(object): partition.setFlag(flag.value) debug(f'\tType: {part_mod.type.value}') - debug(f'\tFilesystem: {part_mod.fs_type.value}') + debug(f'\tFilesystem: {part_mod.safe_fs_type.value}') debug(f'\tGeometry: {start_sector.value} start sector, {length_sector.value} length') try: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 35fbd40c..97623772 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -283,7 +283,7 @@ class _PartitionInfo: partition: Partition name: str type: PartitionType - fs_type: FilesystemType + fs_type: Optional[FilesystemType] path: Path start: Size length: Size @@ -313,7 +313,7 @@ class _PartitionInfo: def from_partition( cls, partition: Partition, - fs_type: FilesystemType, + fs_type: Optional[FilesystemType], partuuid: str, mountpoints: List[Path], btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = [] @@ -594,7 +594,7 @@ class PartitionModification: type: PartitionType start: Size length: Size - fs_type: FilesystemType + fs_type: Optional[FilesystemType] mountpoint: Optional[Path] = None mount_options: List[str] = field(default_factory=list) flags: List[PartitionFlag] = field(default_factory=list) @@ -613,6 +613,9 @@ class PartitionModification: if self.is_exists_or_modify() and not self.dev_path: raise ValueError('If partition marked as existing a path must be set') + if self.fs_type is None and self.status == ModificationStatus.Modify: + raise ValueError('FS type must not be empty on modifications with status type modify') + def __hash__(self): return hash(self._obj_id) @@ -628,6 +631,12 @@ class PartitionModification: raise ValueError('Device path was not set') return self.dev_path + @property + def safe_fs_type(self) -> FilesystemType: + if self.fs_type is None: + raise ValueError('File system type is not set') + return self.fs_type + @classmethod def from_existing_partition(cls, partition_info: _PartitionInfo) -> PartitionModification: if partition_info.btrfs_subvol_infos: @@ -714,7 +723,7 @@ class PartitionModification: 'type': self.type.value, 'start': self.start.__dump__(), 'length': self.length.__dump__(), - 'fs_type': self.fs_type.value, + 'fs_type': self.fs_type.value if self.fs_type else '', 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, 'flags': [f.name for f in self.flags], @@ -731,7 +740,7 @@ class PartitionModification: 'Type': self.type.value, 'Start': self.start.format_size(Unit.MiB), 'Length': self.length.format_size(Unit.MiB), - 'FS type': self.fs_type.value, + 'FS type': self.fs_type.value if self.fs_type else 'Unknown', 'Mountpoint': self.mountpoint if self.mountpoint else '', 'Mount options': ', '.join(self.mount_options), 'Flags': ', '.join([f.name for f in self.flags]), diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 0680bf6e..f1a7f71a 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -667,28 +667,29 @@ class Installer: ): for mod in self._disk_config.device_modifications: for part in mod.partitions: - if (pkg := part.fs_type.installation_pkg) is not None: - self.base_packages.append(pkg) - if (module := part.fs_type.installation_module) is not None: - self.modules.append(module) - if (binary := part.fs_type.installation_binary) is not None: - self._binaries.append(binary) - - # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. - if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: - if 'fsck' in self._hooks: - self._hooks.remove('fsck') - - if part in self._disk_encryption.partitions: - if self._disk_encryption.hsm_device: - # Required bby mkinitcpio to add support for fido2-device options - self._pacstrap('libfido2') - - if 'sd-encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') - else: - if 'encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') + if part.fs_type is not None: + if (pkg := part.fs_type.installation_pkg) is not None: + self.base_packages.append(pkg) + if (module := part.fs_type.installation_module) is not None: + self.modules.append(module) + if (binary := part.fs_type.installation_binary) is not None: + self._binaries.append(binary) + + # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. + if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if 'fsck' in self._hooks: + self._hooks.remove('fsck') + + if part in self._disk_encryption.partitions: + if self._disk_encryption.hsm_device: + # Required bby mkinitcpio to add support for fido2-device options + self._pacstrap('libfido2') + + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') + else: + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') if not SysInfo.has_uefi(): self.base_packages.append('grub') @@ -857,7 +858,7 @@ class Installer: # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. - options_entry = f'rw rootfstype={root_partition.fs_type.fs_type_mount} {" ".join(self._kernel_params)}\n' + options_entry = f'rw rootfstype={root_partition.safe_fs_type.fs_type_mount} {" ".join(self._kernel_params)}\n' for sub_vol in root_partition.btrfs_subvols: if sub_vol.is_root(): @@ -868,7 +869,7 @@ class Installer: if self._zram_enabled: options_entry = "zswap.enabled=0 " + options_entry - if root_partition.fs_type.is_crypto(): + if root_partition.safe_fs_type.is_crypto(): # 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) debug('Root partition is an encrypted device, identifying by PARTUUID: {root_partition.partuuid}') @@ -900,15 +901,15 @@ class Installer: _file = "/etc/default/grub" - if root_partition.fs_type.is_crypto(): + if root_partition.safe_fs_type.is_crypto(): debug(f"Using UUID {root_partition.uuid} as encrypted root identifier") - cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_partition.uuid}:cryptlvm rootfstype={root_partition.fs_type.value}\"/'" + cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_partition.uuid}:cryptlvm rootfstype={root_partition.safe_fs_type.value}\"/'" enable_cryptdisk = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'" SysCommand(f"/usr/bin/arch-chroot {self.target} {enable_cryptdisk} {_file}") else: - cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"rootfstype={root_partition.fs_type.value}\"/'" + cmd_line_linux = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"rootfstype={root_partition.safe_fs_type.value}\"/'" SysCommand(f"/usr/bin/arch-chroot {self.target} {cmd_line_linux} {_file}") @@ -984,14 +985,14 @@ class Installer: # blkid doesn't trigger on loopback devices really well, # so we'll use the old manual method until we get that sorted out. - if root_partition.fs_type.is_crypto(): + if root_partition.safe_fs_type.is_crypto(): # 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) debug(f'Identifying root partition by PARTUUID: {root_partition.partuuid}') - kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') + kernel_parameters.append(f'cryptdevice=PARTUUID={root_partition.partuuid}:luksdev root=/dev/mapper/luksdev rw rootfstype={root_partition.safe_fs_type.value} {" ".join(self._kernel_params)}') else: debug(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}') - kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.fs_type.value} {" ".join(self._kernel_params)}') + kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.safe_fs_type.value} {" ".join(self._kernel_params)}') device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) diff --git a/pyproject.toml b/pyproject.toml index 8b6ae4c7..f67f1eca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ packages = ["archinstall"] python_version = "3.10" files = "archinstall/" exclude = "tests" +#check_untyped_defs=true [tool.bandit] targets = ["archinstall"] -- cgit v1.2.3-54-g00ecf From ffb9366280803578bd47f2d7102a5772fc44caab Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:33:14 -0400 Subject: Skip rom devices (#1906) --- archinstall/lib/disk/device_handler.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 2c88e382..9acf0999 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -45,6 +45,9 @@ class DeviceHandler(object): block_devices = {} for device in getAllDevices(): + if get_lsblk_info(device.path).type == 'rom': + continue + try: disk = Disk(device) except DiskLabelException as err: -- cgit v1.2.3-54-g00ecf From afaf42e6469206e181f6373c1ded209217539e71 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Mon, 17 Jul 2023 00:14:44 +0200 Subject: Enable separate /boot and /boot/esp via XBOOTLDR in systemd-boot (#1859) * Disabled /boot check for now * Making '/boot' more dynamic * str() on boot_partition didn't work * _pacstrap -> pacman.strap() * Added 'finding' the EFI partition logic * f-string qotations * Locked down so get_boot_partition() looks for /boot and get_efi_partition() looks for /boot/efi - essentially hardcoding it for now, as there's no easy way to distinguish between the EFI partition or BOOT partition if they are both FAT32 for some reason. * Added some debugging output * Fixed some mypy complaints * Fixed PosixPath() vs str comparison * Changed FAT32 comparitor, should be FilesystemType.Fat32 now * Fixed PosixPath() vs str comparison * Re-ordered _add_systemd_bootloader() argument order, to match the other functions. This will cause the function to break on scripts that call this explicitly. * is_boot() now returns True if any type of valid boot flags are set, not just the 'Boot' flag. This allows us to check for XBOOTLDR flag as well. * Converted static INT to _ped.PARTITION_ definition. This matches the way pyparted checks for flags on partitions. * /boot/efi -> /boot/EFI (while the recommendation from bootctl is to mount it to /efi, I want to test it with custom paths first) * Removed _ped from mypy checks * flake8 fix * Added ESP flag to partitions * Added more docs in the docstring * Renamed *efi_partition to *xbootldr_partition within this PR changes * Naming collision, PartitionType -> PartitionGUIDs to avoid overwriting existing PartitionType * Check for XBOOTLDR instead of fixed EFI mountpoint in get_xbootldr_partition() * Mixed up XBOOTLDR and EFI partitions a bit, brought back get_efi_partition() which now filters out XBOOTLDR partitions and only returns a partition when there is a boot partition found by get_boot_partition() * Fixed symbiosis between get_boot() and get_efi() so that they don't report the same potential partition * Removed debugging code * Improved comments surrounding why /loader/ rather than /loader/ - this may change --- archinstall/lib/disk/device_model.py | 45 ++++++++++++++++++++++++++++++++---- archinstall/lib/installer.py | 43 +++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 15 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 97623772..ad3426b6 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -13,6 +13,7 @@ from typing import Optional, List, Dict, TYPE_CHECKING, Any from typing import Union import parted # type: ignore +import _ped # type: ignore from parted import Disk, Geometry, Partition from ..exceptions import DiskError, SysCallError @@ -525,7 +526,21 @@ class PartitionType(Enum): class PartitionFlag(Enum): - Boot = 1 + """ + Flags are taken from _ped because pyparted uses this to look + up their flag definitions: https://github.com/dcantrell/pyparted/blob/c4e0186dad45c8efbe67c52b02c8c4319df8aa9b/src/parted/__init__.py#L200-L202 + Which is the way libparted checks for its flags: https://git.savannah.gnu.org/gitweb/?p=parted.git;a=blob;f=libparted/labels/gpt.c;hb=4a0e468ed63fff85a1f9b923189f20945b32f4f1#l183 + """ + Boot = _ped.PARTITION_BOOT + XBOOTLDR = _ped.PARTITION_BLS_BOOT # Note: parted calls this bls_boot + ESP = _ped.PARTITION_ESP + + +# class PartitionGUIDs(Enum): +# """ +# A list of Partition type GUIDs (lsblk -o+PARTTYPE) can be found here: https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs +# """ +# XBOOTLDR = 'bc13c2ff-59e6-4262-a352-b275fd6f7172' class FilesystemType(Enum): @@ -605,6 +620,8 @@ class PartitionModification: partuuid: Optional[str] = None uuid: Optional[str] = None + _boot_indicator_flags = [PartitionFlag.Boot, PartitionFlag.XBOOTLDR] + def __post_init__(self): # needed to use the object as a dictionary key due to hash func if not hasattr(self, '_obj_id'): @@ -674,7 +691,10 @@ class PartitionModification: raise ValueError('Mountpoint is not specified') def is_boot(self) -> bool: - return PartitionFlag.Boot in self.flags + """ + Returns True if any of the boot indicator flags are found in self.flags + """ + return any(set(self.flags) & set(self._boot_indicator_flags)) def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: if relative_mountpoint is not None and self.mountpoint is not None: @@ -765,9 +785,24 @@ class DeviceModification: def add_partition(self, partition: PartitionModification): self.partitions.append(partition) + def get_efi_partition(self) -> Optional[PartitionModification]: + """ + Similar to get_boot_partition() but excludes XBOOTLDR partitions from it's candidates. + """ + fliltered = filter(lambda x: x.is_boot() and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) + return next(fliltered, None) + def get_boot_partition(self) -> Optional[PartitionModification]: - liltered = filter(lambda x: x.is_boot(), self.partitions) - return next(liltered, None) + """ + Returns the first partition marked as XBOOTLDR (PARTTYPE id of bc13c2ff-...) or Boot and has a mountpoint. + Only returns XBOOTLDR if separate EFI is detected using self.get_efi_partition() + """ + if efi_partition := self.get_efi_partition(): + fliltered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) + else: + fliltered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) + + return next(fliltered, None) def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: filtered = filter(lambda x: x.is_root(relative_path), self.partitions) @@ -886,6 +921,7 @@ class LsblkInfo: rota: bool = False tran: Optional[str] = None partuuid: Optional[str] = None + parttype :Optional[str] = None uuid: Optional[str] = None fstype: Optional[str] = None fsver: Optional[str] = None @@ -909,6 +945,7 @@ class LsblkInfo: 'rota': self.rota, 'tran': self.tran, 'partuuid': self.partuuid, + 'parttype' : self.parttype, 'uuid': self.uuid, 'fstype': self.fstype, 'fsver': self.fsver, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 083bd7c9..9582df77 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -172,7 +172,7 @@ class Installer: ) def sanity_check(self): - self._verify_boot_part() + # self._verify_boot_part() self._verify_service_stop() def mount_ordered_layout(self): @@ -677,6 +677,12 @@ class Installer: else: raise ValueError(f"Archinstall currently only supports setting up swap on zram") + def _get_efi_partition(self) -> Optional[disk.PartitionModification]: + for layout in self._disk_config.device_modifications: + if partition := layout.get_efi_partition(): + return partition + return None + def _get_boot_partition(self) -> Optional[disk.PartitionModification]: for layout in self._disk_config.device_modifications: if boot := layout.get_boot_partition(): @@ -689,7 +695,12 @@ class Installer: return root return None - def _add_systemd_bootloader(self, root_partition: disk.PartitionModification): + def _add_systemd_bootloader( + self, + boot_partition: disk.PartitionModification, + root_partition: disk.PartitionModification, + efi_partition: Optional[disk.PartitionModification] + ): self.pacman.strap('efibootmgr') if not SysInfo.has_uefi(): @@ -698,15 +709,24 @@ class Installer: # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. + bootctl_options = [ + f'--esp-path={efi_partition.mountpoint}' if efi_partition else '', + f'--boot-path={boot_partition.mountpoint}' if boot_partition else '' + ] # Install the boot loader try: - SysCommand(f'/usr/bin/arch-chroot {self.target} bootctl --esp-path=/boot install') + SysCommand(f"/usr/bin/arch-chroot {self.target} bootctl {' '.join(bootctl_options)} install") except SysCallError: # Fallback, try creating the boot loader without touching the EFI variables - SysCommand(f'/usr/bin/arch-chroot {self.target} bootctl --no-variables --esp-path=/boot install') + SysCommand(f"/usr/bin/arch-chroot {self.target} bootctl --no-variables {' '.join(bootctl_options)} install") - # Ensure that the /boot/loader directory exists before we try to create files in it + # Ensure that the $BOOT/loader/ directory exists before we try to create files in it. + # + # As mentioned in https://github.com/archlinux/archinstall/pull/1859 - we store the + # loader entries in $BOOT/loader/ rather than $ESP/loader/ + # The current reasoning being that $BOOT works in both use cases as well + # as being tied to the current installation. This may change. loader_dir = self.target / 'boot/loader' loader_dir.mkdir(parents=True, exist_ok=True) @@ -732,7 +752,7 @@ class Installer: else: loader.write(f"{line}\n") - # Ensure that the /boot/loader/entries directory exists before we try to create files in it + # Ensure that the $BOOT/loader/entries/ directory exists before we try to create files in it entries_dir = loader_dir / 'entries' entries_dir.mkdir(parents=True, exist_ok=True) @@ -836,12 +856,12 @@ class Installer: self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) + SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory={boot_partition.mountpoint} --bootloader-id=GRUB --removable', peek_output=True) except SysCallError: try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable', peek_output=True) + SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory={boot_partition.mountpoint} --bootloader-id=GRUB --removable', peek_output=True) except SysCallError as err: - raise DiskError(f"Could not install GRUB to {self.target}/boot: {err}") + raise DiskError(f"Could not install GRUB to {self.target}{boot_partition.mountpoint}: {err}") else: device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) @@ -861,7 +881,7 @@ class Installer: raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {err}") try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o /boot/grub/grub.cfg') + SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o {boot_partition.mountpoint}/grub/grub.cfg') except SysCallError as err: raise DiskError(f"Could not configure GRUB: {err}") @@ -1055,6 +1075,7 @@ TIMEOUT=5 if plugin.on_add_bootloader(self): return True + efi_partition = self._get_efi_partition() boot_partition = self._get_boot_partition() root_partition = self._get_root_partition() @@ -1068,7 +1089,7 @@ TIMEOUT=5 match bootloader: case Bootloader.Systemd: - self._add_systemd_bootloader(root_partition) + self._add_systemd_bootloader(boot_partition, root_partition, efi_partition) case Bootloader.Grub: self._add_grub_bootloader(boot_partition, root_partition) case Bootloader.Efistub: -- cgit v1.2.3-54-g00ecf From d76f4a029604dffe740ef1d44fa5f34ec0b23480 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 25 Jul 2023 05:16:02 -0400 Subject: Fix boot partition regression (#1942) * Fix boot partition regression * Fix spelling --- archinstall/lib/disk/device_model.py | 14 ++++++++------ archinstall/lib/installer.py | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index ad3426b6..b1d07d98 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -789,8 +789,8 @@ class DeviceModification: """ Similar to get_boot_partition() but excludes XBOOTLDR partitions from it's candidates. """ - fliltered = filter(lambda x: x.is_boot() and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) - return next(fliltered, None) + filtered = filter(lambda x: x.is_boot() and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) + return next(filtered, None) def get_boot_partition(self) -> Optional[PartitionModification]: """ @@ -798,11 +798,13 @@ class DeviceModification: Only returns XBOOTLDR if separate EFI is detected using self.get_efi_partition() """ if efi_partition := self.get_efi_partition(): - fliltered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) + filtered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) + if boot_partition := next(filtered, None): + return boot_partition + return efi_partition else: - fliltered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) - - return next(fliltered, None) + filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) + return next(filtered, None) def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: filtered = filter(lambda x: x.is_root(relative_path), self.partitions) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 836b6b79..f5999002 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -726,10 +726,11 @@ class Installer: # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. - bootctl_options = [ - f'--esp-path={efi_partition.mountpoint}' if efi_partition else '', - f'--boot-path={boot_partition.mountpoint}' if boot_partition else '' - ] + bootctl_options = [] + + if efi_partition and boot_partition != efi_partition: + bootctl_options.append(f'--esp-path={efi_partition.mountpoint}') + bootctl_options.append(f'--boot-path={boot_partition.mountpoint}') # Install the boot loader try: -- cgit v1.2.3-54-g00ecf From a548d7df70102252a557214edc74b77f4859f031 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 25 Jul 2023 19:19:14 +1000 Subject: Fix 1916 (#1920) * Do not stdout when menu is active * Handle missing libfido2 gracefully * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/encryption_menu.py | 25 ++++++++++++++++++++++--- archinstall/lib/disk/fido.py | 9 +++++---- archinstall/lib/menu/menu.py | 9 +++++++++ archinstall/lib/output.py | 14 ++++++++------ 4 files changed, 44 insertions(+), 13 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 89eade2b..234e3b03 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -71,6 +71,7 @@ class DiskEncryptionMenu(AbstractSubMenu): description=_('Use HSM to unlock encrypted drive'), func=lambda preset: select_hsm(preset), display_func=lambda x: self._display_hsm(x), + preview_func=self._prev_hsm, dependencies=['encryption_password'], default=self._preset.hsm_device, enabled=True @@ -93,8 +94,6 @@ class DiskEncryptionMenu(AbstractSubMenu): if device: return device.manufacturer - if not Fido2.get_fido2_devices(): - return str(_('No HSM devices available')) return None def _prev_disk_layouts(self) -> Optional[str]: @@ -106,6 +105,22 @@ class DiskEncryptionMenu(AbstractSubMenu): return None + def _prev_hsm(self) -> Optional[str]: + try: + Fido2.get_fido2_devices() + except ValueError: + return str(_('Unable to determine fido2 devices. Is libfido2 installed?')) + + fido_device: Optional[Fido2Device] = self._menu_options['HSM'].current_selection + + if fido_device: + output = '{}: {}'.format(str(_('Path')), fido_device.path) + output += '{}: {}'.format(str(_('Manufacturer')), fido_device.manufacturer) + output += '{}: {}'.format(str(_('Product')), fido_device.product) + return output + + return None + def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: title = str(_('Select disk encryption option')) @@ -130,7 +145,11 @@ def select_encrypted_password() -> Optional[str]: def select_hsm(preset: Optional[Fido2Device] = None) -> Optional[Fido2Device]: title = _('Select a FIDO2 device to use for HSM') - fido_devices = Fido2.get_fido2_devices() + + try: + fido_devices = Fido2.get_fido2_devices() + except ValueError: + return None if fido_devices: choice = TableMenu(title, data=fido_devices).run() diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index d8c8fd3c..96a74991 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -7,6 +7,7 @@ from typing import List, Optional from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes from ..output import error, info +from ..exceptions import SysCallError class Fido2: @@ -36,13 +37,13 @@ class Fido2: # to prevent continous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: - ret: Optional[str] = None try: - ret = SysCommand("systemd-cryptenroll --fido2-device=list").decode('UTF-8') - except: + ret: Optional[str] = SysCommand("systemd-cryptenroll --fido2-device=list").decode('UTF-8') + except SysCallError: error('fido2 support is most likely not installed') + raise ValueError('HSM devices can not be detected, is libfido2 installed?') + if not ret: - error('Unable to retrieve fido2 devices') return [] fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 768dfe55..358ba5e4 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -34,6 +34,11 @@ class MenuSelection: class Menu(TerminalMenu): + _menu_is_active: bool = False + + @staticmethod + def is_menu_active() -> bool: + return Menu._menu_is_active @classmethod def back(cls) -> str: @@ -260,6 +265,8 @@ class Menu(TerminalMenu): return MenuSelection(type_=MenuSelectionType.Skip) def run(self) -> MenuSelection: + Menu._menu_is_active = True + selection = self._show() if selection.type_ == MenuSelectionType.Reset: @@ -277,6 +284,8 @@ class Menu(TerminalMenu): selection.type_ = MenuSelectionType.Skip selection.value = None + Menu._menu_is_active = False + return selection def set_cursor_pos(self,pos :int): diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index d1c95ec5..9f2a2ae3 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -318,9 +318,11 @@ def log( Journald.log(text, level=level) - # Finally, print the log unless we skipped it based on level. - # We use sys.stdout.write()+flush() instead of print() to try and - # fix issue #94 - if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): - sys.stdout.write(f"{text}\n") - sys.stdout.flush() + from .menu import Menu + if not Menu.is_menu_active(): + # Finally, print the log unless we skipped it based on level. + # We use sys.stdout.write()+flush() instead of print() to try and + # fix issue #94 + if level != logging.DEBUG or storage.get('arguments', {}).get('verbose', False): + sys.stdout.write(f"{text}\n") + sys.stdout.flush() -- cgit v1.2.3-54-g00ecf From 3cea9baff6b0b15b766c7095f1d56adece74b58e Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sun, 30 Jul 2023 23:40:40 +0200 Subject: Muted partprobe (#1964) * Muted partprobe * Missing import --- archinstall/lib/disk/device_handler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 9acf0999..d46275d1 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -3,6 +3,7 @@ from __future__ import annotations import json import os import time +import logging from pathlib import Path from typing import List, Dict, Any, Optional, TYPE_CHECKING @@ -23,7 +24,7 @@ from .device_model import ( from ..exceptions import DiskError, UnknownFilesystemFormat from ..general import SysCommand, SysCallError, JSON from ..luks import Luks2 -from ..output import debug, error, info, warn +from ..output import debug, error, info, warn, log from ..utils.util import is_subpath if TYPE_CHECKING: @@ -584,7 +585,10 @@ class DeviceHandler(object): debug(f'Calling partprobe: {command}') SysCommand(command) except SysCallError as err: - error(f'"{command}" failed to run: {err}') + if 'have been written, but we have been unable to inform the kernel of the change' in str(err): + log(f"Partprobe was not able to inform the kernel of the new disk state (ignoring error): {err}", fg="gray", level=logging.INFO) + else: + error(f'"{command}" failed to run (continuing anyway): {err}') def _wipe(self, dev_path: Path): """ -- cgit v1.2.3-54-g00ecf From c8e0b9a4d685b941e3b406bc6f8ecfaef60e1f5f Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 14 Sep 2023 20:04:25 +1000 Subject: Default to unknown on partition types (#2037) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index b1d07d98..8ea4e06e 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -20,6 +20,7 @@ from ..exceptions import DiskError, SysCallError from ..general import SysCommand from ..output import debug, error from ..storage import storage +from ..output import info if TYPE_CHECKING: _: Any @@ -509,13 +510,15 @@ class BDevice: class PartitionType(Enum): Boot = 'boot' Primary = 'primary' + _Unknown = 'unknown' @classmethod def get_type_from_code(cls, code: int) -> PartitionType: if code == parted.PARTITION_NORMAL: return PartitionType.Primary - - raise DiskError(f'Partition code not supported: {code}') + else: + info(f'Partition code not supported: {code}') + return PartitionType._Unknown def get_partition_code(self) -> Optional[int]: if self == PartitionType.Primary: @@ -659,9 +662,9 @@ class PartitionModification: if partition_info.btrfs_subvol_infos: mountpoint = None subvol_mods = [] - for info in partition_info.btrfs_subvol_infos: + for i in partition_info.btrfs_subvol_infos: subvol_mods.append( - SubvolumeModification.from_existing_subvol_info(info) + SubvolumeModification.from_existing_subvol_info(i) ) else: mountpoint = partition_info.mountpoints[0] if partition_info.mountpoints else None -- cgit v1.2.3-54-g00ecf From 0258b0335e2174b932ee47159512bca56c607623 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 14 Sep 2023 20:05:53 +1000 Subject: Fix Bootloader installation (#2032) * Fix broken path * Update * Update --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 4 +++ archinstall/lib/disk/device_model.py | 1 - archinstall/lib/installer.py | 50 ++++++++++++++++++++++------------ 3 files changed, 36 insertions(+), 19 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index d46275d1..7731bbc3 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -117,6 +117,10 @@ class DeviceHandler(object): return part return None + def get_parent_device_path(self, dev_path: Path) -> Path: + lsblk = get_lsblk_info(dev_path) + return Path(f'/dev/{lsblk.pkname}') + def get_uuid_for_path(self, path: Path) -> Optional[str]: partition = self.find_partition(path) return partition.partuuid if partition else None diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8ea4e06e..7611eda5 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -1083,7 +1083,6 @@ def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: def get_all_lsblk_info() -> List[LsblkInfo]: return _fetch_lsblk_info() - def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> List[LsblkInfo]: def _check(infos: List[LsblkInfo]) -> List[LsblkInfo]: devices = [] diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ba57a001..7337fe6f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -880,17 +880,31 @@ class Installer: self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory={boot_partition.mountpoint} --bootloader-id=GRUB --removable', peek_output=True) + SysCommand( + f'/usr/bin/arch-chroot {self.target} grub-install ' + f'--debug ' + f'--target=x86_64-efi ' + f'--efi-directory={boot_partition.mountpoint} ' + f'--bootloader-id=GRUB ' + f'--removable', + peek_output=True + ) except SysCallError: try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-install --debug --target=x86_64-efi --efi-directory={boot_partition.mountpoint} --bootloader-id=GRUB --removable', peek_output=True) + SysCommand( + f'/usr/bin/arch-chroot {self.target} ' + f'grub-install ' + f'--debug ' + f'--target=x86_64-efi ' + f'--efi-directory={boot_partition.mountpoint} ' + f'--bootloader-id=GRUB ' + f'--removable', + peek_output=True + ) except SysCallError as err: raise DiskError(f"Could not install GRUB to {self.target}{boot_partition.mountpoint}: {err}") else: - device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) - - if not device: - raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}') + parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) try: cmd = f'/usr/bin/arch-chroot' \ @@ -898,14 +912,17 @@ class Installer: f' grub-install' \ f' --debug' \ f' --target=i386-pc' \ - f' --recheck {device.device_info.path}' + f' --recheck {parent_dev_path}' SysCommand(cmd, peek_output=True) except SysCallError as err: raise DiskError(f"Failed to install GRUB boot on {boot_partition.dev_path}: {err}") try: - SysCommand(f'/usr/bin/arch-chroot {self.target} grub-mkconfig -o {boot_partition.mountpoint}/grub/grub.cfg') + SysCommand( + f'/usr/bin/arch-chroot {self.target} ' + f'grub-mkconfig -o {boot_partition.mountpoint}/grub/grub.cfg' + ) except SysCallError as err: raise DiskError(f"Could not configure GRUB: {err}") @@ -923,10 +940,6 @@ class Installer: # partition before the format. root_uuid = get_lsblk_info(root_partition.safe_dev_path).uuid - device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) - if not device: - raise ValueError(f'Can not find block device: {boot_partition.safe_dev_path}') - def create_pacman_hook(contents: str): HOOK_DIR = "/etc/pacman.d/hooks" SysCommand(f"/usr/bin/arch-chroot {self.target} mkdir -p {HOOK_DIR}") @@ -940,6 +953,8 @@ class Installer: f' cp' \ f' /usr/share/limine/BOOTX64.EFI' \ f' /boot/EFI/BOOT/' + + SysCommand(cmd, peek_output=True) except SysCallError as err: raise DiskError(f"Failed to install Limine BOOTX64.EFI on {boot_partition.dev_path}: {err}") @@ -957,6 +972,8 @@ When = PostTransaction Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/BOOT/ """) else: + parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) + try: # The `limine.sys` file, contains stage 3 code. cmd = f'/usr/bin/arch-chroot' \ @@ -972,7 +989,7 @@ Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/BOOT/ f' {self.target}' \ f' limine' \ f' bios-install' \ - f' {device.device_info.path}' + f' {parent_dev_path}' SysCommand(cmd, peek_output=True) except SysCallError as err: @@ -1062,13 +1079,10 @@ TIMEOUT=5 debug(f'Root partition is an encrypted device identifying by PARTUUID: {root_partition.partuuid}') kernel_parameters.append(f'root=PARTUUID={root_partition.partuuid} rw rootfstype={root_partition.safe_fs_type.value} {" ".join(self._kernel_params)}') - device = disk.device_handler.get_device_by_partition_path(boot_partition.safe_dev_path) - - if not device: - raise ValueError(f'Unable to find block device: {boot_partition.safe_dev_path}') + parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) cmd = f'efibootmgr ' \ - f'--disk {device.device_info.path} ' \ + f'--disk {parent_dev_path} ' \ f'--part {boot_partition.safe_dev_path} ' \ f'--create ' \ f'--label "{label}" ' \ -- cgit v1.2.3-54-g00ecf From 56567221b6984f592ad9f591be814c03d554ca2a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Thu, 14 Sep 2023 20:06:23 +1000 Subject: Fix 1971 - manual partitioning (#2031) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 31 ++++++++++++++++--------------- archinstall/lib/disk/device_model.py | 2 ++ archinstall/lib/disk/partitioning_menu.py | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 7731bbc3..7cd79784 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -246,7 +246,7 @@ class DeviceHandler(object): info(f'luks2 locking device: {dev_path}') luks_handler.lock() - def _validate(self, device_mod: DeviceModification): + def _validate_partitions(self, partitions: List[PartitionModification]): checks = { # verify that all partitions have a path set (which implies that they have been created) lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), @@ -257,7 +257,7 @@ class DeviceHandler(object): } for check, exc in checks.items(): - found = next(filter(check, device_mod.partitions), None) + found = next(filter(check, partitions), None) if found is not None: raise exc @@ -270,12 +270,16 @@ class DeviceHandler(object): Format can be given an overriding path, for instance /dev/null to test the formatting functionality and in essence the support for the given filesystem. """ - self._validate(device_mod) + + # don't touch existing partitions + filtered_part = [p for p in device_mod.partitions if not p.exists()] + + self._validate_partitions(filtered_part) # make sure all devices are unmounted - self._umount_all_existing(device_mod) + self._umount_all_existing(device_mod.device_path) - for part_mod in device_mod.partitions: + for part_mod in filtered_part: # partition will be encrypted if enc_conf is not None and part_mod in enc_conf.partitions: self._perform_enc_formatting( @@ -446,10 +450,10 @@ class DeviceHandler(object): return luks_handler - def _umount_all_existing(self, modification: DeviceModification): - info(f'Unmounting all partitions: {modification.device_path}') + def _umount_all_existing(self, device_path: Path): + info(f'Unmounting all existing partitions: {device_path}') - existing_partitions = self._devices[modification.device_path].partition_infos + existing_partitions = self._devices[device_path].partition_infos for partition in existing_partitions: debug(f'Unmounting: {partition.path}') @@ -476,7 +480,7 @@ class DeviceHandler(object): raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions') # make sure all devices are unmounted - self._umount_all_existing(modification) + self._umount_all_existing(modification.device_path) # WARNING: the entire device will be wiped and all data lost if modification.wipe: @@ -489,13 +493,10 @@ class DeviceHandler(object): info(f'Creating partitions: {modification.device_path}') - # TODO sort by delete first - - for part_mod in modification.partitions: - # don't touch existing partitions - if part_mod.exists(): - continue + # don't touch existing partitions + filtered_part = [p for p in modification.partitions if not p.exists()] + for part_mod in filtered_part: # if the entire disk got nuked then we don't have to delete # any existing partitions anymore because they're all gone already requires_delete = modification.wipe is False diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 7611eda5..28ee3116 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -96,6 +96,7 @@ class DiskLayoutConfiguration: length=Size.parse_args(partition['length']), mount_options=partition['mount_options'], mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, + dev_path=Path(partition['dev_path']) if partition['dev_path'] else None, type=PartitionType(partition['type']), flags=[PartitionFlag[f] for f in partition.get('flags', [])], btrfs_subvols=SubvolumeModification.parse_args(partition.get('btrfs', [])), @@ -750,6 +751,7 @@ class PartitionModification: 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, 'flags': [f.name for f in self.flags], + 'dev_path': str(self.dev_path) if self.dev_path else None, 'btrfs': [vol.__dump__() for vol in self.btrfs_subvols] } diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 4acb4e85..4c4d8fac 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -347,7 +347,7 @@ def manual_partitioning( manual_preset = preset menu_list = PartitioningList(prompt, device, manual_preset) - partitions = menu_list.run() + partitions: List[PartitionModification] = menu_list.run() if menu_list.is_last_choice_cancel(): return preset -- cgit v1.2.3-54-g00ecf From f4a6d11373c61f77236f95b2a97f505c6eab55a2 Mon Sep 17 00:00:00 2001 From: Himadri Bhattacharjee Date: Sat, 16 Sep 2023 23:21:08 +0000 Subject: fix: make nodatacow and compress mutually exclusive (#1998) * fix: make nodatacow and compress mutually exclusive * fix: raise ValueError when both compress and nodatacow flags are set --- archinstall/lib/disk/device_model.py | 10 ++++++++-- archinstall/lib/disk/subvolume_menu.py | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 28ee3116..6eeb0d91 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -412,12 +412,18 @@ class SubvolumeModification: mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None + compress = entry.get('compress', False) + nodatacow = entry.get('nodatacow', False) + + if compress and nodatacow: + raise ValueError('compress and nodatacow flags cannot be enabled simultaneously on a btfrs subvolume') + mods.append( SubvolumeModification( entry['name'], mountpoint, - entry.get('compress', False), - entry.get('nodatacow', False) + compress, + nodatacow ) ) diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 32a0e616..2b70d7b2 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -46,7 +46,6 @@ class SubvolumeMenu(ListManager): ['nodatacow', 'compress'], skip=True, preset_values=preset_options, - multi=True ).run() if choice.type_ == MenuSelectionType.Selection: -- cgit v1.2.3-54-g00ecf From 12b7017240a040fd4fbebf7c5794a1ca5560f0ea Mon Sep 17 00:00:00 2001 From: Alexander Seiler Date: Mon, 18 Sep 2023 14:04:36 +0200 Subject: Fix many typos (#1692) Signed-off-by: Alexander Seiler --- archinstall/__init__.py | 2 +- archinstall/default_profiles/profile.py | 6 +++--- archinstall/lib/disk/device_model.py | 2 +- archinstall/lib/disk/fido.py | 2 +- archinstall/lib/general.py | 2 +- archinstall/lib/global_menu.py | 4 ++-- archinstall/lib/installer.py | 10 +++++----- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/general_conf.py | 2 +- archinstall/lib/menu/list_manager.py | 2 +- archinstall/lib/menu/menu.py | 2 +- archinstall/lib/output.py | 2 +- archinstall/lib/translationhandler.py | 2 +- archinstall/scripts/guided.py | 4 ++-- archinstall/scripts/swiss.py | 6 +++--- examples/interactive_installation.py | 4 ++-- 16 files changed, 27 insertions(+), 27 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 39588904..7645ae39 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -43,7 +43,7 @@ __version__ = "2.6.0" storage['__version__'] = __version__ -# add the custome _ as a builtin, it can now be used anywhere in the +# add the custom _ as a builtin, it can now be used anywhere in the # project to mark strings as translatable with _('translate me') DeferredTranslation.install() diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index 982bd5a3..49a9c19d 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -81,7 +81,7 @@ class Profile: def packages(self) -> List[str]: """ Returns a list of packages that should be installed when - this profile is among the choosen ones + this profile is among the chosen ones """ return self._packages @@ -128,7 +128,7 @@ class Profile: """ Set the custom settings for the profile. This is also called when the settings are parsed from the config - and can be overriden to perform further actions based on the profile + and can be overridden to perform further actions based on the profile """ self.custom_settings = settings @@ -179,7 +179,7 @@ class Profile: def preview_text(self) -> Optional[str]: """ Used for preview text in profiles_bck. If a description is set for a - profile it will automatically display that one in the preivew. + profile it will automatically display that one in the preview. If no preview or a different text should be displayed just """ if self.description: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 6eeb0d91..69038b01 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -202,7 +202,7 @@ class Size: # not sure why we would ever wanna convert to percentages if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing paramter total size to be able to convert to percentage') + raise ValueError('Missing parameter total size to be able to convert to percentage') if self.unit == target_unit: return self diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 96a74991..9eeba56a 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -34,7 +34,7 @@ class Fido2: /dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID """ - # to prevent continous reloading which will slow + # to prevent continuous reloading which will slow # down moving the cursor in the menu if not cls._loaded or reload: try: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 611378ee..e22e7eed 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: def generate_password(length :int = 64) -> str: - haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace + haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace return ''.join(secrets.choice(haystack) for i in range(length)) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index fb62b7b5..b38dac0b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -15,7 +15,7 @@ from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .storage import storage from .configuration import save_config -from .interactions import add_number_of_parrallel_downloads +from .interactions import add_number_of_parallel_downloads from .interactions import ask_additional_packages_to_install from .interactions import ask_for_additional_users from .interactions import ask_for_audio_selection @@ -119,7 +119,7 @@ class GlobalMenu(AbstractMenu): self._menu_options['parallel downloads'] = \ Selector( _('Parallel Downloads'), - lambda preset: add_number_of_parrallel_downloads(preset), + lambda preset: add_number_of_parallel_downloads(preset), display_func=lambda x: x if x else '0', default=0 ) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 09e91ab8..34c9441f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -194,7 +194,7 @@ class Installer: for part_mod in sorted_part_mods: if luks_handler := luks_handlers.get(part_mod): # mount encrypted partition - self._mount_luks_partiton(part_mod, luks_handler) + self._mount_luks_partition(part_mod, luks_handler) else: # partition is not encrypted self._mount_partition(part_mod) @@ -219,7 +219,7 @@ class Installer: if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) - def _mount_luks_partiton(self, part_mod: disk.PartitionModification, luks_handler: Luks2): + def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint and luks_handler.mapper_dev: target = self.target / part_mod.relative_mountpoint @@ -315,7 +315,7 @@ class Installer: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') if not gen_fstab: - raise RequirementError(f'Genrating fstab returned empty value') + raise RequirementError(f'Generating fstab returned empty value') with open(fstab_path, 'a') as fp: fp.write(gen_fstab) @@ -434,7 +434,7 @@ class Installer: return False - def activate_time_syncronization(self) -> None: + def activate_time_synchronization(self) -> None: info('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers') self.enable_service('systemd-timesyncd') @@ -1008,7 +1008,7 @@ When = PostTransaction Exec = /bin/sh -c \\"/usr/bin/limine bios-install /dev/disk/by-uuid/{root_uuid} && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/\\" """) - # Limine does not ship with a default configuation file. We are going to + # Limine does not ship with a default configuration file. We are going to # create a basic one that is similar to the one GRUB generates. try: config = f""" diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 53be8e7a..50c0012d 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -11,7 +11,7 @@ from .disk_conf import ( from .general_conf import ( ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_archinstall_language, ask_additional_packages_to_install, - add_number_of_parrallel_downloads, select_additional_repositories + add_number_of_parallel_downloads, select_additional_repositories ) from .system_conf import ( diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 14fcc3f8..8dd6e94f 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -164,7 +164,7 @@ def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]: return packages -def add_number_of_parrallel_downloads(input_number :Optional[int] = None) -> Optional[int]: +def add_number_of_parallel_downloads(input_number :Optional[int] = None) -> Optional[int]: max_recommended = 5 print(_(f"This option enables the number of parallel downloads that can occur during package downloads")) print(_("Enter the number of parallel downloads to be enabled.\n\nNote:\n")) diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index be31fdf0..54fb6a1b 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -80,7 +80,7 @@ class ListManager: self._data = self.handle_action(choice.value, None, self._data) elif choice.value in self._terminate_actions: break - else: # an entry of the existing selection was choosen + else: # an entry of the existing selection was chosen selected_entry = data_formatted[choice.value] # type: ignore self._run_actions_on_entry(selected_entry) diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 358ba5e4..3bd31b88 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -123,7 +123,7 @@ class Menu(TerminalMenu): :param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state :type allow_reset: bool - param allow_reset_warning_msg: If raise_error_on_interrupt is True the warnign is set, a user confirmation is displayed + param allow_reset_warning_msg: If raise_error_on_interrupt is True the warning is set, a user confirmation is displayed type allow_reset_warning_msg: str :param extra_bottom_space: Add an extra empty line at the end of the menu diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 63d9c1fb..62a1ba27 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -22,7 +22,7 @@ class FormattedOutput: ) -> Dict[str, Any]: """ the original values returned a dataclass as dict thru the call to some specific methods - this version allows thru the parameter class_formatter to call a dynamicly selected formatting method. + this version allows thru the parameter class_formatter to call a dynamically selected formatting method. Can transmit a filter list to the class_formatter, """ if class_formatter: diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index a2e44065..33230562 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -138,7 +138,7 @@ class TranslationHandler: def get_language_by_abbr(self, abbr: str) -> Language: """ - Get a language object by its abbrevation, e.g. en + Get a language object by its abbreviation, e.g. en """ try: return next(filter(lambda x: x.abbr == abbr, self._translated_languages)) diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 51549fa8..d7cf16cd 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -185,7 +185,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -193,7 +193,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 80fa0a48..c04ccca4 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -54,7 +54,7 @@ class SetupMenu(GlobalMenu): super().setup_selection_menu_options() self._menu_options['mode'] = menu.Selector( - 'Excution mode', + 'Execution mode', lambda x : select_mode(), display_func=lambda x: x.value if x else '', default=ExecutionMode.Full) @@ -249,7 +249,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -257,7 +257,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 9eac029c..f8cc75fc 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -163,7 +163,7 @@ def perform_installation(mountpoint: Path): installation.set_timezone(timezone) if archinstall.arguments.get('ntp', False): - installation.activate_time_syncronization() + installation.activate_time_synchronization() if archinstall.accessibility_tools_in_use(): installation.enable_espeakup() @@ -171,7 +171,7 @@ def perform_installation(mountpoint: Path): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) - # This step must be after profile installs to allow profiles_bck to install language pre-requisits. + # This step must be after profile installs to allow profiles_bck to install language pre-requisites. # After which, this step will set the language both for console and x11 if x11 was installed for instance. installation.set_keyboard_language(locale_config.kb_layout) -- cgit v1.2.3-54-g00ecf From c0ff55d55b855f6975f4e588f6368ccb2a4294ac Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 19 Sep 2023 06:47:46 +1000 Subject: Allow assigning mountpoint on existing partitions (#2067) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/partitioning_menu.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 4c4d8fac..549c7f34 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -61,14 +61,13 @@ class PartitioningList(ListManager): if not selection.exists(): not_filter += [self._actions['mark_formatting']] else: - # only allow these options if the existing partition + # only allow options if the existing partition # was marked as formatting, otherwise we run into issues where # 1. select a new fs -> potentially mark as wipe now # 2. Switch back to old filesystem -> should unmark wipe now, but # how do we know it was the original one? not_filter += [ self._actions['set_filesystem'], - self._actions['assign_mountpoint'], self._actions['mark_bootable'], self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes'] -- cgit v1.2.3-54-g00ecf From 3e2999bc2752c76e666d916bdb80608ad74528bf Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:49:29 -0400 Subject: Fix acquisition of UUID (#2077) * Fix acquisition of UUID * Fix inadequate solution * Add check for UUID --- archinstall/lib/disk/device_handler.py | 26 +++++++++++++++++--------- archinstall/lib/installer.py | 6 +----- 2 files changed, 18 insertions(+), 14 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 7cd79784..a95e21e3 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -291,6 +291,11 @@ class DeviceHandler(object): else: self._perform_formatting(part_mod.safe_fs_type, part_mod.safe_dev_path) + lsblk_info = self._fetch_part_info(part_mod.safe_dev_path) + + part_mod.partuuid = lsblk_info.partuuid + part_mod.uuid = lsblk_info.uuid + def _perform_partitioning( self, part_mod: PartitionModification, @@ -354,15 +359,10 @@ class DeviceHandler(object): # the partition has a real path now as it was created part_mod.dev_path = Path(partition.path) - - lsblk_info = self._fetch_partuuid(part_mod.dev_path) - - part_mod.partuuid = lsblk_info.partuuid - part_mod.uuid = lsblk_info.uuid except PartitionException as ex: raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex - def _fetch_partuuid(self, path: Path) -> LsblkInfo: + def _fetch_part_info(self, path: Path) -> LsblkInfo: attempts = 3 lsblk_info: Optional[LsblkInfo] = None @@ -371,16 +371,24 @@ class DeviceHandler(object): time.sleep(attempt_nr + 1) lsblk_info = get_lsblk_info(path) - if lsblk_info.partuuid: + if lsblk_info.partuuid and lsblk_info.uuid: break self.partprobe(path) - if not lsblk_info or not lsblk_info.partuuid: + if not lsblk_info: + debug(f'Unable to get partition information: {path}') + raise DiskError(f'Unable to get partition information: {path}') + + if not lsblk_info.partuuid: debug(f'Unable to determine new partition uuid: {path}\n{lsblk_info}') raise DiskError(f'Unable to determine new partition uuid: {path}') - debug(f'partuuid found: {lsblk_info.json()}') + if not lsblk_info.uuid: + debug(f'Unable to determine new uuid: {path}\n{lsblk_info}') + raise DiskError(f'Unable to determine new uuid: {path}') + + debug(f'partition information found: {lsblk_info.json()}') return lsblk_info diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 2eb63279..cd6f651d 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -8,8 +8,6 @@ import time from pathlib import Path from typing import Any, List, Optional, TYPE_CHECKING, Union, Dict, Callable -from ..lib.disk.device_model import get_lsblk_info - from . import disk from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .general import SysCommand @@ -937,9 +935,7 @@ class Installer: self.pacman.strap('limine') info(f"Limine boot partition: {boot_partition.dev_path}") - # XXX: We cannot use `root_partition.uuid` since corresponds to the UUID of the root - # partition before the format. - root_uuid = get_lsblk_info(root_partition.safe_dev_path).uuid + root_uuid = root_partition.uuid def create_pacman_hook(contents: str): HOOK_DIR = "/etc/pacman.d/hooks" -- cgit v1.2.3-54-g00ecf From 6d908e8cd7d395a1f8f2473b2dc9ca3e2ace1226 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:13:22 -0400 Subject: Fix `_add_efistub_bootloader()` partition number with partn (#2084) --- archinstall/lib/disk/device_handler.py | 7 ++++++- archinstall/lib/disk/device_model.py | 3 +++ archinstall/lib/installer.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index a95e21e3..494319a1 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -293,6 +293,7 @@ class DeviceHandler(object): lsblk_info = self._fetch_part_info(part_mod.safe_dev_path) + part_mod.partn = lsblk_info.partn part_mod.partuuid = lsblk_info.partuuid part_mod.uuid = lsblk_info.uuid @@ -371,7 +372,7 @@ class DeviceHandler(object): time.sleep(attempt_nr + 1) lsblk_info = get_lsblk_info(path) - if lsblk_info.partuuid and lsblk_info.uuid: + if lsblk_info.partn and lsblk_info.partuuid and lsblk_info.uuid: break self.partprobe(path) @@ -380,6 +381,10 @@ class DeviceHandler(object): debug(f'Unable to get partition information: {path}') raise DiskError(f'Unable to get partition information: {path}') + if not lsblk_info.partn: + debug(f'Unable to determine new partition number: {path}\n{lsblk_info}') + raise DiskError(f'Unable to determine new partition number: {path}') + if not lsblk_info.partuuid: debug(f'Unable to determine new partition uuid: {path}\n{lsblk_info}') raise DiskError(f'Unable to determine new partition uuid: {path}') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 69038b01..ec0207a1 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -627,6 +627,7 @@ class PartitionModification: # only set if the device was created or exists dev_path: Optional[Path] = None + partn: Optional[int] = None partuuid: Optional[str] = None uuid: Optional[str] = None @@ -933,6 +934,7 @@ class LsblkInfo: ptuuid: str = '' rota: bool = False tran: Optional[str] = None + partn: Optional[int] = None partuuid: Optional[str] = None parttype :Optional[str] = None uuid: Optional[str] = None @@ -957,6 +959,7 @@ class LsblkInfo: 'ptuuid': self.ptuuid, 'rota': self.rota, 'tran': self.tran, + 'partn': self.partn, 'partuuid': self.partuuid, 'parttype' : self.parttype, 'uuid': self.uuid, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d71e6ad3..d0aa2252 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1085,7 +1085,7 @@ TIMEOUT=5 cmd = f'efibootmgr ' \ f'--disk {parent_dev_path} ' \ - f'--part {boot_partition.safe_dev_path} ' \ + f'--part {boot_partition.partn} ' \ f'--create ' \ f'--label "{label}" ' \ f'--loader {loader} ' \ -- cgit v1.2.3-54-g00ecf From ad6cbcfd3d5e21177150dd82f9c00661dff4e98e Mon Sep 17 00:00:00 2001 From: Thomas Aldrian Date: Sat, 23 Sep 2023 03:11:21 +0100 Subject: Fix GRUB with non-/boot ESP (#2015) * Fix GRUB with non-/boot ESP Fixes #2001 * GRUB EFI fixes * Fix flake8 and mypy --------- Co-authored-by: Anton Hvornum Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 9 +++++++-- archinstall/lib/installer.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index ec0207a1..ad2628ae 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -800,20 +800,25 @@ class DeviceModification: def get_efi_partition(self) -> Optional[PartitionModification]: """ Similar to get_boot_partition() but excludes XBOOTLDR partitions from it's candidates. + Also works with ESP flag. """ - filtered = filter(lambda x: x.is_boot() and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) + filtered = filter(lambda x: (x.is_boot() or PartitionFlag.ESP in x.flags) and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) return next(filtered, None) def get_boot_partition(self) -> Optional[PartitionModification]: """ Returns the first partition marked as XBOOTLDR (PARTTYPE id of bc13c2ff-...) or Boot and has a mountpoint. Only returns XBOOTLDR if separate EFI is detected using self.get_efi_partition() + Will return None if no suitable partition is found. """ if efi_partition := self.get_efi_partition(): filtered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) if boot_partition := next(filtered, None): return boot_partition - return efi_partition + if efi_partition.is_boot(): + return efi_partition + else: + return None else: filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) return next(filtered, None) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d0aa2252..afc1184b 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -874,7 +874,8 @@ class Installer: def _add_grub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification + root_partition: disk.PartitionModification, + efi_partition: Optional[disk.PartitionModification] ): self.pacman.strap('grub') # no need? @@ -895,12 +896,15 @@ class Installer: '--debug' ] - if SysInfo.has_uefi(): + if SysInfo.has_uefi() and efi_partition is not None: + info(f"GRUB EFI partition: {efi_partition.dev_path}") + self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? add_options = [ '--target=x86_64-efi', - f'--efi-directory={boot_partition.mountpoint}', + f'--efi-directory={efi_partition.mountpoint}' + f'--boot-directory={boot_partition.mountpoint if boot_partition else "/boot"}' '--bootloader-id=GRUB', '--removable' ] @@ -913,8 +917,10 @@ class Installer: try: SysCommand(command, peek_output=True) except SysCallError as err: - raise DiskError(f"Could not install GRUB to {self.target}{boot_partition.mountpoint}: {err}") + raise DiskError(f"Could not install GRUB to {self.target}{efi_partition.mountpoint}: {err}") else: + info(f"GRUB boot partition: {boot_partition.dev_path}") + parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) add_options = [ @@ -931,7 +937,7 @@ class Installer: try: SysCommand( f'/usr/bin/arch-chroot {self.target} ' - f'grub-mkconfig -o {boot_partition.mountpoint}/grub/grub.cfg' + f'grub-mkconfig -o {boot_partition.mountpoint if boot_partition else "/boot"}/grub/grub.cfg' ) except SysCallError as err: raise DiskError(f"Could not configure GRUB: {err}") @@ -1131,7 +1137,7 @@ TIMEOUT=5 case Bootloader.Systemd: self._add_systemd_bootloader(boot_partition, root_partition, efi_partition) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root_partition) + self._add_grub_bootloader(boot_partition, root_partition, efi_partition) case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root_partition) case Bootloader.Limine: -- cgit v1.2.3-54-g00ecf From ab5de3e2e6f5afba81d441cc9a46ebadaad88732 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Sat, 23 Sep 2023 20:51:20 -0400 Subject: Add support for loop devices (#2097) --- archinstall/lib/disk/device_handler.py | 15 +++++++++++++-- archinstall/lib/disk/device_model.py | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 494319a1..9646103f 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -10,7 +10,7 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING from parted import ( # type: ignore Disk, Geometry, FileSystem, PartitionException, DiskLabelException, - getAllDevices, freshDisk, Partition, Device + getDevice, getAllDevices, freshDisk, Partition, Device ) from .device_model import ( @@ -45,7 +45,18 @@ class DeviceHandler(object): def load_devices(self): block_devices = {} - for device in getAllDevices(): + devices = getAllDevices() + + try: + loop_devices = SysCommand(['losetup', '-a']) + except SysCallError as err: + debug(f'Failed to get loop devices: {err}') + else: + for ld_info in str(loop_devices).splitlines(): + loop_device = getDevice(ld_info.split(':', maxsplit=1)[0]) + devices.append(loop_device) + + for device in devices: if get_lsblk_info(device.path).type == 'rom': continue diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index ad2628ae..b520ad1b 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -374,7 +374,10 @@ class _DeviceInfo: @classmethod def from_disk(cls, disk: Disk) -> _DeviceInfo: device = disk.device - device_type = parted.devices[device.type] + if device.type == 18: + device_type = 'loop' + else: + device_type = parted.devices[device.type] sector_size = Size(device.sectorSize, Unit.B) free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] -- cgit v1.2.3-54-g00ecf From 360a1b4f337e45b2dc26c9af067e53fbd364231f Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 24 Sep 2023 18:14:02 +1000 Subject: Unify json functions (#2102) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_model.py | 34 +++++++++++++------------ archinstall/lib/general.py | 8 +++--- archinstall/lib/models/audio_configuration.py | 2 +- archinstall/lib/models/bootloader.py | 8 +++--- archinstall/lib/models/network_configuration.py | 6 ++--- archinstall/lib/output.py | 2 -- 6 files changed, 29 insertions(+), 31 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index b520ad1b..8bc41e0c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -49,10 +49,10 @@ class DiskLayoutConfiguration: if self.config_type == DiskLayoutType.Pre_mount and self.relative_mountpoint is None: raise ValueError('Must set a relative mountpoint when layout type is pre-mount"') - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: return { 'config_type': self.config_type.value, - 'device_modifications': [mod.__dump__() for mod in self.device_modifications] + 'device_modifications': [mod.json() for mod in self.device_modifications] } @classmethod @@ -171,12 +171,12 @@ class Size: raise ValueError('Percent unit size must specify a total size') return self.total_size # type: ignore - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: return { 'value': self.value, 'unit': self.unit.name, - 'sector_size': self.sector_size.__dump__() if self.sector_size else None, - 'total_size': self._total_size.__dump__() if self._total_size else None + 'sector_size': self.sector_size.json() if self.sector_size else None, + 'total_size': self._total_size.json() if self._total_size else None } @classmethod @@ -457,7 +457,7 @@ class SubvolumeModification: return self.mountpoint == Path('/') return False - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: return { 'name': str(self.name), 'mountpoint': str(self.mountpoint), @@ -466,12 +466,7 @@ class SubvolumeModification: } def table_data(self) -> Dict[str, Any]: - return { - 'name': str(self.name), - 'mountpoint': str(self.mountpoint), - 'compress': self.compress, - 'nodatacow': self.nodatacow - } + return self.json() class DeviceGeometry: @@ -755,14 +750,14 @@ class PartitionModification: 'obj_id': self.obj_id, 'status': self.status.value, 'type': self.type.value, - 'start': self.start.__dump__(), - 'length': self.length.__dump__(), + 'start': self.start.json(), + 'length': self.length.json(), 'fs_type': self.fs_type.value if self.fs_type else '', 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, 'flags': [f.name for f in self.flags], 'dev_path': str(self.dev_path) if self.dev_path else None, - 'btrfs': [vol.__dump__() for vol in self.btrfs_subvols] + 'btrfs': [vol.json() for vol in self.btrfs_subvols] } def table_data(self) -> Dict[str, Any]: @@ -830,7 +825,7 @@ class DeviceModification: filtered = filter(lambda x: x.is_root(relative_path), self.partitions) return next(filtered, None) - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: """ Called when generating configuration files """ @@ -922,6 +917,13 @@ class Fido2Device: 'product': self.product } + def table_data(self) -> Dict[str, str]: + return { + 'Path': str(self.path), + 'Manufacturer': self.manufacturer, + 'Product': self.product + } + @classmethod def parse_arg(cls, arg: Dict[str, str]) -> 'Fido2Device': return Fido2Device( diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index e22e7eed..71981fb6 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -69,8 +69,6 @@ def jsonify(obj: Any, safe: bool = True) -> Any: # a dictionary representation of the object so that it can be # processed by the json library. return jsonify(obj.json(), safe) - if hasattr(obj, '__dump__'): - return obj.__dump__() if isinstance(obj, (datetime, date)): return obj.isoformat() if isinstance(obj, (list, set, tuple)): @@ -462,13 +460,13 @@ def run_custom_user_commands(commands :List[str], installation :Installer) -> No for index, command in enumerate(commands): script_path = f"/var/tmp/user-command.{index}.sh" chroot_path = f"{installation.target}/{script_path}" - + info(f'Executing custom command "{command}" ...') with open(chroot_path, "w") as user_script: user_script.write(command) - + SysCommand(f"arch-chroot {installation.target} bash {script_path}") - + os.unlink(chroot_path) diff --git a/archinstall/lib/models/audio_configuration.py b/archinstall/lib/models/audio_configuration.py index 3a4029db..88cd5d8e 100644 --- a/archinstall/lib/models/audio_configuration.py +++ b/archinstall/lib/models/audio_configuration.py @@ -24,7 +24,7 @@ class Audio(Enum): class AudioConfiguration: audio: Audio - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: return { 'audio': self.audio.value } diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index be9812a0..fa3f32c8 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -14,12 +14,12 @@ class Bootloader(Enum): Efistub = 'Efistub' Limine = 'Limine' - def json(self): + def json(self) -> str: return self.value - @classmethod - def values(cls) -> List[str]: - return [e.value for e in cls] + @staticmethod + def values() -> List[str]: + return [e.value for e in Bootloader] @classmethod def get_default(cls) -> Bootloader: diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index fac7bbef..1777df62 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -42,7 +42,7 @@ class Nic: 'dns': self.dns } - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: return { 'iface': self.iface, 'ip': self.ip, @@ -94,10 +94,10 @@ class NetworkConfiguration: type: NicType nics: List[Nic] = field(default_factory=list) - def __dump__(self) -> Dict[str, Any]: + def json(self) -> Dict[str, Any]: config: Dict[str, Any] = {'type': self.type.value} if self.nics: - config['nics'] = [n.__dump__() for n in self.nics] + config['nics'] = [n.json() for n in self.nics] return config diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 62a1ba27..945a6c4f 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -38,8 +38,6 @@ class FormattedOutput: raise ValueError('Unsupported formatting call') elif hasattr(o, 'table_data'): return o.table_data() - elif hasattr(o, 'json'): - return o.json() elif is_dataclass(o): return asdict(o) else: -- cgit v1.2.3-54-g00ecf From b141609990fa4f7305443ee6ea6fe8796604c539 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 24 Sep 2023 19:47:38 +1000 Subject: Fix 1669 | Refactor display of sizes in tables (#2100) * Use sector as default display * Display tables in sector size * Refactor size * Update * Update * fix flake8 --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/__init__.py | 1 + archinstall/lib/disk/device_model.py | 169 +++++++++++++++++++----------- archinstall/lib/disk/partitioning_menu.py | 39 ++++--- archinstall/lib/installer.py | 2 +- archinstall/lib/interactions/disk_conf.py | 55 ++++++---- examples/config-sample.json | 17 +-- examples/full_automated_installation.py | 15 +-- 7 files changed, 178 insertions(+), 120 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index cdc96373..24dafef5 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -14,6 +14,7 @@ from .device_model import ( PartitionTable, Unit, Size, + SectorSize, SubvolumeModification, DeviceGeometry, PartitionType, diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8bc41e0c..08861a63 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -93,7 +93,7 @@ class DiskLayoutConfiguration: status=ModificationStatus(partition['status']), fs_type=FilesystemType(partition['fs_type']), start=Size.parse_args(partition['start']), - length=Size.parse_args(partition['length']), + length=Size.parse_args(partition['size']), mount_options=partition['mount_options'], mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, dev_path=Path(partition['dev_path']) if partition['dev_path'] else None, @@ -138,80 +138,89 @@ class Unit(Enum): sectors = 'sectors' # size in sector - Percent = '%' # size in percentile - @staticmethod def get_all_units() -> List[str]: return [u.name for u in Unit] + @staticmethod + def get_si_units() -> List[Unit]: + return [u for u in Unit if 'i' not in u.name and u.name != 'sectors'] + @dataclass -class Size: +class SectorSize: value: int unit: Unit - sector_size: Optional[Size] = None # only required when unit is sector - total_size: Optional[Size] = None # required when operating on percentages def __post_init__(self): - if self.unit == Unit.sectors and self.sector_size is None: - raise ValueError('Sector size is required when unit is sectors') - elif self.unit == Unit.Percent: - if self.value < 0 or self.value > 100: - raise ValueError('Percentage must be between 0 and 100') - elif self.total_size is None: - raise ValueError('Total size is required when unit is percentage') + match self.unit: + case Unit.sectors: + raise ValueError('Unit type sector not allowed for SectorSize') - @property - def _total_size(self) -> Size: + @staticmethod + def default() -> SectorSize: + return SectorSize(512, Unit.B) + + def json(self) -> Dict[str, Any]: + return { + 'value': self.value, + 'unit': self.unit.name, + } + + @classmethod + def parse_args(cls, arg: Dict[str, Any]) -> SectorSize: + return SectorSize( + arg['value'], + Unit[arg['unit']] + ) + + def normalize(self) -> int: """ - Save method to get the total size, mainly to satisfy mypy - This shouldn't happen as the Size object fails instantiation on missing total size + will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent and self.total_size is None: - raise ValueError('Percent unit size must specify a total size') - return self.total_size # type: ignore + return int(self.value * self.unit.value) # type: ignore + + +@dataclass +class Size: + value: int + unit: Unit + sector_size: SectorSize + + def __post_init__(self): + if not isinstance(self.sector_size, SectorSize): + raise ValueError('sector size must be of type SectorSize') def json(self) -> Dict[str, Any]: return { 'value': self.value, 'unit': self.unit.name, - 'sector_size': self.sector_size.json() if self.sector_size else None, - 'total_size': self._total_size.json() if self._total_size else None + 'sector_size': self.sector_size.json() if self.sector_size else None } @classmethod def parse_args(cls, size_arg: Dict[str, Any]) -> Size: sector_size = size_arg['sector_size'] - total_size = size_arg['total_size'] return Size( size_arg['value'], Unit[size_arg['unit']], - Size.parse_args(sector_size) if sector_size else None, - Size.parse_args(total_size) if total_size else None + SectorSize.parse_args(sector_size), ) def convert( self, target_unit: Unit, - sector_size: Optional[Size] = None, - total_size: Optional[Size] = None + sector_size: Optional[SectorSize] = None ) -> Size: if target_unit == Unit.sectors and sector_size is None: raise ValueError('If target has unit sector, a sector size must be provided') - # not sure why we would ever wanna convert to percentages - if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing parameter total size to be able to convert to percentage') - if self.unit == target_unit: return self - elif self.unit == Unit.Percent: - amount = int(self._total_size._normalize() * (self.value / 100)) - return Size(amount, Unit.B) elif self.unit == Unit.sectors: norm = self._normalize() - return Size(norm, Unit.B).convert(target_unit, sector_size) + return Size(norm, Unit.B, self.sector_size).convert(target_unit, sector_size) else: if target_unit == Unit.sectors and sector_size is not None: norm = self._normalize() @@ -219,7 +228,7 @@ class Size: return Size(sectors, Unit.sectors, sector_size) else: value = int(self._normalize() / target_unit.value) # type: ignore - return Size(value, target_unit) + return Size(value, target_unit, self.sector_size) def as_text(self) -> str: return self.format_size( @@ -230,31 +239,45 @@ class Size: def format_size( self, target_unit: Unit, - sector_size: Optional[Size] = None, + sector_size: Optional[SectorSize] = None, include_unit: bool = True ) -> str: - if self.unit == Unit.Percent: - return f'{self.value}%' - else: - target_size = self.convert(target_unit, sector_size) - if include_unit: - return f'{target_size.value} {target_unit.name}' - return f'{target_size.value}' + target_size = self.convert(target_unit, sector_size) + + if include_unit: + return f'{target_size.value} {target_unit.name}' + return f'{target_size.value}' + + def format_highest(self, include_unit: bool = True) -> str: + si_units = Unit.get_si_units() + all_si_values = [self.convert(si) for si in si_units] + filtered = filter(lambda x: x.value >= 1, all_si_values) + + # we have to get the max by the unit value as we're interested + # in getting the value in the highest possible unit without floats + si_value = max(filtered, key=lambda x: x.unit.value) + + if include_unit: + return f'{si_value.value} {si_value.unit.name}' + return f'{si_value.value}' def _normalize(self) -> int: """ will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent: - return self.convert(Unit.B).value - elif self.unit == Unit.sectors and self.sector_size is not None: - return self.value * self.sector_size._normalize() + if self.unit == Unit.sectors and self.sector_size is not None: + return self.value * self.sector_size.normalize() return int(self.value * self.unit.value) # type: ignore def __sub__(self, other: Size) -> Size: src_norm = self._normalize() dest_norm = other._normalize() - return Size(abs(src_norm - dest_norm), Unit.B) + return Size(abs(src_norm - dest_norm), Unit.B, self.sector_size) + + def __add__(self, other: Size) -> Size: + src_norm = self._normalize() + dest_norm = other._normalize() + return Size(abs(src_norm + dest_norm), Unit.B, self.sector_size) def __lt__(self, other): return self._normalize() < other._normalize() @@ -296,14 +319,22 @@ class _PartitionInfo: mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) + @property + def sector_size(self) -> SectorSize: + sector_size = self.partition.geometry.device.sectorSize + return SectorSize(sector_size, Unit.B) + def table_data(self) -> Dict[str, Any]: + end = self.start + self.length + part_info = { 'Name': self.name, 'Type': self.type.value, 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), 'Path': str(self.path), - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'Flags': ', '.join([f.name for f in self.flags]) } @@ -327,10 +358,14 @@ class _PartitionInfo: start = Size( partition.geometry.start, Unit.sectors, - Size(partition.disk.device.sectorSize, Unit.B) + SectorSize(partition.disk.device.sectorSize, Unit.B) ) - length = Size(int(partition.getLength(unit='B')), Unit.B) + length = Size( + int(partition.getLength(unit='B')), + Unit.B, + SectorSize(partition.disk.device.sectorSize, Unit.B) + ) return _PartitionInfo( partition=partition, @@ -355,7 +390,7 @@ class _DeviceInfo: type: str total_size: Size free_space_regions: List[DeviceGeometry] - sector_size: Size + sector_size: SectorSize read_only: bool dirty: bool @@ -365,7 +400,7 @@ class _DeviceInfo: 'Model': self.model, 'Path': str(self.path), 'Type': self.type, - 'Size': self.total_size.format_size(Unit.MiB), + 'Size': self.total_size.format_highest(), 'Free space': int(total_free_space), 'Sector size': self.sector_size.value, 'Read only': self.read_only @@ -379,15 +414,17 @@ class _DeviceInfo: else: device_type = parted.devices[device.type] - sector_size = Size(device.sectorSize, Unit.B) + sector_size = SectorSize(device.sectorSize, Unit.B) free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] + sector_size = SectorSize(device.sectorSize, Unit.B) + return _DeviceInfo( model=device.model.strip(), path=Path(device.path), type=device_type, sector_size=sector_size, - total_size=Size(int(device.getLength(unit='B')), Unit.B), + total_size=Size(int(device.getLength(unit='B')), Unit.B, sector_size), free_space_regions=free_space, read_only=device.readOnly, dirty=device.dirty @@ -470,7 +507,7 @@ class SubvolumeModification: class DeviceGeometry: - def __init__(self, geometry: Geometry, sector_size: Size): + def __init__(self, geometry: Geometry, sector_size: SectorSize): self._geometry = geometry self._sector_size = sector_size @@ -498,7 +535,7 @@ class DeviceGeometry: 'Sector size': self._sector_size.value, 'Start (sector/B)': start_str, 'End (sector/B)': end_str, - 'Length (sectors/B)': length_str + 'Size (sectors/B)': length_str } @@ -751,7 +788,7 @@ class PartitionModification: 'status': self.status.value, 'type': self.type.value, 'start': self.start.json(), - 'length': self.length.json(), + 'size': self.length.json(), 'fs_type': self.fs_type.value if self.fs_type else '', 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, @@ -764,12 +801,15 @@ class PartitionModification: """ Called for displaying data in table format """ + end = self.start + self.length + part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'FS type': self.fs_type.value if self.fs_type else 'Unknown', 'Mountpoint': self.mountpoint if self.mountpoint else '', 'Mount options': ', '.join(self.mount_options), @@ -938,7 +978,7 @@ class LsblkInfo: name: str = '' path: Path = Path() pkname: str = '' - size: Size = field(default_factory=lambda: Size(0, Unit.B)) + size: Size = field(default_factory=lambda: Size(0, Unit.B, SectorSize.default())) log_sec: int = 0 pttype: str = '' ptuuid: str = '' @@ -1017,7 +1057,8 @@ class LsblkInfo: if isinstance(getattr(lsblk_info, data_field), Path): val = Path(blockdevice[lsblk_field]) elif isinstance(getattr(lsblk_info, data_field), Size): - val = Size(blockdevice[lsblk_field], Unit.B) + sector_size = SectorSize(blockdevice['log-sec'], Unit.B) + val = Size(blockdevice[lsblk_field], Unit.B, sector_size) else: val = blockdevice[lsblk_field] diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 549c7f34..c5263b82 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus, DeviceGeometry + ModificationStatus, DeviceGeometry, SectorSize from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -194,42 +194,47 @@ class PartitioningList(ListManager): def _validate_value( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, - value: str + text: str, + start: Optional[Size] ) -> Optional[Size]: - match = re.match(r'([0-9]+)([a-zA-Z|%]*)', value, re.I) + match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I) if match: - value, unit = match.groups() + str_value, unit = match.groups() - if unit == '%': - unit = Unit.Percent.name + if unit == '%' and start: + available = total_size - start + value = int(available.value * (int(str_value) / 100)) + unit = available.unit.name + else: + value = int(str_value) if unit and unit not in Unit.get_all_units(): return None unit = Unit[unit] if unit else Unit.sectors - return Size(int(value), unit, sector_size, total_size) + return Size(value, unit, sector_size) return None def _enter_size( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, prompt: str, - default: Size + default: Size, + start: Optional[Size], ) -> Size: while True: value = TextInput(prompt).run().strip() - size: Optional[Size] = None if not value: size = default else: - size = self._validate_value(sector_size, total_size, value) + size = self._validate_value(sector_size, total_size, value, start) if size: return size @@ -247,7 +252,7 @@ class PartitioningList(ListManager): total_bytes = device_info.total_size.format_size(Unit.B) prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n' - prompt += str(_('All entered values can be suffixed with a unit: B, KB, KiB, MB, MiB...')) + '\n' + prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n' prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n' print(prompt) @@ -260,13 +265,14 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, start_prompt, - default_start + default_start, + None ) if start_size.value == largest_free_area.start: end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size) else: - end_size = Size(100, Unit.Percent, total_size=device_info.total_size) + end_size = device_info.total_size # prompt until valid end sector was entered end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text()) @@ -274,7 +280,8 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, end_prompt, - end_size + end_size, + start_size ) return start_size, end_size diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 05eb5867..a238bb8f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -163,7 +163,7 @@ class Installer: lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount) if len(lsblk_info) > 0: - if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB): + if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()): raise DiskError( f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. ' f'Please resize it to at least 200MiB and re-run the installation.' diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 78e4cff4..8542ab75 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -170,13 +170,13 @@ def select_disk_config( return None -def _boot_partition() -> disk.PartitionModification: +def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: if SysInfo.has_uefi(): - start = disk.Size(1, disk.Unit.MiB) - size = disk.Size(512, disk.Unit.MiB) + start = disk.Size(1, disk.Unit.MiB, sector_size) + size = disk.Size(512, disk.Unit.MiB, sector_size) else: - start = disk.Size(3, disk.Unit.MiB) - size = disk.Size(203, disk.Unit.MiB) + start = disk.Size(3, disk.Unit.MiB, sector_size) + size = disk.Size(203, disk.Unit.MiB, sector_size) # boot partition return disk.PartitionModification( @@ -215,8 +215,9 @@ def suggest_single_disk_layout( if not filesystem_type: filesystem_type = select_main_filesystem_format(advanced_options) - min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) - root_partition_size = disk.Size(20, disk.Unit.GiB) + sector_size = device.device_info.sector_size + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size) + root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) using_subvolumes = False using_home_partition = False compression = False @@ -244,7 +245,7 @@ def suggest_single_disk_layout( # Also re-align the start to 1MiB since we don't need the first sectors # like we do in MBR layouts where the boot loader is installed traditionally. - boot_partition = _boot_partition() + boot_partition = _boot_partition(sector_size) device_modification.add_partition(boot_partition) if not using_subvolumes: @@ -259,11 +260,11 @@ def suggest_single_disk_layout( using_home_partition = False # root partition - start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + start = disk.Size(513, disk.Unit.MiB, sector_size) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB, sector_size) # Set a size for / (/root) if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + length = device.device_info.total_size - start else: length = min(device.device_info.total_size, root_partition_size) @@ -294,11 +295,14 @@ def suggest_single_disk_layout( # If we don't want to use subvolumes, # But we want to be able to re-use data between re-installs.. # A second partition for /home would be nice if we have the space for it + start = root_partition.length + length = device.device_info.total_size - root_partition.length + home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), fs_type=filesystem_type, mount_options=['compress=zstd'] if compression else [] @@ -319,9 +323,9 @@ def suggest_multi_disk_layout( # Not really a rock solid foundation of information to stand on, but it's a start: # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB) + min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) compression = False if not filesystem_type: @@ -362,28 +366,41 @@ def suggest_multi_disk_layout( root_device_modification = disk.DeviceModification(root_device, wipe=True) home_device_modification = disk.DeviceModification(home_device, wipe=True) + root_device_sector_size = root_device_modification.device.device_info.sector_size + home_device_sector_size = home_device_modification.device.device_info.sector_size + # add boot partition to the root device - boot_partition = _boot_partition() + boot_partition = _boot_partition(root_device_sector_size) root_device_modification.add_partition(boot_partition) + if SysInfo.has_uefi(): + root_start = disk.Size(513, disk.Unit.MiB, root_device_sector_size) + else: + root_start = disk.Size(206, disk.Unit.MiB, root_device_sector_size) + + root_length = root_device.device_info.total_size - root_start + # add root partition to the root device root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + start=root_start, + length=root_length, mountpoint=Path('/'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type ) root_device_modification.add_partition(root_partition) + start = disk.Size(1, disk.Unit.MiB, home_device_sector_size) + length = home_device.device_info.total_size - start + # add home partition to home device home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type, diff --git a/examples/config-sample.json b/examples/config-sample.json index ed1cc38e..d43f7ea6 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -17,9 +17,8 @@ "Boot" ], "fs_type": "fat32", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 512 }, @@ -28,7 +27,6 @@ "obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 1 }, @@ -39,9 +37,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, @@ -50,7 +47,6 @@ "obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 513 }, @@ -61,14 +57,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": { - "sector_size": null, - "total_size": null, - "unit": "B", - "value": 250148290560 - }, "unit": "Percent", "value": 100 }, @@ -77,7 +67,6 @@ "obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f", "start": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index 79e85348..d25575d4 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -23,8 +23,8 @@ device_modification = disk.DeviceModification(device, wipe=True) boot_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(512, disk.Unit.MiB), + start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size), mountpoint=Path('/boot'), fs_type=disk.FilesystemType.Fat32, flags=[disk.PartitionFlag.Boot] @@ -35,20 +35,23 @@ device_modification.add_partition(boot_partition) root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB), - length=disk.Size(20, disk.Unit.GiB), + start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size), mountpoint=None, fs_type=fs_type, mount_options=[], ) device_modification.add_partition(root_partition) +start_home = root_partition.length +length_home = device.device_info.total_size - start_home + # create a new home partition home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start_home, + length=length_home, mountpoint=Path('/home'), fs_type=fs_type, mount_options=[] -- cgit v1.2.3-54-g00ecf From 717a22371fd25aac10d1f435b65842e800fd9d7e Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 26 Sep 2023 04:57:45 -0400 Subject: Fix `mountpoint` for pre-mounted disk configuration (#2113) * Fix `mountpoint` for pre-mounted disk configuration * Add missing commas --- archinstall/lib/disk/device_handler.py | 4 +++- archinstall/lib/disk/device_model.py | 22 ++++++---------------- archinstall/lib/installer.py | 6 +++--- archinstall/lib/interactions/disk_conf.py | 1 - examples/auto_discovery_mounted.py | 1 - 5 files changed, 12 insertions(+), 22 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 9646103f..4cb35c03 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -594,7 +594,9 @@ class DeviceHandler(object): if is_subpath(mountpoint, base_mountpoint): path = Path(part_info.disk.device.path) part_mods.setdefault(path, []) - part_mods[path].append(PartitionModification.from_existing_partition(part_info)) + part_mod = PartitionModification.from_existing_partition(part_info) + part_mod.mountpoint = mountpoint.root / mountpoint.relative_to(base_mountpoint) + part_mods[path].append(part_mod) break device_mods: List[DeviceModification] = [] diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 08861a63..b1f012f7 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -42,12 +42,6 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) - # used for pre-mounted config - relative_mountpoint: Optional[Path] = None - - def __post_init__(self): - if self.config_type == DiskLayoutType.Pre_mount and self.relative_mountpoint is None: - raise ValueError('Must set a relative mountpoint when layout type is pre-mount"') def json(self) -> Dict[str, Any]: return { @@ -487,10 +481,8 @@ class SubvolumeModification: raise ValueError('Mountpoint is not specified') - def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: + def is_root(self) -> bool: if self.mountpoint: - if relative_mountpoint is not None: - return self.mountpoint.relative_to(relative_mountpoint) == Path('.') return self.mountpoint == Path('/') return False @@ -742,14 +734,12 @@ class PartitionModification: """ return any(set(self.flags) & set(self._boot_indicator_flags)) - def is_root(self, relative_mountpoint: Optional[Path] = None) -> bool: - if relative_mountpoint is not None and self.mountpoint is not None: - return self.mountpoint.relative_to(relative_mountpoint) == Path('.') - elif self.mountpoint is not None: + def is_root(self) -> bool: + if self.mountpoint is not None: return Path('/') == self.mountpoint else: for subvol in self.btrfs_subvols: - if subvol.is_root(relative_mountpoint): + if subvol.is_root(): return True return False @@ -861,8 +851,8 @@ class DeviceModification: filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) return next(filtered, None) - def get_root_partition(self, relative_path: Optional[Path]) -> Optional[PartitionModification]: - filtered = filter(lambda x: x.is_root(relative_path), self.partitions) + def get_root_partition(self) -> Optional[PartitionModification]: + filtered = filter(lambda x: x.is_root(), self.partitions) return next(filtered, None) def json(self) -> Dict[str, Any]: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 8a0acf64..f8f59cc0 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -716,7 +716,7 @@ class Installer: def _get_root_partition(self) -> Optional[disk.PartitionModification]: for mod in self._disk_config.device_modifications: - if root := mod.get_root_partition(self._disk_config.relative_mountpoint): + if root := mod.get_root_partition(): return root return None @@ -903,8 +903,8 @@ class Installer: add_options = [ '--target=x86_64-efi', - f'--efi-directory={efi_partition.mountpoint}' - f'--boot-directory={boot_partition.mountpoint if boot_partition else "/boot"}' + f'--efi-directory={efi_partition.mountpoint}', + f'--boot-directory={boot_partition.mountpoint if boot_partition else "/boot"}', '--bootloader-id=GRUB', '--removable' ] diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 8542ab75..253a623d 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -139,7 +139,6 @@ def select_disk_config( return disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Pre_mount, - relative_mountpoint=path, device_modifications=mods ) diff --git a/examples/auto_discovery_mounted.py b/examples/auto_discovery_mounted.py index 0bd30cd1..e3cb80b7 100644 --- a/examples/auto_discovery_mounted.py +++ b/examples/auto_discovery_mounted.py @@ -9,5 +9,4 @@ mods = disk.device_handler.detect_pre_mounted_mods(root_mount_dir) disk_config = disk.DiskLayoutConfiguration( disk.DiskLayoutType.Pre_mount, device_modifications=mods, - relative_mountpoint=Path('/mnt/archinstall') ) -- cgit v1.2.3-54-g00ecf From bc8d4ea2346b09adc641ae51648ac67df939d27a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 26 Sep 2023 18:57:50 +1000 Subject: Fix 2103 (#2114) Co-authored-by: Daniel Girtler --- archinstall/lib/disk/device_handler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 4cb35c03..8af33fde 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -49,12 +49,11 @@ class DeviceHandler(object): try: loop_devices = SysCommand(['losetup', '-a']) - except SysCallError as err: - debug(f'Failed to get loop devices: {err}') - else: for ld_info in str(loop_devices).splitlines(): loop_device = getDevice(ld_info.split(':', maxsplit=1)[0]) devices.append(loop_device) + except Exception as err: + debug(f'Failed to get loop devices: {err}') for device in devices: if get_lsblk_info(device.path).type == 'rom': -- cgit v1.2.3-54-g00ecf From a811decea5db5ce7c26e8e42cd49c233fa959fe1 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Wed, 27 Sep 2023 01:39:35 -0400 Subject: Fix mount debug message (#2116) --- archinstall/lib/disk/device_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 8af33fde..066d3295 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -553,7 +553,7 @@ class DeviceHandler(object): command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' - debug(f'Mounting {dev_path}: command') + debug(f'Mounting {dev_path}: {command}') try: SysCommand(command) -- cgit v1.2.3-54-g00ecf From 3695c37bc4c530cc381ccee86e3e190f50a4a416 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Thu, 28 Sep 2023 07:00:06 -0400 Subject: Fix mount command whitespace (#2126) --- archinstall/lib/disk/device_handler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 066d3295..f2835b7b 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -546,12 +546,16 @@ class DeviceHandler(object): info(f'Device already mounted at {target_mountpoint}') return - str_options = ','.join(options) - str_options = f'-o {str_options}' if str_options else '' + cmd = ['mount'] - mount_fs = f'-t {mount_fs}' if mount_fs else '' + if len(options): + cmd.extend(('-o', ','.join(options))) + if mount_fs: + cmd.extend(('-t', mount_fs)) - command = f'mount {mount_fs} {str_options} {dev_path} {target_mountpoint}' + cmd.extend((str(dev_path), str(target_mountpoint))) + + command = ' '.join(cmd) debug(f'Mounting {dev_path}: {command}') -- cgit v1.2.3-54-g00ecf From 9f5c2bb70b0a4551eaa871164a3c9d71c1e65086 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:09:28 -0400 Subject: Add support for ESP partition flag (#2133) --- archinstall/lib/disk/device_model.py | 18 +++++++++++------- archinstall/lib/disk/partitioning_menu.py | 7 +++++++ archinstall/lib/installer.py | 11 +++++++++-- archinstall/lib/interactions/disk_conf.py | 4 +++- 4 files changed, 30 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index b1f012f7..6992bccb 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -658,7 +658,8 @@ class PartitionModification: partuuid: Optional[str] = None uuid: Optional[str] = None - _boot_indicator_flags = [PartitionFlag.Boot, PartitionFlag.XBOOTLDR] + _efi_indicator_flags = (PartitionFlag.Boot, PartitionFlag.ESP) + _boot_indicator_flags = (PartitionFlag.Boot, PartitionFlag.XBOOTLDR) def __post_init__(self): # needed to use the object as a dictionary key due to hash func @@ -728,6 +729,13 @@ class PartitionModification: raise ValueError('Mountpoint is not specified') + def is_efi(self) -> bool: + return ( + any(set(self.flags) & set(self._efi_indicator_flags)) + and self.fs_type == FilesystemType.Fat32 + and PartitionFlag.XBOOTLDR not in self.flags + ) + def is_boot(self) -> bool: """ Returns True if any of the boot indicator flags are found in self.flags @@ -828,9 +836,8 @@ class DeviceModification: def get_efi_partition(self) -> Optional[PartitionModification]: """ Similar to get_boot_partition() but excludes XBOOTLDR partitions from it's candidates. - Also works with ESP flag. """ - filtered = filter(lambda x: (x.is_boot() or PartitionFlag.ESP in x.flags) and x.fs_type == FilesystemType.Fat32 and PartitionFlag.XBOOTLDR not in x.flags, self.partitions) + filtered = filter(lambda x: x.is_efi() and x.mountpoint, self.partitions) return next(filtered, None) def get_boot_partition(self) -> Optional[PartitionModification]: @@ -843,10 +850,7 @@ class DeviceModification: filtered = filter(lambda x: x.is_boot() and x != efi_partition and x.mountpoint, self.partitions) if boot_partition := next(filtered, None): return boot_partition - if efi_partition.is_boot(): - return efi_partition - else: - return None + return efi_partition else: filtered = filter(lambda x: x.is_boot() and x.mountpoint, self.partitions) return next(filtered, None) diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index c5263b82..a9478158 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -6,6 +6,7 @@ from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus, DeviceGeometry, SectorSize +from ..hardware import SysInfo from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -105,10 +106,14 @@ class PartitioningList(ListManager): entry.mountpoint = self._prompt_mountpoint() if entry.mountpoint == Path('/boot'): entry.set_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + entry.set_flag(PartitionFlag.ESP) case 'mark_formatting' if entry: self._prompt_formatting(entry) case 'mark_bootable' if entry: entry.invert_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + entry.invert_flag(PartitionFlag.ESP) case 'set_filesystem' if entry: fs_type = self._prompt_partition_fs_type() if fs_type: @@ -310,6 +315,8 @@ class PartitioningList(ListManager): if partition.mountpoint == Path('/boot'): partition.set_flag(PartitionFlag.Boot) + if SysInfo.has_uefi(): + partition.set_flag(PartitionFlag.ESP) return partition diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index e7895a1a..585389ed 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -903,15 +903,22 @@ class Installer: '--debug' ] - if SysInfo.has_uefi() and efi_partition is not None: + if SysInfo.has_uefi(): + if not efi_partition: + raise ValueError('Could not detect efi partition') + info(f"GRUB EFI partition: {efi_partition.dev_path}") self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + boot_dir_arg = [] + if boot_partition != efi_partition: + boot_dir_arg.append(f'--boot-directory={boot_dir}') + add_options = [ '--target=x86_64-efi', f'--efi-directory={efi_partition.mountpoint}', - f'--boot-directory={boot_dir}', + *boot_dir_arg, '--bootloader-id=GRUB', '--removable' ] diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 253a623d..84a3196c 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -170,9 +170,11 @@ def select_disk_config( def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: + flags = [disk.PartitionFlag.Boot] if SysInfo.has_uefi(): start = disk.Size(1, disk.Unit.MiB, sector_size) size = disk.Size(512, disk.Unit.MiB, sector_size) + flags.append(disk.PartitionFlag.ESP) else: start = disk.Size(3, disk.Unit.MiB, sector_size) size = disk.Size(203, disk.Unit.MiB, sector_size) @@ -185,7 +187,7 @@ def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: length=size, mountpoint=Path('/boot'), fs_type=disk.FilesystemType.Fat32, - flags=[disk.PartitionFlag.Boot] + flags=flags ) -- cgit v1.2.3-54-g00ecf From 8257e9f73f6d8f233731b5632863a5d7a4ae827e Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 2 Oct 2023 01:50:20 -0400 Subject: Fix missing info for pre-mounted disk configuration (#2143) --- archinstall/lib/disk/device_handler.py | 9 ++++++++- archinstall/lib/disk/device_model.py | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index f2835b7b..6927671d 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -83,7 +83,9 @@ class DeviceHandler(object): _PartitionInfo.from_partition( partition, fs_type, + lsblk_info.partn, lsblk_info.partuuid, + lsblk_info.uuid, lsblk_info.mountpoints, subvol_infos ) @@ -598,7 +600,12 @@ class DeviceHandler(object): path = Path(part_info.disk.device.path) part_mods.setdefault(path, []) part_mod = PartitionModification.from_existing_partition(part_info) - part_mod.mountpoint = mountpoint.root / mountpoint.relative_to(base_mountpoint) + if part_mod.mountpoint: + part_mod.mountpoint = mountpoint.root / mountpoint.relative_to(base_mountpoint) + else: + for subvol in part_mod.btrfs_subvols: + if sm := subvol.mountpoint: + subvol.mountpoint = sm.root / sm.relative_to(base_mountpoint) part_mods[path].append(part_mod) break diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 6992bccb..26169485 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -308,7 +308,9 @@ class _PartitionInfo: start: Size length: Size flags: List[PartitionFlag] + partn: int partuuid: str + uuid: str disk: Disk mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) @@ -342,7 +344,9 @@ class _PartitionInfo: cls, partition: Partition, fs_type: Optional[FilesystemType], + partn: int, partuuid: str, + uuid: str, mountpoints: List[Path], btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = [] ) -> _PartitionInfo: @@ -370,7 +374,9 @@ class _PartitionInfo: start=start, length=length, flags=flags, + partn=partn, partuuid=partuuid, + uuid=uuid, disk=partition.disk, mountpoints=mountpoints, btrfs_subvol_infos=btrfs_subvol_infos @@ -713,6 +719,9 @@ class PartitionModification: length=partition_info.length, fs_type=partition_info.fs_type, dev_path=partition_info.path, + partn=partition_info.partn, + partuuid=partition_info.partuuid, + uuid=partition_info.uuid, flags=partition_info.flags, mountpoint=mountpoint, btrfs_subvols=subvol_mods -- cgit v1.2.3-54-g00ecf From 5c903df55fac449baae1e9cc23b04f6beeb55364 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 2 Oct 2023 21:01:23 +1100 Subject: Simplify SysCommand decoding (#2121) --- archinstall/lib/disk/device_handler.py | 17 ++++----- archinstall/lib/disk/device_model.py | 11 +++--- archinstall/lib/disk/fido.py | 7 +--- archinstall/lib/general.py | 13 +++++-- archinstall/lib/installer.py | 23 +++++------ archinstall/lib/locale.py | 61 ----------------------------- archinstall/lib/locale/__init__.py | 12 ++++-- archinstall/lib/locale/locale.py | 61 ----------------------------- archinstall/lib/locale/locale_menu.py | 2 +- archinstall/lib/locale/utils.py | 70 ++++++++++++++++++++++++++++++++++ archinstall/lib/luks.py | 2 +- archinstall/lib/packages/packages.py | 2 +- 12 files changed, 117 insertions(+), 164 deletions(-) delete mode 100644 archinstall/lib/locale.py delete mode 100644 archinstall/lib/locale/locale.py create mode 100644 archinstall/lib/locale/utils.py (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 6927671d..baed2f6f 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -154,20 +154,19 @@ class DeviceHandler(object): mountpoint = Path(common_prefix) try: - result = SysCommand(f'btrfs subvolume list {mountpoint}') + result = SysCommand(f'btrfs subvolume list {mountpoint}').decode() except SysCallError as err: debug(f'Failed to read btrfs subvolume information: {err}') return subvol_infos try: - if decoded := result.decode('utf-8'): - # ID 256 gen 16 top level 5 path @ - for line in decoded.splitlines(): - # expected output format: - # ID 257 gen 8 top level 5 path @home - name = Path(line.split(' ')[-1]) - sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) - subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) + # ID 256 gen 16 top level 5 path @ + for line in result.splitlines(): + # expected output format: + # ID 257 gen 8 top level 5 path @home + name = Path(line.split(' ')[-1]) + sub_vol_mountpoint = lsblk_info.btrfs_subvol_info.get(name, None) + subvol_infos.append(_BtrfsSubvolumeInfo(name, sub_vol_mountpoint)) except json.decoder.JSONDecodeError as err: error(f"Could not decode lsblk JSON: {result}") raise err diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 26169485..4ac53b0c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -1111,12 +1111,12 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = for retry_attempt in range(retry): try: - result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}') + result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode() break except SysCallError as err: # Get the output minus the message/info from lsblk if it returns a non-zero exit code. if err.worker: - err_str = err.worker.decode('UTF-8') + err_str = err.worker.decode() debug(f'Error calling lsblk: {err_str}') else: raise err @@ -1127,10 +1127,9 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = time.sleep(1) try: - if decoded := result.decode('utf-8'): - block_devices = json.loads(decoded) - blockdevices = block_devices['blockdevices'] - return [LsblkInfo.from_json(device) for device in blockdevices] + block_devices = json.loads(result) + blockdevices = block_devices['blockdevices'] + return [LsblkInfo.from_json(device) for device in blockdevices] except json.decoder.JSONDecodeError as err: error(f"Could not decode lsblk JSON: {result}") raise err diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 9eeba56a..49904c17 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -2,7 +2,7 @@ from __future__ import annotations import getpass from pathlib import Path -from typing import List, Optional +from typing import List from .device_model import PartitionModification, Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes @@ -38,14 +38,11 @@ class Fido2: # down moving the cursor in the menu if not cls._loaded or reload: try: - ret: Optional[str] = SysCommand("systemd-cryptenroll --fido2-device=list").decode('UTF-8') + ret = SysCommand("systemd-cryptenroll --fido2-device=list").decode() except SysCallError: error('fido2 support is most likely not installed') raise ValueError('HSM devices can not be detected, is libfido2 installed?') - if not ret: - return [] - fido_devices: str = clear_vt100_escape_codes(ret) # type: ignore manufacturer_pos = 0 diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 71981fb6..3697cf2d 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -430,10 +430,15 @@ class SysCommand: return True - def decode(self, *args, **kwargs) -> Optional[str]: - if self.session: - return self.session._trace_log.decode(*args, **kwargs) - return None + def decode(self, encoding: str = 'utf-8', errors='backslashreplace', strip: bool = True) -> str: + if not self.session: + raise ValueError('No session available to decode') + + val = self.session._trace_log.decode(encoding, errors=errors) + + if strip: + return val.strip() + return val @property def exit_code(self) -> Optional[int]: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 585389ed..ad98d9a8 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -131,7 +131,7 @@ class Installer: We need to wait for it before we continue since we opted in to use a custom mirror/region. """ info('Waiting for time sync (systemd-timesyncd.service) to complete.') - while SysCommand('timedatectl show --property=NTPSynchronized --value').decode().rstrip() != 'yes': + while SysCommand('timedatectl show --property=NTPSynchronized --value').decode() != 'yes': time.sleep(1) info('Waiting for automatic mirror selection (reflector) to complete.') @@ -282,7 +282,7 @@ class Installer: if enable_resume: resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode('UTF-8').strip() - resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode('UTF-8').split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() + resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode().split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() self._hooks.append('resume') self._kernel_params.append(f'resume=UUID={resume_uuid}') @@ -312,9 +312,6 @@ class Installer: except SysCallError as err: raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n Error: {err}') - if not gen_fstab: - raise RequirementError(f'Generating fstab returned empty value') - with open(fstab_path, 'a') as fp: fp.write(gen_fstab) @@ -1318,17 +1315,21 @@ TIMEOUT=5 if os.path.splitext(service_name)[1] not in ('.service', '.target', '.timer'): service_name += '.service' # Just to be safe - last_execution_time = b''.join(SysCommand(f"systemctl show --property=ActiveEnterTimestamp --no-pager {service_name}", environment_vars={'SYSTEMD_COLORS': '0'})) - last_execution_time = last_execution_time.lstrip(b'ActiveEnterTimestamp=').strip() + last_execution_time = SysCommand( + f"systemctl show --property=ActiveEnterTimestamp --no-pager {service_name}", + environment_vars={'SYSTEMD_COLORS': '0'} + ).decode().lstrip('ActiveEnterTimestamp=') + if not last_execution_time: return None - return last_execution_time.decode('UTF-8') + return last_execution_time def _service_state(self, service_name: str) -> str: if os.path.splitext(service_name)[1] not in ('.service', '.target', '.timer'): service_name += '.service' # Just to be safe - state = b''.join(SysCommand(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS': '0'})) - - return state.strip().decode('UTF-8') + return SysCommand( + f'systemctl show --no-pager -p SubState --value {service_name}', + environment_vars={'SYSTEMD_COLORS': '0'} + ).decode() diff --git a/archinstall/lib/locale.py b/archinstall/lib/locale.py deleted file mode 100644 index ab158984..00000000 --- a/archinstall/lib/locale.py +++ /dev/null @@ -1,61 +0,0 @@ -from itertools import takewhile -from pathlib import Path -from typing import Iterator, List - -from .exceptions import ServiceException, SysCallError -from .general import SysCommand -from .output import error - - -def list_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def list_locales() -> List[str]: - entries = Path('/etc/locale.gen').read_text().splitlines() - # Before the list of locales begins there's an empty line with a '#' in front - # so we'll collect the locales from bottom up and halt when we're done. - locales = list(takewhile(bool, map(lambda entry: entry.strip('\n\t #'), reversed(entries)))) - locales.reverse() - return locales - - -def list_x11_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def verify_keyboard_layout(layout :str) -> bool: - for language in list_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def verify_x11_keyboard_layout(layout :str) -> bool: - for language in list_x11_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def set_keyboard_language(locale :str) -> bool: - if len(locale.strip()): - if not verify_keyboard_layout(locale): - error(f"Invalid keyboard locale specified: {locale}") - return False - - try: - SysCommand(f'localectl set-keymap {locale}') - except SysCallError as err: - raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") - - return True - - return False - - -def list_timezones() -> Iterator[str]: - for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale/__init__.py b/archinstall/lib/locale/__init__.py index 6c32d6f3..90f1aecc 100644 --- a/archinstall/lib/locale/__init__.py +++ b/archinstall/lib/locale/__init__.py @@ -1,6 +1,10 @@ from .locale_menu import LocaleConfiguration -from .locale import ( - list_keyboard_languages, list_locales, list_x11_keyboard_languages, - verify_keyboard_layout, verify_x11_keyboard_layout, set_kb_layout, - list_timezones +from .utils import ( + list_keyboard_languages, + list_locales, + list_x11_keyboard_languages, + verify_keyboard_layout, + verify_x11_keyboard_layout, + list_timezones, + set_kb_layout ) diff --git a/archinstall/lib/locale/locale.py b/archinstall/lib/locale/locale.py deleted file mode 100644 index 90f20cc6..00000000 --- a/archinstall/lib/locale/locale.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Iterator, List - -from ..exceptions import ServiceException, SysCallError -from ..general import SysCommand -from ..output import error - - -def list_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def list_locales() -> List[str]: - locales = [] - - with open('/usr/share/i18n/SUPPORTED') as file: - for line in file: - if line != 'C.UTF-8 UTF-8\n': - locales.append(line.rstrip()) - - return locales - - -def list_x11_keyboard_languages() -> Iterator[str]: - for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() - - -def verify_keyboard_layout(layout :str) -> bool: - for language in list_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def verify_x11_keyboard_layout(layout :str) -> bool: - for language in list_x11_keyboard_languages(): - if layout.lower() == language.lower(): - return True - return False - - -def set_kb_layout(locale :str) -> bool: - if len(locale.strip()): - if not verify_keyboard_layout(locale): - error(f"Invalid keyboard locale specified: {locale}") - return False - - try: - SysCommand(f'localectl set-keymap {locale}') - except SysCallError as err: - raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") - - return True - - return False - - -def list_timezones() -> Iterator[str]: - for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): - yield line.decode('UTF-8').strip() diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py index 2e254315..729b3b6e 100644 --- a/archinstall/lib/locale/locale_menu.py +++ b/archinstall/lib/locale/locale_menu.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict, Any, TYPE_CHECKING, Optional -from .locale import set_kb_layout, list_keyboard_languages, list_locales +from .utils import list_keyboard_languages, list_locales, set_kb_layout from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu if TYPE_CHECKING: diff --git a/archinstall/lib/locale/utils.py b/archinstall/lib/locale/utils.py new file mode 100644 index 00000000..330ca0ce --- /dev/null +++ b/archinstall/lib/locale/utils.py @@ -0,0 +1,70 @@ +from typing import Iterator, List + +from ..exceptions import ServiceException, SysCallError +from ..general import SysCommand +from ..output import error + + +def list_keyboard_languages() -> Iterator[str]: + for line in SysCommand( + "localectl --no-pager list-keymaps", + environment_vars={'SYSTEMD_COLORS': '0'} + ).decode(): + yield line + + +def list_locales() -> List[str]: + locales = [] + + with open('/usr/share/i18n/SUPPORTED') as file: + for line in file: + if line != 'C.UTF-8 UTF-8\n': + locales.append(line.rstrip()) + + return locales + + +def list_x11_keyboard_languages() -> Iterator[str]: + for line in SysCommand( + "localectl --no-pager list-x11-keymap-layouts", + environment_vars={'SYSTEMD_COLORS': '0'} + ).decode(): + yield line + + +def verify_keyboard_layout(layout :str) -> bool: + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def verify_x11_keyboard_layout(layout :str) -> bool: + for language in list_x11_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + + +def set_kb_layout(locale :str) -> bool: + if len(locale.strip()): + if not verify_keyboard_layout(locale): + error(f"Invalid keyboard locale specified: {locale}") + return False + + try: + SysCommand(f'localectl set-keymap {locale}') + except SysCallError as err: + raise ServiceException(f"Unable to set locale '{locale}' for console: {err}") + + return True + + return False + + +def list_timezones() -> Iterator[str]: + for line in SysCommand( + "timedatectl --no-pager list-timezones", + environment_vars={'SYSTEMD_COLORS': '0'} + ).decode(): + yield line diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 84b0e6fd..ea09ae7c 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -116,7 +116,7 @@ class Luks2: command = f'/usr/bin/cryptsetup luksUUID {self.luks_dev_path}' try: - return SysCommand(command).decode().strip() # type: ignore + return SysCommand(command).decode() except SysCallError as err: info(f'Unable to get UUID for Luks device: {self.luks_dev_path}') raise err diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index b71b0ce8..7491df07 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -37,7 +37,7 @@ def group_search(name :str) -> List[PackageSearchResult]: raise err # Just to be sure some code didn't slip through the exception - data = response.read().decode('UTF-8') + data = response.read().decode('utf-8') return [PackageSearchResult(**package) for package in json.loads(data)['results']] -- cgit v1.2.3-54-g00ecf From edbc13590366e93bb8a85eacf104d5613bc5793a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 3 Oct 2023 18:31:17 +1100 Subject: Extend the mypy checks (#2120) * Extend the mypy checks * Update * Update * Update --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 4 ++-- archinstall/lib/disk/device_model.py | 12 ++++++------ archinstall/lib/disk/encryption_menu.py | 7 ++++--- archinstall/lib/global_menu.py | 11 +++++++---- archinstall/lib/installer.py | 14 ++++++++++---- archinstall/lib/menu/abstract_menu.py | 2 +- archinstall/lib/models/gen.py | 20 +++++++++++++++++--- archinstall/lib/packages/packages.py | 4 ++-- archinstall/lib/profile/profiles_handler.py | 6 +++--- archinstall/lib/translationhandler.py | 2 +- pyproject.toml | 2 +- 11 files changed, 54 insertions(+), 30 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 07b85f96..11b47c48 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,8 +225,8 @@ def load_config(): if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) - if arguments.get('network_config', None) is not None: - config = NetworkConfiguration.parse_arg(arguments.get('network_config')) + if (net_config := arguments.get('network_config', None)) is not None: + config = NetworkConfiguration.parse_arg(net_config) arguments['network_config'] = config if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 4ac53b0c..cd955851 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -308,9 +308,9 @@ class _PartitionInfo: start: Size length: Size flags: List[PartitionFlag] - partn: int - partuuid: str - uuid: str + partn: Optional[int] + partuuid: Optional[str] + uuid: Optional[str] disk: Disk mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) @@ -344,9 +344,9 @@ class _PartitionInfo: cls, partition: Partition, fs_type: Optional[FilesystemType], - partn: int, - partuuid: str, - uuid: str, + partn: Optional[int], + partuuid: Optional[str], + uuid: Optional[str], mountpoints: List[Path], btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = [] ) -> _PartitionInfo: diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 234e3b03..c3a1c32f 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -3,6 +3,7 @@ from typing import Dict, Optional, Any, TYPE_CHECKING, List from ..disk import ( DeviceModification, + DiskLayoutConfiguration, PartitionModification, DiskEncryption, EncryptionType @@ -26,7 +27,7 @@ if TYPE_CHECKING: class DiskEncryptionMenu(AbstractSubMenu): def __init__( self, - mods: List[DeviceModification], + disk_config: DiskLayoutConfiguration, data_store: Dict[str, Any], preset: Optional[DiskEncryption] = None ): @@ -35,7 +36,7 @@ class DiskEncryptionMenu(AbstractSubMenu): else: self._preset = DiskEncryption() - self._modifications = mods + self._disk_config = disk_config super().__init__(data_store=data_store) def setup_selection_menu_options(self): @@ -59,7 +60,7 @@ class DiskEncryptionMenu(AbstractSubMenu): self._menu_options['partitions'] = \ Selector( _('Partitions'), - func=lambda preset: select_partitions_to_encrypt(self._modifications.device_modifications, preset), + func=lambda preset: select_partitions_to_encrypt(self._disk_config.device_modifications, preset), display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None, dependencies=['encryption_password'], default=self._preset.partitions, diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index b38dac0b..deda2ef6 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -176,8 +176,11 @@ class GlobalMenu(AbstractMenu): self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) def _missing_configs(self) -> List[str]: - def check(s): - return self._menu_options.get(s).has_selection() + def check(s) -> bool: + obj = self._menu_options.get(s) + if obj and obj.has_selection(): + return True + return False def has_superuser() -> bool: sel = self._menu_options['!users'] @@ -228,7 +231,7 @@ class GlobalMenu(AbstractMenu): return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: - mods: Optional[List[disk.DeviceModification]] = self._menu_options['disk_config'].current_selection + mods: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection if not mods: # this should not happen as the encryption menu has the disk_config as dependency @@ -263,7 +266,7 @@ class GlobalMenu(AbstractMenu): def _prev_additional_pkgs(self): selector = self._menu_options['packages'] - if selector.has_selection(): + if selector.current_selection: packages: List[str] = selector.current_selection return format_cols(packages, None) return None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ad98d9a8..8e716d3d 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -131,7 +131,11 @@ class Installer: We need to wait for it before we continue since we opted in to use a custom mirror/region. """ info('Waiting for time sync (systemd-timesyncd.service) to complete.') - while SysCommand('timedatectl show --property=NTPSynchronized --value').decode() != 'yes': + + while True: + time_val = SysCommand('timedatectl show --property=NTPSynchronized --value').decode() + if time_val and time_val.strip() == 'yes': + break time.sleep(1) info('Waiting for automatic mirror selection (reflector) to complete.') @@ -237,7 +241,7 @@ class Installer: gen_enc_file = self._disk_encryption.should_generate_encryption_file(part_mod) luks_handler = Luks2( - part_mod.dev_path, + part_mod.safe_dev_path, mapper_name=part_mod.mapper_name, password=self._disk_encryption.encryption_password ) @@ -281,8 +285,10 @@ class Installer: self._fstab_entries.append(f'{file} none swap defaults 0 0') if enable_resume: - resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode('UTF-8').strip() - resume_offset = SysCommand(f'/usr/bin/filefrag -v {self.target}{file}').decode().split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() + resume_uuid = SysCommand(f'findmnt -no UUID -T {self.target}{file}').decode() + resume_offset = SysCommand( + f'/usr/bin/filefrag -v {self.target}{file}' + ).decode().split('0:', 1)[1].split(":", 1)[1].split("..", 1)[0].strip() self._hooks.append('resume') self._kernel_params.append(f'resume=UUID={resume_uuid}') diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 306c500a..053f3c30 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -14,7 +14,7 @@ class Selector: def __init__( self, description: str, - func: Optional[Callable[[str], Any]] = None, + func: Optional[Callable[[Any], Any]] = None, display_func: Optional[Callable] = None, default: Optional[Any] = None, enabled: bool = False, diff --git a/archinstall/lib/models/gen.py b/archinstall/lib/models/gen.py index cc8d7605..fb7e5751 100644 --- a/archinstall/lib/models/gen.py +++ b/archinstall/lib/models/gen.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional, List +from typing import Optional, List, Dict, Any @dataclass @@ -87,6 +87,10 @@ class PackageSearchResult: makedepends: List[str] checkdepends: List[str] + @staticmethod + def from_json(data: Dict[str, Any]) -> 'PackageSearchResult': + return PackageSearchResult(**data) + @property def pkg_version(self) -> str: return self.pkgver @@ -107,8 +111,18 @@ class PackageSearch: page: int results: List[PackageSearchResult] - def __post_init__(self): - self.results = [PackageSearchResult(**x) for x in self.results] + @staticmethod + def from_json(data: Dict[str, Any]) -> 'PackageSearch': + results = [PackageSearchResult.from_json(r) for r in data['results']] + + return PackageSearch( + version=data['version'], + limit=data['limit'], + valid=data['valid'], + num_pages=data['num_pages'], + page=data['page'], + results=results + ) @dataclass diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index 7491df07..e495b03f 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -55,8 +55,8 @@ def package_search(package :str) -> PackageSearch: raise PackageError(f"Could not locate package: [{response.code}] {response}") data = response.read().decode('UTF-8') - - return PackageSearch(**json.loads(data)) + json_data = json.loads(data) + return PackageSearch.from_json(json_data) def find_package(package :str) -> List[PackageSearchResult]: diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 03039321..515cdfe9 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -138,16 +138,16 @@ class ProfileHandler: profiles = [profiles] for profile in profiles: - self._profiles.append(profile) + self.profiles.append(profile) - self._verify_unique_profile_names(self._profiles) + self._verify_unique_profile_names(self.profiles) def remove_custom_profiles(self, profiles: Union[TProfile, List[TProfile]]): if not isinstance(profiles, list): profiles = [profiles] remove_names = [p.name for p in profiles] - self._profiles = [p for p in self._profiles if p.name not in remove_names] + self._profiles = [p for p in self.profiles if p.name not in remove_names] def get_profile_by_name(self, name: str) -> Optional[Profile]: return next(filter(lambda x: x.name == name, self.profiles), None) # type: ignore diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index 33230562..3ea4c70e 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -206,4 +206,4 @@ class DeferredTranslation: @classmethod def install(cls): import builtins - builtins._ = cls + builtins._ = cls # type: ignore diff --git a/pyproject.toml b/pyproject.toml index 36ee0492..445aa277 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ packages = ["archinstall"] python_version = "3.11" files = "archinstall/" exclude = "tests" -#check_untyped_defs=true +check_untyped_defs=true [tool.bandit] targets = ["archinstall"] -- cgit v1.2.3-54-g00ecf From 5b6cab27f10bffa343eeff573b2bf90262da634b Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:57:59 -0400 Subject: Fix logic error in `_fetch_lsblk_info()` loop (#2196) --- archinstall/lib/disk/device_model.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index cd955851..15e68116 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -1106,10 +1106,7 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = if not dev_path: dev_path = '' - if retry == 0: - retry = 1 - - for retry_attempt in range(retry): + for retry_attempt in range(retry + 1): try: result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode() break @@ -1121,7 +1118,7 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = else: raise err - if retry_attempt == retry - 1: + if retry_attempt == retry: raise err time.sleep(1) -- cgit v1.2.3-54-g00ecf From 30a374a65b84c2d7dfbb13a4643fb27f31bc71e2 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 20 Nov 2023 06:54:04 -0500 Subject: Fix parsing pre-mounted disk configuration from configuration file (#2221) --- archinstall/lib/disk/device_model.py | 31 +++++++++++++++++++++++++++---- archinstall/lib/interactions/disk_conf.py | 3 ++- 2 files changed, 29 insertions(+), 5 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 15e68116..54b4932b 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -42,12 +42,20 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) + # used for pre-mounted config + mountpoint: Optional[Path] = None def json(self) -> Dict[str, Any]: - return { - 'config_type': self.config_type.value, - 'device_modifications': [mod.json() for mod in self.device_modifications] - } + if self.config_type == DiskLayoutType.Pre_mount: + return { + 'config_type': self.config_type.value, + 'mountpoint': str(self.mountpoint) + } + else: + return { + 'config_type': self.config_type.value, + 'device_modifications': [mod.json() for mod in self.device_modifications] + } @classmethod def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: @@ -64,6 +72,21 @@ class DiskLayoutConfiguration: device_modifications=device_modifications ) + if config_type == DiskLayoutType.Pre_mount.value: + if not (mountpoint := disk_config.get('mountpoint')): + raise ValueError('Must set a mountpoint when layout type is pre-mount') + + path = Path(str(mountpoint)) + + mods = device_handler.detect_pre_mounted_mods(path) + device_modifications.extend(mods) + + storage['MOUNT_POINT'] = path + + config.mountpoint = path + + return config + for entry in disk_config.get('device_modifications', []): device_path = Path(entry.get('device', None)) if entry.get('device', None) else None diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index bf24a22c..8e9643df 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -145,7 +145,8 @@ def select_disk_config( return disk.DiskLayoutConfiguration( config_type=disk.DiskLayoutType.Pre_mount, - device_modifications=mods + device_modifications=mods, + mountpoint=path ) preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] -- cgit v1.2.3-54-g00ecf From f876ddc68eedb57f315b345aa45703b7acaabc98 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 20 Nov 2023 06:56:12 -0500 Subject: Remove superfluous use of `partprobe` (#2212) --- archinstall/lib/disk/device_handler.py | 5 ----- archinstall/lib/luks.py | 1 - 2 files changed, 6 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index baed2f6f..50e8c59c 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -378,7 +378,6 @@ class DeviceHandler(object): attempts = 3 lsblk_info: Optional[LsblkInfo] = None - self.partprobe(path) for attempt_nr in range(attempts): time.sleep(attempt_nr + 1) lsblk_info = get_lsblk_info(path) @@ -386,8 +385,6 @@ class DeviceHandler(object): if lsblk_info.partn and lsblk_info.partuuid and lsblk_info.uuid: break - self.partprobe(path) - if not lsblk_info: debug(f'Unable to get partition information: {path}') raise DiskError(f'Unable to get partition information: {path}') @@ -526,8 +523,6 @@ class DeviceHandler(object): requires_delete = modification.wipe is False self._perform_partitioning(part_mod, modification.device, disk, requires_delete=requires_delete) - self.partprobe(modification.device.device_info.path) - def mount( self, dev_path: Path, diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 4b3b3bc7..c917420e 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -162,7 +162,6 @@ class Luks2: # Get crypt-information about the device by doing a reverse lookup starting with the partition path # For instance: /dev/sda - disk.device_handler.partprobe(self.luks_dev_path) lsblk_info = disk.get_lsblk_info(self.luks_dev_path) # For each child (sub-partition/sub-device) -- cgit v1.2.3-54-g00ecf From e6344f93f7e476d05bbcd642f2ed91fdde545870 Mon Sep 17 00:00:00 2001 From: czapek <32851089+48cf@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:19:17 +0100 Subject: Fix Limine bootloader deployment (#2216) * Add `get_unique_path_for_device` to `DeviceHandler` * Fix Limine bootloader deployment * Fail if UKI is enabled with Limine * Support more configuration options with Limine * Fix linter errors * Fix boot partition fs_type check for Limine --- archinstall/lib/disk/device_handler.py | 14 ++++ archinstall/lib/global_menu.py | 7 +- archinstall/lib/installer.py | 135 +++++++++++++++------------------ 3 files changed, 81 insertions(+), 75 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 50e8c59c..fcf52013 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -133,6 +133,20 @@ class DeviceHandler(object): lsblk = get_lsblk_info(dev_path) return Path(f'/dev/{lsblk.pkname}') + def get_unique_path_for_device(self, dev_path: Path) -> Optional[Path]: + paths = Path('/dev/disk/by-id').glob('*') + linked_targets = {p.resolve(): p for p in paths} + linked_wwn_targets = {p: linked_targets[p] for p in linked_targets + if p.name.startswith('wwn-') or p.name.startswith('nvme-eui.')} + + if dev_path in linked_wwn_targets: + return linked_wwn_targets[dev_path] + + if dev_path in linked_targets: + return linked_targets[dev_path] + + return None + def get_uuid_for_path(self, path: Path) -> Optional[str]: partition = self.find_partition(path) return partition.partuuid if partition else None diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index e4aa1235..d3d87603 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -363,8 +363,11 @@ class GlobalMenu(AbstractMenu): if boot_partition is None: return "Boot partition not found" - if bootloader == Bootloader.Limine and boot_partition.fs_type == disk.FilesystemType.Btrfs: - return "Limine bootloader does not support booting from BTRFS filesystem" + if bootloader == Bootloader.Limine: + if boot_partition.fs_type != disk.FilesystemType.Fat32: + return "Limine does not support booting from filesystems other than FAT32" + elif self._menu_options['uki'].current_selection: + return "Limine does not support booting UKIs" return None diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 2aa0d9dd..e2ca5e2b 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -972,70 +972,54 @@ class Installer: def _add_limine_bootloader( self, boot_partition: disk.PartitionModification, + efi_partition: Optional[disk.PartitionModification], root_partition: disk.PartitionModification ): self.pacman.strap('limine') - info(f"Limine boot partition: {boot_partition.dev_path}") - root_uuid = root_partition.uuid + info(f"Limine boot partition: {boot_partition.dev_path}") - def create_pacman_hook(contents: str): - HOOK_DIR = "/etc/pacman.d/hooks" - SysCommand(f"/usr/bin/arch-chroot {self.target} mkdir -p {HOOK_DIR}") - SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{contents}' > {HOOK_DIR}/liminedeploy.hook\"") + limine_path = self.target / 'usr' / 'share' / 'limine' + hook_command = None if SysInfo.has_uefi(): + if not efi_partition: + raise ValueError('Could not detect efi partition') + elif not efi_partition.mountpoint: + raise ValueError('EFI partition is not mounted') + + info(f"Limine EFI partition: {efi_partition.dev_path}") + try: - # The `limine.sys` file, contains stage 3 code. - cmd = f'/usr/bin/arch-chroot' \ - f' {self.target}' \ - f' cp' \ - f' /usr/share/limine/BOOTX64.EFI' \ - f' /boot/EFI/BOOT/' - - SysCommand(cmd, peek_output=True) - except SysCallError as err: - raise DiskError(f"Failed to install Limine BOOTX64.EFI on {boot_partition.dev_path}: {err}") + efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI' / 'BOOT' + efi_dir_path.mkdir(parents=True, exist_ok=True) - # Create the EFI limine pacman hook. - create_pacman_hook(""" -[Trigger] -Operation = Install -Operation = Upgrade -Type = Package -Target = limine + for file in ('BOOTIA32.EFI', 'BOOTX64.EFI'): + shutil.copy(limine_path / file, efi_dir_path) + except Exception as err: + raise DiskError(f'Failed to install Limine in {self.target}{efi_partition.mountpoint}: {err}') -[Action] -Description = Deploying Limine after upgrade... -When = PostTransaction -Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/BOOT/ - """) + hook_command = f'/usr/bin/cp /usr/share/limine/BOOTIA32.EFI {efi_partition.mountpoint}/EFI/BOOT/' \ + f' && /usr/bin/cp /usr/share/limine/BOOTX64.EFI {efi_partition.mountpoint}/EFI/BOOT/' else: parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path) - try: - # The `limine.sys` file, contains stage 3 code. - cmd = f'/usr/bin/arch-chroot' \ - f' {self.target}' \ - f' cp' \ - f' /usr/share/limine/limine-bios.sys' \ - f' /boot/limine-bios.sys' + if unique_path := disk.device_handler.get_unique_path_for_device(parent_dev_path): + parent_dev_path = unique_path - SysCommand(cmd, peek_output=True) + try: + # The `limine-bios.sys` file contains stage 3 code. + shutil.copy(limine_path / 'limine-bios.sys', self.target / 'boot') # `limine bios-install` deploys the stage 1 and 2 to the disk. - cmd = f'/usr/bin/arch-chroot' \ - f' {self.target}' \ - f' limine' \ - f' bios-install' \ - f' {parent_dev_path}' + SysCommand(f'/usr/bin/arch-chroot {self.target} limine bios-install {parent_dev_path}', peek_output=True) + except Exception as err: + raise DiskError(f'Failed to install Limine on {parent_dev_path}: {err}') - SysCommand(cmd, peek_output=True) - except SysCallError as err: - raise DiskError(f"Failed to install Limine on {boot_partition.dev_path}: {err}") + hook_command = f'/usr/bin/limine bios-install {parent_dev_path}' \ + f' && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/' - create_pacman_hook(f""" -[Trigger] + hook_contents = f'''[Trigger] Operation = Install Operation = Upgrade Type = Package @@ -1044,33 +1028,38 @@ Target = limine [Action] Description = Deploying Limine after upgrade... When = PostTransaction -# XXX: Kernel name descriptors cannot be used since they are not persistent and -# can change after each boot. -Exec = /bin/sh -c \\"/usr/bin/limine bios-install /dev/disk/by-uuid/{root_uuid} && /usr/bin/cp /usr/share/limine/limine-bios.sys /boot/\\" - """) +Exec = /bin/sh -c "{hook_command}" +''' - # Limine does not ship with a default configuration file. We are going to - # create a basic one that is similar to the one GRUB generates. - try: - config = f""" -TIMEOUT=5 - -:Arch Linux - PROTOCOL=linux - KERNEL_PATH=boot:///vmlinuz-linux - CMDLINE=root=UUID={root_uuid} rw rootfstype={root_partition.safe_fs_type.value} loglevel=3 - MODULE_PATH=boot:///initramfs-linux.img - -:Arch Linux (fallback) - PROTOCOL=linux - KERNEL_PATH=boot:///vmlinuz-linux - CMDLINE=root=UUID={root_uuid} rw rootfstype={root_partition.safe_fs_type.value} loglevel=3 - MODULE_PATH=boot:///initramfs-linux-fallback.img - """ - - SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{config}' > /boot/limine.cfg\"") - except SysCallError as err: - raise DiskError(f"Could not configure Limine: {err}") + hooks_dir = self.target / 'etc' / 'pacman.d' / 'hooks' + hooks_dir.mkdir(parents=True, exist_ok=True) + + hook_path = hooks_dir / '99-limine.hook' + hook_path.write_text(hook_contents) + + microcode = [] + + if ucode := self._get_microcode(): + microcode = [f'MODULE_PATH=boot:///{ucode}'] + + kernel_params = ' '.join(self._get_kernel_params(root_partition)) + config_contents = 'TIMEOUT=5\n' + + for kernel in self.kernels: + for variant in ('', '-fallback'): + entry = [ + f'PROTOCOL=linux', + f'KERNEL_PATH=boot:///vmlinuz-{kernel}', + *microcode, + f'MODULE_PATH=boot:///initramfs-{kernel}{variant}.img', + f'CMDLINE={kernel_params}', + ] + + config_contents += f'\n:Arch Linux ({kernel}{variant})\n' + config_contents += '\n'.join([f' {it}' for it in entry]) + '\n' + + config_path = self.target / 'boot' / 'limine.cfg' + config_path.write_text(config_contents) self.helper_flags['bootloader'] = "limine" @@ -1227,7 +1216,7 @@ TIMEOUT=5 case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled) case Bootloader.Limine: - self._add_limine_bootloader(boot_partition, root_partition) + self._add_limine_bootloader(boot_partition, efi_partition, root_partition) def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: return self.pacman.strap(packages) -- cgit v1.2.3-54-g00ecf From b39e3dc88637732df5356cf503436531cccd24d8 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:49:14 -0500 Subject: Fix partition table commit (#2294) --- archinstall/lib/disk/device_handler.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index fcf52013..5b97f25d 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -322,7 +322,7 @@ class DeviceHandler(object): part_mod.partuuid = lsblk_info.partuuid part_mod.uuid = lsblk_info.uuid - def _perform_partitioning( + def _setup_partition( self, part_mod: PartitionModification, block_device: BDevice, @@ -339,7 +339,6 @@ class DeviceHandler(object): raise DiskError(f'No partition for dev path found: {part_mod.safe_dev_path}') disk.deletePartition(part_info.partition) - disk.commit() if part_mod.status == ModificationStatus.Delete: return @@ -378,16 +377,12 @@ class DeviceHandler(object): try: disk.addPartition(partition=partition, constraint=disk.device.optimalAlignedConstraint) - disk.commit() - - # the creation will take a bit of time - time.sleep(3) - - # the partition has a real path now as it was created - part_mod.dev_path = Path(partition.path) except PartitionException as ex: raise DiskError(f'Unable to add partition, most likely due to overlapping sectors: {ex}') from ex + # the partition has a path now that it has been added + part_mod.dev_path = Path(partition.path) + def _fetch_part_info(self, path: Path) -> LsblkInfo: attempts = 3 lsblk_info: Optional[LsblkInfo] = None @@ -535,7 +530,9 @@ class DeviceHandler(object): # if the entire disk got nuked then we don't have to delete # any existing partitions anymore because they're all gone already requires_delete = modification.wipe is False - self._perform_partitioning(part_mod, modification.device, disk, requires_delete=requires_delete) + self._setup_partition(part_mod, modification.device, disk, requires_delete=requires_delete) + + disk.commit() def mount( self, -- cgit v1.2.3-54-g00ecf From 21dd295259e26580faa259dfe32d8e9554ae8933 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 8 Mar 2024 00:42:25 +1100 Subject: Fix 2337 (and similar) - Ignore existing partitions (#2342) * Fix 2337 * Update --- archinstall/lib/disk/device_handler.py | 8 ++++---- archinstall/lib/disk/device_model.py | 3 +++ archinstall/lib/disk/filesystem.py | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 5b97f25d..59ee150d 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -296,15 +296,15 @@ class DeviceHandler(object): the formatting functionality and in essence the support for the given filesystem. """ - # don't touch existing partitions - filtered_part = [p for p in device_mod.partitions if not p.exists()] + # only verify partitions that are being created or modified + create_or_modify_parts = [p for p in device_mod.partitions if p.is_create_or_modify()] - self._validate_partitions(filtered_part) + self._validate_partitions(create_or_modify_parts) # make sure all devices are unmounted self._umount_all_existing(device_mod.device_path) - for part_mod in filtered_part: + for part_mod in create_or_modify_parts: # partition will be encrypted if enc_conf is not None and part_mod in enc_conf.partitions: self._perform_enc_formatting( diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 54b4932b..d4563faa 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -793,6 +793,9 @@ class PartitionModification: def is_exists_or_modify(self) -> bool: return self.status in [ModificationStatus.Exist, ModificationStatus.Modify] + def is_create_or_modify(self) -> bool: + return self.status in [ModificationStatus.Create, ModificationStatus.Modify] + @property def mapper_name(self) -> Optional[str]: if self.dev_path: diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index dc99afce..9c6e6d35 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -55,8 +55,9 @@ class FilesystemHandler: device_handler.format(mod, enc_conf=self._enc_config) for part_mod in mod.partitions: - if part_mod.fs_type == FilesystemType.Btrfs: - device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) + if part_mod.is_create_or_modify(): + if part_mod.fs_type == FilesystemType.Btrfs: + device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) def _do_countdown(self) -> bool: SIG_TRIGGER = False -- cgit v1.2.3-54-g00ecf From c210cdcb8f0883ac13a6ee22aebb8f01f3043e09 Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:09:26 -0400 Subject: Fix Btrfs mount options (#2404) --- archinstall/lib/disk/device_handler.py | 26 +++++++-------- archinstall/lib/disk/device_model.py | 36 ++++----------------- archinstall/lib/disk/partitioning_menu.py | 39 +++++++++++++++++----- archinstall/lib/disk/subvolume_menu.py | 27 ++-------------- archinstall/lib/installer.py | 52 +++++++++--------------------- archinstall/lib/interactions/disk_conf.py | 35 +++++++++++++------- docs/cli_parameters/config/disk_config.rst | 10 ------ 7 files changed, 91 insertions(+), 134 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 59ee150d..c06247e6 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -437,9 +437,19 @@ class DeviceHandler(object): if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') - self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount( + luks_handler.mapper_dev, + self._TMP_BTRFS_MOUNT, + create_target_mountpoint=True, + options=part_mod.mount_options + ) else: - self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + self.mount( + part_mod.safe_dev_path, + self._TMP_BTRFS_MOUNT, + create_target_mountpoint=True, + options=part_mod.mount_options + ) for sub_vol in part_mod.btrfs_subvols: debug(f'Creating subvolume: {sub_vol.name}') @@ -451,18 +461,6 @@ class DeviceHandler(object): SysCommand(f"btrfs subvolume create {subvol_path}") - if sub_vol.nodatacow: - try: - SysCommand(f'chattr +C {subvol_path}') - except SysCallError as err: - raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') - - if sub_vol.compress: - try: - SysCommand(f'chattr +c {subvol_path}') - except SysCallError as err: - raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') - if luks_handler is not None and luks_handler.mapper_dev is not None: self.umount(luks_handler.mapper_dev) luks_handler.lock() diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index d4563faa..423c65e4 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -315,6 +315,11 @@ class Size: return self._normalize() >= other._normalize() +class BtrfsMountOption(Enum): + compress = 'compress=zstd' + nodatacow = 'nodatacow' + + @dataclass class _BtrfsSubvolumeInfo: name: Path @@ -458,8 +463,6 @@ class _DeviceInfo: class SubvolumeModification: name: Path mountpoint: Optional[Path] = None - compress: bool = False - nodatacow: bool = False @classmethod def from_existing_subvol_info(cls, info: _BtrfsSubvolumeInfo) -> SubvolumeModification: @@ -475,30 +478,10 @@ class SubvolumeModification: mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None - compress = entry.get('compress', False) - nodatacow = entry.get('nodatacow', False) - - if compress and nodatacow: - raise ValueError('compress and nodatacow flags cannot be enabled simultaneously on a btfrs subvolume') - - mods.append( - SubvolumeModification( - entry['name'], - mountpoint, - compress, - nodatacow - ) - ) + mods.append(SubvolumeModification(entry['name'], mountpoint)) return mods - @property - def mount_options(self) -> List[str]: - options = [] - options += ['compress'] if self.compress else [] - options += ['nodatacow'] if self.nodatacow else [] - return options - @property def relative_mountpoint(self) -> Path: """ @@ -516,12 +499,7 @@ class SubvolumeModification: return False def json(self) -> Dict[str, Any]: - return { - 'name': str(self.name), - 'mountpoint': str(self.mountpoint), - 'compress': self.compress, - 'nodatacow': self.nodatacow - } + return {'name': str(self.name), 'mountpoint': str(self.mountpoint)} def table_data(self) -> Dict[str, Any]: return self.json() diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index a9478158..823605e3 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus, DeviceGeometry, SectorSize + ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption from ..hardware import SysInfo from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn @@ -30,6 +30,7 @@ class PartitioningList(ListManager): 'mark_bootable': str(_('Mark/Unmark as bootable')), 'set_filesystem': str(_('Change filesystem')), 'btrfs_mark_compressed': str(_('Mark/Unmark as compressed')), # btrfs only + 'btrfs_mark_nodatacow': str(_('Mark/Unmark as nodatacow')), # btrfs only 'btrfs_set_subvolumes': str(_('Set subvolumes')), # btrfs only 'delete_partition': str(_('Delete partition')) } @@ -71,12 +72,17 @@ class PartitioningList(ListManager): self._actions['set_filesystem'], self._actions['mark_bootable'], self._actions['btrfs_mark_compressed'], + self._actions['btrfs_mark_nodatacow'], self._actions['btrfs_set_subvolumes'] ] # non btrfs partitions shouldn't get btrfs options if selection.fs_type != FilesystemType.Btrfs: - not_filter += [self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes']] + not_filter += [ + self._actions['btrfs_mark_compressed'], + self._actions['btrfs_mark_nodatacow'], + self._actions['btrfs_set_subvolumes'] + ] else: not_filter += [self._actions['assign_mountpoint']] @@ -122,7 +128,9 @@ class PartitioningList(ListManager): if fs_type == FilesystemType.Btrfs: entry.mountpoint = None case 'btrfs_mark_compressed' if entry: - self._set_compressed(entry) + self._toggle_mount_option(entry, BtrfsMountOption.compress) + case 'btrfs_mark_nodatacow' if entry: + self._toggle_mount_option(entry, BtrfsMountOption.nodatacow) case 'btrfs_set_subvolumes' if entry: self._set_btrfs_subvolumes(entry) case 'delete_partition' if entry: @@ -141,13 +149,28 @@ class PartitioningList(ListManager): else: return [d for d in data if d != entry] - def _set_compressed(self, partition: PartitionModification): - compression = 'compress=zstd' + def _toggle_mount_option( + self, + partition: PartitionModification, + option: BtrfsMountOption + ): + if option.value not in partition.mount_options: + if option == BtrfsMountOption.compress: + partition.mount_options = [ + o for o in partition.mount_options + if o != BtrfsMountOption.nodatacow.value + ] + + partition.mount_options = [ + o for o in partition.mount_options + if not o.startswith(BtrfsMountOption.compress.name) + ] - if compression in partition.mount_options: - partition.mount_options = [o for o in partition.mount_options if o != compression] + partition.mount_options.append(option.value) else: - partition.mount_options.append(compression) + partition.mount_options = [ + o for o in partition.mount_options if o != option.value + ] def _set_btrfs_subvolumes(self, partition: PartitionModification): partition.btrfs_subvols = SubvolumeMenu( diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 2b70d7b2..48afa829 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Dict, List, Optional, Any, TYPE_CHECKING from .device_model import SubvolumeModification -from ..menu import Menu, TextInput, MenuSelectionType, ListManager +from ..menu import TextInput, ListManager from ..output import FormattedOutput if TYPE_CHECKING: @@ -36,23 +36,6 @@ class SubvolumeMenu(ListManager): def selected_action_display(self, subvolume: SubvolumeModification) -> str: return str(subvolume.name) - def _prompt_options(self, editing: Optional[SubvolumeModification] = None) -> List[str]: - preset_options = [] - if editing: - preset_options = editing.mount_options - - choice = Menu( - str(_("Select the desired subvolume options ")), - ['nodatacow', 'compress'], - skip=True, - preset_values=preset_options, - ).run() - - if choice.type_ == MenuSelectionType.Selection: - return choice.value # type: ignore - - return [] - def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]: name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() @@ -64,13 +47,7 @@ class SubvolumeMenu(ListManager): if not mountpoint: return None - options = self._prompt_options(editing) - - subvolume = SubvolumeModification(Path(name), Path(mountpoint)) - subvolume.compress = 'compress' in options - subvolume.nodatacow = 'nodatacow' in options - - return subvolume + return SubvolumeModification(Path(name), Path(mountpoint)) def handle_action( self, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d5ea889b..c53e922d 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -240,7 +240,11 @@ class Installer: disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options) if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path: - self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols) + self._mount_btrfs_subvol( + part_mod.dev_path, + part_mod.btrfs_subvols, + part_mod.mount_options + ) def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined @@ -251,11 +255,18 @@ class Installer: if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) - def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]): + def _mount_btrfs_subvol( + self, + dev_path: Path, + subvolumes: List[disk.SubvolumeModification], + mount_options: List[str] = [] + ): for subvol in subvolumes: - mountpoint = self.target / subvol.relative_mountpoint - mount_options = subvol.mount_options + [f'subvol={subvol.name}'] - disk.device_handler.mount(dev_path, mountpoint, options=mount_options) + disk.device_handler.mount( + dev_path, + self.target / subvol.relative_mountpoint, + options=mount_options + [f'subvol={subvol.name}'] + ) def generate_key_files(self): for part_mod in self._disk_encryption.partitions: @@ -382,37 +393,6 @@ class Installer: for entry in self._fstab_entries: fp.write(f'{entry}\n') - for mod in self._disk_config.device_modifications: - for part_mod in mod.partitions: - if part_mod.fs_type != disk.FilesystemType.Btrfs: - continue - - with fstab_path.open('r') as fp: - fstab = fp.readlines() - - # Replace the {installation}/etc/fstab with entries - # using the compress=zstd where the mountpoint has compression set. - for index, line in enumerate(fstab): - # So first we grab the mount options by using subvol=.*? as a locator. - # And we also grab the mountpoint for the entry, for instance /var/log - subvoldef = re.findall(',.*?subvol=.*?[\t ]', line) - mountpoint = re.findall('[\t ]/.*?[\t ]', line) - - if not subvoldef or not mountpoint: - continue - - for sub_vol in part_mod.btrfs_subvols: - # We then locate the correct subvolume and check if it's compressed, - # and skip entries where compression is already defined - # We then sneak in the compress=zstd option if it doesn't already exist: - if sub_vol.compress and str(sub_vol.mountpoint) == Path( - mountpoint[0].strip()) and ',compress=zstd,' not in line: - fstab[index] = line.replace(subvoldef[0], f',compress=zstd{subvoldef[0]}') - break - - with fstab_path.open('w') as fp: - fp.writelines(fstab) - def set_hostname(self, hostname: str, *args: str, **kwargs: str) -> None: with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 72a32311..9d0042d6 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING from typing import Optional, List, Tuple from .. import disk +from ..disk.device_model import BtrfsMountOption from ..hardware import SysInfo from ..menu import Menu from ..menu import TableMenu @@ -214,6 +215,20 @@ def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType return options[choice.single_value] +def select_mount_options() -> List[str]: + prompt = str(_('Would you like to use compression or disable CoW?')) + options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))] + choice = Menu(prompt, options, sort=False).run() + + if choice.type_ == MenuSelectionType.Selection: + if choice.single_value == options[0]: + return [BtrfsMountOption.compress.value] + else: + return [BtrfsMountOption.nodatacow.value] + + return [] + + def suggest_single_disk_layout( device: disk.BDevice, filesystem_type: Optional[disk.FilesystemType] = None, @@ -228,7 +243,7 @@ def suggest_single_disk_layout( root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) using_subvolumes = False using_home_partition = False - compression = False + mount_options = [] device_size_gib = device.device_info.total_size if filesystem_type == disk.FilesystemType.Btrfs: @@ -236,9 +251,7 @@ def suggest_single_disk_layout( choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() using_subvolumes = choice.value == Menu.yes() - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() + mount_options = select_mount_options() device_modification = disk.DeviceModification(device, wipe=True) @@ -290,7 +303,7 @@ def suggest_single_disk_layout( length=root_length, mountpoint=Path('/') if not using_subvolumes else None, fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options ) device_modification.add_partition(root_partition) @@ -323,7 +336,7 @@ def suggest_single_disk_layout( length=home_length, mountpoint=Path('/home'), fs_type=filesystem_type, - mount_options=['compress=zstd'] if compression else [] + mount_options=mount_options ) device_modification.add_partition(home_partition) @@ -344,7 +357,7 @@ def suggest_multi_disk_layout( min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) - compression = False + mount_options = [] if not filesystem_type: filesystem_type = select_main_filesystem_format(advanced_options) @@ -371,9 +384,7 @@ def suggest_multi_disk_layout( return [] if filesystem_type == disk.FilesystemType.Btrfs: - prompt = str(_('Would you like to use BTRFS compression?')) - choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() - compression = choice.value == Menu.yes() + mount_options = select_mount_options() device_paths = ', '.join([str(d.device_info.path) for d in devices]) @@ -409,7 +420,7 @@ def suggest_multi_disk_layout( start=root_start, length=root_length, mountpoint=Path('/'), - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options, fs_type=filesystem_type ) root_device_modification.add_partition(root_partition) @@ -427,7 +438,7 @@ def suggest_multi_disk_layout( start=home_start, length=home_length, mountpoint=Path('/home'), - mount_options=['compress=zstd'] if compression else [], + mount_options=mount_options, fs_type=filesystem_type, ) home_device_modification.add_partition(home_partition) diff --git a/docs/cli_parameters/config/disk_config.rst b/docs/cli_parameters/config/disk_config.rst index 3dc01fb2..b09d0dc0 100644 --- a/docs/cli_parameters/config/disk_config.rst +++ b/docs/cli_parameters/config/disk_config.rst @@ -186,34 +186,24 @@ This example contains both subvolumes and compression. { "btrfs": [ { - "compress": false, "mountpoint": "/", "name": "@", - "nodatacow": false }, { - "compress": false, "mountpoint": "/home", "name": "@home", - "nodatacow": false }, { - "compress": false, "mountpoint": "/var/log", "name": "@log", - "nodatacow": false }, { - "compress": false, "mountpoint": "/var/cache/pacman/pkg", "name": "@pkg", - "nodatacow": false }, { - "compress": false, "mountpoint": "/.snapshots", "name": "@.snapshots", - "nodatacow": false } ], "dev_path": null, -- cgit v1.2.3-54-g00ecf From df2884085dc06f71f2bb201a316828c23b6299dd Mon Sep 17 00:00:00 2001 From: codefiles <11915375+codefiles@users.noreply.github.com> Date: Tue, 12 Mar 2024 05:25:15 -0400 Subject: Add udev sync before lsblk that follows formatting (#2412) --- archinstall/lib/disk/device_handler.py | 18 ++++----------- archinstall/lib/disk/device_model.py | 42 +++++++++++++++------------------- 2 files changed, 22 insertions(+), 38 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index c06247e6..6e91ac2e 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -2,7 +2,6 @@ from __future__ import annotations import json import os -import time import logging from pathlib import Path from typing import List, Dict, Any, Optional, TYPE_CHECKING @@ -316,6 +315,9 @@ class DeviceHandler(object): else: self._perform_formatting(part_mod.safe_fs_type, part_mod.safe_dev_path) + # synchronize with udev before using lsblk + SysCommand('udevadm settle') + lsblk_info = self._fetch_part_info(part_mod.safe_dev_path) part_mod.partn = lsblk_info.partn @@ -384,19 +386,7 @@ class DeviceHandler(object): part_mod.dev_path = Path(partition.path) def _fetch_part_info(self, path: Path) -> LsblkInfo: - attempts = 3 - lsblk_info: Optional[LsblkInfo] = None - - for attempt_nr in range(attempts): - time.sleep(attempt_nr + 1) - lsblk_info = get_lsblk_info(path) - - if lsblk_info.partn and lsblk_info.partuuid and lsblk_info.uuid: - break - - if not lsblk_info: - debug(f'Unable to get partition information: {path}') - raise DiskError(f'Unable to get partition information: {path}') + lsblk_info = get_lsblk_info(path) if not lsblk_info.partn: debug(f'Unable to determine new partition number: {path}\n{lsblk_info}') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 423c65e4..fe96203c 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -3,7 +3,6 @@ from __future__ import annotations import dataclasses import json import math -import time import uuid from dataclasses import dataclass, field from enum import Enum @@ -1103,39 +1102,34 @@ def _clean_field(name: str, clean_type: CleanType) -> str: return name.replace('_percentage', '%').replace('_', '-') -def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None, retry: int = 3) -> List[LsblkInfo]: +def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None) -> List[LsblkInfo]: fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] - lsblk_fields = ','.join(fields) - - if not dev_path: - dev_path = '' - - for retry_attempt in range(retry + 1): - try: - result = SysCommand(f'lsblk --json -b -o+{lsblk_fields} {dev_path}').decode() - break - except SysCallError as err: - # Get the output minus the message/info from lsblk if it returns a non-zero exit code. - if err.worker: - err_str = err.worker.decode() - debug(f'Error calling lsblk: {err_str}') - else: - raise err + cmd = ['lsblk', '--json', '--bytes', '--output', '+' + ','.join(fields)] + + if dev_path: + cmd.append(str(dev_path)) - if retry_attempt == retry: - raise err + try: + result = SysCommand(cmd).decode() + except SysCallError as err: + # Get the output minus the message/info from lsblk if it returns a non-zero exit code. + if err.worker: + err_str = err.worker.decode() + debug(f'Error calling lsblk: {err_str}') - time.sleep(1) + if dev_path: + raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + + raise err try: block_devices = json.loads(result) - blockdevices = block_devices['blockdevices'] - return [LsblkInfo.from_json(device) for device in blockdevices] except json.decoder.JSONDecodeError as err: error(f"Could not decode lsblk JSON: {result}") raise err - raise DiskError(f'Failed to read disk "{dev_path}" with lsblk') + blockdevices = block_devices['blockdevices'] + return [LsblkInfo.from_json(device) for device in blockdevices] def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: -- cgit v1.2.3-54-g00ecf From b470b16ec923260cfd9c5b9f2b88e0a39611b463 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 15 Apr 2024 18:49:00 +1000 Subject: LVM support (#2104) * Submenu for disk configuration * Update * Add LVM manual config * PV selection * LVM volume menu * Update * Fix mypy * Update * Update * Update * Update * Update * Update * Update * Update * Update LVM * Update * Update * Btrfs support * Refactor * LVM on Luks * Luks on LVM * Update * LVM on Luks * Update * Update * mypy * Update * Fix bug with LuksOnLvm and Btrfs * Update * Update * Info -> Debug output --- archinstall/lib/disk/__init__.py | 9 +- archinstall/lib/disk/device_handler.py | 264 ++++++++++--- archinstall/lib/disk/device_model.py | 408 +++++++++++++++++++-- archinstall/lib/disk/disk_menu.py | 140 +++++++ archinstall/lib/disk/encryption_menu.py | 131 +++++-- archinstall/lib/disk/fido.py | 8 +- archinstall/lib/disk/filesystem.py | 295 ++++++++++++++- archinstall/lib/disk/partitioning_menu.py | 18 +- archinstall/lib/disk/subvolume_menu.py | 18 +- archinstall/lib/global_menu.py | 70 ++-- archinstall/lib/installer.py | 427 +++++++++++++++++----- archinstall/lib/interactions/disk_conf.py | 134 ++++++- archinstall/lib/interactions/manage_users_conf.py | 18 +- archinstall/lib/luks.py | 30 +- archinstall/lib/menu/abstract_menu.py | 93 ++--- archinstall/lib/menu/list_manager.py | 28 +- archinstall/lib/menu/menu.py | 8 +- archinstall/lib/menu/table_selection_menu.py | 4 +- archinstall/lib/mirrors.py | 15 - archinstall/scripts/guided.py | 2 +- examples/interactive_installation.py | 2 +- 21 files changed, 1711 insertions(+), 411 deletions(-) create mode 100644 archinstall/lib/disk/disk_menu.py (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 24dafef5..7f881273 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -11,6 +11,11 @@ from .device_model import ( BDevice, DiskLayoutType, DiskLayoutConfiguration, + LvmLayoutType, + LvmConfiguration, + LvmVolumeGroup, + LvmVolume, + LvmVolumeStatus, PartitionTable, Unit, Size, @@ -30,7 +35,7 @@ from .device_model import ( CleanType, get_lsblk_info, get_all_lsblk_info, - get_lsblk_by_mountpoint + get_lsblk_by_mountpoint, ) from .encryption_menu import ( select_encryption_type, @@ -39,3 +44,5 @@ from .encryption_menu import ( select_partitions_to_encrypt, DiskEncryptionMenu, ) + +from .disk_menu import DiskLayoutConfigurationMenu diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 6e91ac2e..7ba70382 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -3,8 +3,9 @@ from __future__ import annotations import json import os import logging +import time from pathlib import Path -from typing import List, Dict, Any, Optional, TYPE_CHECKING +from typing import List, Dict, Any, Optional, TYPE_CHECKING, Literal, Iterable from parted import ( # type: ignore Disk, Geometry, FileSystem, @@ -17,11 +18,12 @@ from .device_model import ( BDevice, _DeviceInfo, _PartitionInfo, FilesystemType, Unit, PartitionTable, ModificationStatus, get_lsblk_info, LsblkInfo, - _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption + _BtrfsSubvolumeInfo, get_all_lsblk_info, DiskEncryption, LvmVolumeGroup, LvmVolume, Size, LvmGroupInfo, + SectorSize, LvmVolumeInfo, LvmPVInfo, SubvolumeModification, BtrfsMountOption ) from ..exceptions import DiskError, UnknownFilesystemFormat -from ..general import SysCommand, SysCallError, JSON +from ..general import SysCommand, SysCallError, JSON, SysCommandWorker from ..luks import Luks2 from ..output import debug, error, info, warn, log from ..utils.util import is_subpath @@ -189,7 +191,7 @@ class DeviceHandler(object): return subvol_infos - def _perform_formatting( + def format( self, fs_type: FilesystemType, path: Path, @@ -234,7 +236,7 @@ class DeviceHandler(object): options += additional_parted_options options_str = ' '.join(options) - info(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') + debug(f'Formatting filesystem: /usr/bin/{command} {options_str} {path}') try: SysCommand(f"/usr/bin/{command} {options_str} {path}") @@ -243,7 +245,33 @@ class DeviceHandler(object): error(msg) raise DiskError(msg) from err - def _perform_enc_formatting( + def encrypt( + self, + dev_path: Path, + mapper_name: Optional[str], + enc_password: str, + lock_after_create: bool = True + ) -> Luks2: + luks_handler = Luks2( + dev_path, + mapper_name=mapper_name, + password=enc_password + ) + + key_file = luks_handler.encrypt() + + luks_handler.unlock(key_file=key_file) + + if not luks_handler.mapper_dev: + raise DiskError('Failed to unlock luks device') + + if lock_after_create: + debug(f'luks2 locking device: {dev_path}') + luks_handler.lock() + + return luks_handler + + def format_encrypted( self, dev_path: Path, mapper_name: Optional[str], @@ -258,71 +286,160 @@ class DeviceHandler(object): key_file = luks_handler.encrypt() - debug(f'Unlocking luks2 device: {dev_path}') luks_handler.unlock(key_file=key_file) if not luks_handler.mapper_dev: raise DiskError('Failed to unlock luks device') info(f'luks2 formatting mapper dev: {luks_handler.mapper_dev}') - self._perform_formatting(fs_type, luks_handler.mapper_dev) + self.format(fs_type, luks_handler.mapper_dev) info(f'luks2 locking device: {dev_path}') luks_handler.lock() - def _validate_partitions(self, partitions: List[PartitionModification]): - checks = { - # verify that all partitions have a path set (which implies that they have been created) - lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), - # crypto luks is not a valid file system type - lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError('Crypto luks cannot be set as a filesystem type'), - # file system type must be set - lambda x: x.fs_type is None: ValueError('File system type must be set for modification') - } - - for check, exc in checks.items(): - found = next(filter(check, partitions), None) - if found is not None: - raise exc - - def format( + def _lvm_info( self, - device_mod: DeviceModification, - enc_conf: Optional['DiskEncryption'] = None - ): - """ - Format can be given an overriding path, for instance /dev/null to test - the formatting functionality and in essence the support for the given filesystem. - """ + cmd: str, + info_type: Literal['lv', 'vg', 'pvseg'] + ) -> Optional[Any]: + raw_info = SysCommand(cmd).decode().split('\n') - # only verify partitions that are being created or modified - create_or_modify_parts = [p for p in device_mod.partitions if p.is_create_or_modify()] + # for whatever reason the output sometimes contains + # "File descriptor X leaked leaked on vgs invocation + data = '\n'.join([raw for raw in raw_info if 'File descriptor' not in raw]) - self._validate_partitions(create_or_modify_parts) + debug(f'LVM info: {data}') - # make sure all devices are unmounted - self._umount_all_existing(device_mod.device_path) - - for part_mod in create_or_modify_parts: - # partition will be encrypted - if enc_conf is not None and part_mod in enc_conf.partitions: - self._perform_enc_formatting( - part_mod.safe_dev_path, - part_mod.mapper_name, - part_mod.safe_fs_type, - enc_conf - ) - else: - self._perform_formatting(part_mod.safe_fs_type, part_mod.safe_dev_path) + reports = json.loads(data) + + for report in reports['report']: + if len(report[info_type]) != 1: + raise ValueError(f'Report does not contain any entry') - # synchronize with udev before using lsblk - SysCommand('udevadm settle') + entry = report[info_type][0] - lsblk_info = self._fetch_part_info(part_mod.safe_dev_path) + match info_type: + case 'pvseg': + return LvmPVInfo( + pv_name=Path(entry['pv_name']), + lv_name=entry['lv_name'], + vg_name=entry['vg_name'], + ) + case 'lv': + return LvmVolumeInfo( + lv_name=entry['lv_name'], + vg_name=entry['vg_name'], + lv_size=Size(int(entry[f'lv_size'][:-1]), Unit.B, SectorSize.default()) + ) + case 'vg': + return LvmGroupInfo( + vg_uuid=entry['vg_uuid'], + vg_size=Size(int(entry[f'vg_size'][:-1]), Unit.B, SectorSize.default()) + ) + + return None - part_mod.partn = lsblk_info.partn - part_mod.partuuid = lsblk_info.partuuid - part_mod.uuid = lsblk_info.uuid + def _lvm_info_with_retry(self, cmd: str, info_type: Literal['lv', 'vg', 'pvseg']) -> Optional[Any]: + attempts = 3 + + for attempt_nr in range(attempts): + try: + return self._lvm_info(cmd, info_type) + except ValueError: + time.sleep(attempt_nr + 1) + + raise ValueError(f'Failed to fetch {info_type} information') + + def lvm_vol_info(self, lv_name: str) -> Optional[LvmVolumeInfo]: + cmd = ( + 'lvs --reportformat json ' + '--unit B ' + f'-S lv_name={lv_name}' + ) + + return self._lvm_info_with_retry(cmd, 'lv') + + def lvm_group_info(self, vg_name: str) -> Optional[LvmGroupInfo]: + cmd = ( + 'vgs --reportformat json ' + '--unit B ' + '-o vg_name,vg_uuid,vg_size ' + f'-S vg_name={vg_name}' + ) + + return self._lvm_info_with_retry(cmd, 'vg') + + def lvm_pvseg_info(self, vg_name: str, lv_name: str) -> Optional[LvmPVInfo]: + cmd = ( + 'pvs ' + '--segments -o+lv_name,vg_name ' + f'-S vg_name={vg_name},lv_name={lv_name} ' + '--reportformat json ' + ) + + return self._lvm_info_with_retry(cmd, 'pvseg') + + def lvm_vol_change(self, vol: LvmVolume, activate: bool): + active_flag = 'y' if activate else 'n' + cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}' + + debug(f'lvchange volume: {cmd}') + SysCommand(cmd) + + def lvm_export_vg(self, vg: LvmVolumeGroup): + cmd = f'vgexport {vg.name}' + + debug(f'vgexport: {cmd}') + SysCommand(cmd) + + def lvm_import_vg(self, vg: LvmVolumeGroup): + cmd = f'vgimport {vg.name}' + + debug(f'vgimport: {cmd}') + SysCommand(cmd) + + def lvm_vol_reduce(self, vol_path: Path, amount: Size): + val = amount.format_size(Unit.B, include_unit=False) + cmd = f'lvreduce -L -{val}B {vol_path}' + + debug(f'Reducing LVM volume size: {cmd}') + SysCommand(cmd) + + def lvm_pv_create(self, pvs: Iterable[Path]): + cmd = 'pvcreate ' + ' '.join([str(pv) for pv in pvs]) + debug(f'Creating LVM PVS: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + def lvm_vg_create(self, pvs: Iterable[Path], vg_name: str): + pvs_str = ' '.join([str(pv) for pv in pvs]) + cmd = f'vgcreate --yes {vg_name} {pvs_str}' + + debug(f'Creating LVM group: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + def lvm_vol_create(self, vg_name: str, volume: LvmVolume, offset: Optional[Size] = None): + if offset is not None: + length = volume.length - offset + else: + length = volume.length + + length_str = length.format_size(Unit.B, include_unit=False) + cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}' + + debug(f'Creating volume: {cmd}') + + worker = SysCommandWorker(cmd) + worker.poll() + worker.write(b'y\n', line_ending=False) + + volume.vg_name = vg_name + volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}') def _setup_partition( self, @@ -385,7 +502,7 @@ class DeviceHandler(object): # the partition has a path now that it has been added part_mod.dev_path = Path(partition.path) - def _fetch_part_info(self, path: Path) -> LsblkInfo: + def fetch_part_info(self, path: Path) -> LsblkInfo: lsblk_info = get_lsblk_info(path) if not lsblk_info.partn: @@ -404,6 +521,37 @@ class DeviceHandler(object): return lsblk_info + def create_lvm_btrfs_subvolumes( + self, + path: Path, + btrfs_subvols: List[SubvolumeModification], + mount_options: List[str] + ): + info(f'Creating subvolumes: {path}') + + self.mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True) + + for sub_vol in btrfs_subvols: + debug(f'Creating subvolume: {sub_vol.name}') + + subvol_path = self._TMP_BTRFS_MOUNT / sub_vol.name + + SysCommand(f"btrfs subvolume create {subvol_path}") + + if BtrfsMountOption.nodatacow.value in mount_options: + try: + SysCommand(f'chattr +C {subvol_path}') + except SysCallError as err: + raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}') + + if BtrfsMountOption.compress.value in mount_options: + try: + SysCommand(f'chattr +c {subvol_path}') + except SysCallError as err: + raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}') + + self.umount(path) + def create_btrfs_volumes( self, part_mod: PartitionModification, @@ -468,8 +616,8 @@ class DeviceHandler(object): return luks_handler - def _umount_all_existing(self, device_path: Path): - info(f'Unmounting all existing partitions: {device_path}') + def umount_all_existing(self, device_path: Path): + debug(f'Unmounting all existing partitions: {device_path}') existing_partitions = self._devices[device_path].partition_infos @@ -498,7 +646,7 @@ class DeviceHandler(object): raise DiskError('Too many partitions on disk, MBR disks can only have 3 primary partitions') # make sure all devices are unmounted - self._umount_all_existing(modification.device_path) + self.umount_all_existing(modification.device_path) # WARNING: the entire device will be wiped and all data lost if modification.wipe: diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index fe96203c..1cd3d674 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -41,6 +41,8 @@ class DiskLayoutType(Enum): class DiskLayoutConfiguration: config_type: DiskLayoutType device_modifications: List[DeviceModification] = field(default_factory=list) + lvm_config: Optional[LvmConfiguration] = None + # used for pre-mounted config mountpoint: Optional[Path] = None @@ -51,13 +53,18 @@ class DiskLayoutConfiguration: 'mountpoint': str(self.mountpoint) } else: - return { + config: Dict[str, Any] = { 'config_type': self.config_type.value, - 'device_modifications': [mod.json() for mod in self.device_modifications] + 'device_modifications': [mod.json() for mod in self.device_modifications], } + if self.lvm_config: + config['lvm_config'] = self.lvm_config.json() + + return config + @classmethod - def parse_arg(cls, disk_config: Dict[str, List[Dict[str, Any]]]) -> Optional[DiskLayoutConfiguration]: + def parse_arg(cls, disk_config: Dict[str, Any]) -> Optional[DiskLayoutConfiguration]: from .device_handler import device_handler device_modifications: List[DeviceModification] = [] @@ -124,6 +131,10 @@ class DiskLayoutConfiguration: device_modification.partitions = device_partitions device_modifications.append(device_modification) + # Parse LVM configuration from settings + if (lvm_arg := disk_config.get('lvm_config', None)) is not None: + config.lvm_config = LvmConfiguration.parse_arg(lvm_arg, config) + return config @@ -133,24 +144,24 @@ class PartitionTable(Enum): class Unit(Enum): - B = 1 # byte - kB = 1000**1 # kilobyte - MB = 1000**2 # megabyte - GB = 1000**3 # gigabyte - TB = 1000**4 # terabyte - PB = 1000**5 # petabyte - EB = 1000**6 # exabyte - ZB = 1000**7 # zettabyte - YB = 1000**8 # yottabyte - - KiB = 1024**1 # kibibyte - MiB = 1024**2 # mebibyte - GiB = 1024**3 # gibibyte - TiB = 1024**4 # tebibyte - PiB = 1024**5 # pebibyte - EiB = 1024**6 # exbibyte - ZiB = 1024**7 # zebibyte - YiB = 1024**8 # yobibyte + B = 1 # byte + kB = 1000 ** 1 # kilobyte + MB = 1000 ** 2 # megabyte + GB = 1000 ** 3 # gigabyte + TB = 1000 ** 4 # terabyte + PB = 1000 ** 5 # petabyte + EB = 1000 ** 6 # exabyte + ZB = 1000 ** 7 # zettabyte + YB = 1000 ** 8 # yottabyte + + KiB = 1024 ** 1 # kibibyte + MiB = 1024 ** 2 # mebibyte + GiB = 1024 ** 3 # gibibyte + TiB = 1024 ** 4 # tebibyte + PiB = 1024 ** 5 # pebibyte + EiB = 1024 ** 6 # exbibyte + ZiB = 1024 ** 7 # zebibyte + YiB = 1024 ** 8 # yobibyte sectors = 'sectors' # size in sector @@ -575,7 +586,7 @@ class PartitionFlag(Enum): Which is the way libparted checks for its flags: https://git.savannah.gnu.org/gitweb/?p=parted.git;a=blob;f=libparted/labels/gpt.c;hb=4a0e468ed63fff85a1f9b923189f20945b32f4f1#l183 """ Boot = _ped.PARTITION_BOOT - XBOOTLDR = _ped.PARTITION_BLS_BOOT # Note: parted calls this bls_boot + XBOOTLDR = _ped.PARTITION_BLS_BOOT # Note: parted calls this bls_boot ESP = _ped.PARTITION_ESP @@ -658,6 +669,10 @@ class PartitionModification: flags: List[PartitionFlag] = field(default_factory=list) btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + # only set when modification was created from an existing + # partition info object to be able to reference it back + part_info: Optional[_PartitionInfo] = None + # only set if the device was created or exists dev_path: Optional[Path] = None partn: Optional[int] = None @@ -724,7 +739,8 @@ class PartitionModification: uuid=partition_info.uuid, flags=partition_info.flags, mountpoint=mountpoint, - btrfs_subvols=subvol_mods + btrfs_subvols=subvol_mods, + part_info=partition_info ) @property @@ -832,6 +848,270 @@ class PartitionModification: return part_mod +class LvmLayoutType(Enum): + Default = 'default' + + # Manual = 'manual_lvm' + + def display_msg(self) -> str: + match self: + case LvmLayoutType.Default: + return str(_('Default layout')) + # case LvmLayoutType.Manual: + # return str(_('Manual configuration')) + + raise ValueError(f'Unknown type: {self}') + + +@dataclass +class LvmVolumeGroup: + name: str + pvs: List[PartitionModification] + volumes: List[LvmVolume] = field(default_factory=list) + + def json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'lvm_pvs': [p.obj_id for p in self.pvs], + 'volumes': [vol.json() for vol in self.volumes] + } + + @staticmethod + def parse_arg(arg: Dict[str, Any], disk_config: DiskLayoutConfiguration) -> LvmVolumeGroup: + lvm_pvs = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('lvm_pvs', []): + lvm_pvs.append(part) + + return LvmVolumeGroup( + arg['name'], + lvm_pvs, + [LvmVolume.parse_arg(vol) for vol in arg['volumes']] + ) + + def contains_lv(self, lv: LvmVolume) -> bool: + return lv in self.volumes + + +class LvmVolumeStatus(Enum): + Exist = 'existing' + Modify = 'modify' + Delete = 'delete' + Create = 'create' + + +@dataclass +class LvmVolume: + status: LvmVolumeStatus + name: str + fs_type: FilesystemType + length: Size + mountpoint: Optional[Path] + mount_options: List[str] = field(default_factory=list) + btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) + + # volume group name + vg_name: Optional[str] = None + # mapper device path /dev// + dev_path: Optional[Path] = None + + def __post_init__(self): + # needed to use the object as a dictionary key due to hash func + if not hasattr(self, '_obj_id'): + self._obj_id = uuid.uuid4() + + def __hash__(self): + return hash(self._obj_id) + + @property + def obj_id(self) -> str: + if hasattr(self, '_obj_id'): + return str(self._obj_id) + return '' + + @property + def mapper_name(self) -> Optional[str]: + if self.dev_path: + return f'{storage.get("ENC_IDENTIFIER", "ai")}{self.safe_dev_path.name}' + return None + + @property + def mapper_path(self) -> Path: + if self.mapper_name: + return Path(f'/dev/mapper/{self.mapper_name}') + + raise ValueError('No mapper path set') + + @property + def safe_dev_path(self) -> Path: + if self.dev_path: + return self.dev_path + raise ValueError('No device path for volume defined') + + @property + def safe_fs_type(self) -> FilesystemType: + if self.fs_type is None: + raise ValueError('File system type is not set') + return self.fs_type + + @property + def relative_mountpoint(self) -> Path: + """ + Will return the relative path based on the anchor + e.g. Path('/mnt/test') -> Path('mnt/test') + """ + if self.mountpoint is not None: + return self.mountpoint.relative_to(self.mountpoint.anchor) + + raise ValueError('Mountpoint is not specified') + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> LvmVolume: + volume = LvmVolume( + status=LvmVolumeStatus(arg['status']), + name=arg['name'], + fs_type=FilesystemType(arg['fs_type']), + length=Size.parse_args(arg['length']), + mountpoint=Path(arg['mountpoint']) if arg['mountpoint'] else None, + mount_options=arg.get('mount_options', []), + btrfs_subvols=SubvolumeModification.parse_args(arg.get('btrfs', [])) + ) + + setattr(volume, '_obj_id', arg['obj_id']) + + return volume + + def json(self) -> Dict[str, Any]: + return { + 'obj_id': self.obj_id, + 'status': self.status.value, + 'name': self.name, + 'fs_type': self.fs_type.value, + 'length': self.length.json(), + 'mountpoint': str(self.mountpoint) if self.mountpoint else None, + 'mount_options': self.mount_options, + 'btrfs': [vol.json() for vol in self.btrfs_subvols] + } + + def table_data(self) -> Dict[str, Any]: + part_mod = { + 'Type': self.status.value, + 'Name': self.name, + 'Size': self.length.format_highest(), + 'FS type': self.fs_type.value, + 'Mountpoint': str(self.mountpoint) if self.mountpoint else '', + 'Mount options': ', '.join(self.mount_options), + 'Btrfs': '{} {}'.format(str(len(self.btrfs_subvols)), 'vol') + } + return part_mod + + def is_modify(self) -> bool: + return self.status == LvmVolumeStatus.Modify + + def exists(self) -> bool: + return self.status == LvmVolumeStatus.Exist + + def is_exists_or_modify(self) -> bool: + return self.status in [LvmVolumeStatus.Exist, LvmVolumeStatus.Modify] + + def is_root(self) -> bool: + if self.mountpoint is not None: + return Path('/') == self.mountpoint + else: + for subvol in self.btrfs_subvols: + if subvol.is_root(): + return True + + return False + + +@dataclass +class LvmGroupInfo: + vg_size: Size + vg_uuid: str + + +@dataclass +class LvmVolumeInfo: + lv_name: str + vg_name: str + lv_size: Size + + +@dataclass +class LvmPVInfo: + pv_name: Path + lv_name: str + vg_name: str + + +@dataclass +class LvmConfiguration: + config_type: LvmLayoutType + vol_groups: List[LvmVolumeGroup] + + def __post_init__(self): + # make sure all volume groups have unique PVs + pvs = [] + for group in self.vol_groups: + for pv in group.pvs: + if pv in pvs: + raise ValueError('A PV cannot be used in multiple volume groups') + pvs.append(pv) + + def json(self) -> Dict[str, Any]: + return { + 'config_type': self.config_type.value, + 'vol_groups': [vol_gr.json() for vol_gr in self.vol_groups] + } + + @staticmethod + def parse_arg(arg: Dict[str, Any], disk_config: DiskLayoutConfiguration) -> LvmConfiguration: + lvm_pvs = [] + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.obj_id in arg.get('lvm_pvs', []): + lvm_pvs.append(part) + + return LvmConfiguration( + config_type=LvmLayoutType(arg['config_type']), + vol_groups=[LvmVolumeGroup.parse_arg(vol_group, disk_config) for vol_group in arg['vol_groups']], + ) + + def get_all_pvs(self) -> List[PartitionModification]: + pvs = [] + for vg in self.vol_groups: + pvs += vg.pvs + + return pvs + + def get_all_volumes(self) -> List[LvmVolume]: + volumes = [] + + for vg in self.vol_groups: + volumes += vg.volumes + + return volumes + + def get_root_volume(self) -> Optional[LvmVolume]: + for vg in self.vol_groups: + filtered = next(filter(lambda x: x.is_root(), vg.volumes), None) + if filtered: + return filtered + + return None + + +# def get_lv_crypt_uuid(self, lv: LvmVolume, encryption: EncryptionType) -> str: +# """ +# Find the LUKS superblock UUID for the device that +# contains the given logical volume +# """ +# for vg in self.vol_groups: +# if vg.contains_lv(lv): + + @dataclass class DeviceModification: device: BDevice @@ -885,11 +1165,16 @@ class DeviceModification: class EncryptionType(Enum): NoEncryption = "no_encryption" Luks = "luks" + LvmOnLuks = 'lvm_on_luks' + LuksOnLvm = 'luks_on_lvm' @classmethod def _encryption_type_mapper(cls) -> Dict[str, 'EncryptionType']: return { - 'Luks': EncryptionType.Luks + str(_('No Encryption')): EncryptionType.NoEncryption, + str(_('LUKS')): EncryptionType.Luks, + str(_('LVM on LUKS')): EncryptionType.LvmOnLuks, + str(_('LUKS on LVM')): EncryptionType.LuksOnLvm } @classmethod @@ -906,18 +1191,31 @@ class EncryptionType(Enum): @dataclass class DiskEncryption: - encryption_type: EncryptionType = EncryptionType.Luks + encryption_type: EncryptionType = EncryptionType.NoEncryption encryption_password: str = '' partitions: List[PartitionModification] = field(default_factory=list) + lvm_volumes: List[LvmVolume] = field(default_factory=list) hsm_device: Optional[Fido2Device] = None - def should_generate_encryption_file(self, part_mod: PartitionModification) -> bool: - return part_mod in self.partitions and part_mod.mountpoint != Path('/') + def __post_init__(self): + if self.encryption_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and not self.partitions: + raise ValueError('Luks or LvmOnLuks encryption require partitions to be defined') + + if self.encryption_type == EncryptionType.LuksOnLvm and not self.lvm_volumes: + raise ValueError('LuksOnLvm encryption require LMV volumes to be defined') + + def should_generate_encryption_file(self, dev: PartitionModification | LvmVolume) -> bool: + if isinstance(dev, PartitionModification): + return dev in self.partitions and dev.mountpoint != Path('/') + elif isinstance(dev, LvmVolume): + return dev in self.lvm_volumes and dev.mountpoint != Path('/') + return False def json(self) -> Dict[str, Any]: obj: Dict[str, Any] = { 'encryption_type': self.encryption_type.value, - 'partitions': [p.obj_id for p in self.partitions] + 'partitions': [p.obj_id for p in self.partitions], + 'lvm_volumes': [vol.obj_id for vol in self.lvm_volumes] } if self.hsm_device: @@ -925,23 +1223,47 @@ class DiskEncryption: return obj + @classmethod + def validate_enc(cls, disk_config: DiskLayoutConfiguration) -> bool: + partitions = [] + + for mod in disk_config.device_modifications: + for part in mod.partitions: + partitions.append(part) + + if len(partitions) > 2: # assume one boot and at least 2 additional + if disk_config.lvm_config: + return False + + return True + @classmethod def parse_arg( cls, disk_config: DiskLayoutConfiguration, arg: Dict[str, Any], password: str = '' - ) -> 'DiskEncryption': + ) -> Optional['DiskEncryption']: + if not cls.validate_enc(disk_config): + return None + enc_partitions = [] for mod in disk_config.device_modifications: for part in mod.partitions: if part.obj_id in arg.get('partitions', []): enc_partitions.append(part) + volumes = [] + if disk_config.lvm_config: + for vol in disk_config.lvm_config.get_all_volumes(): + if vol.obj_id in arg.get('lvm_volumes', []): + volumes.append(vol) + enc = DiskEncryption( EncryptionType(arg['encryption_type']), password, - enc_partitions + enc_partitions, + volumes ) if hsm := arg.get('hsm_device', None): @@ -992,7 +1314,7 @@ class LsblkInfo: tran: Optional[str] = None partn: Optional[int] = None partuuid: Optional[str] = None - parttype :Optional[str] = None + parttype: Optional[str] = None uuid: Optional[str] = None fstype: Optional[str] = None fsver: Optional[str] = None @@ -1017,7 +1339,7 @@ class LsblkInfo: 'tran': self.tran, 'partn': self.partn, 'partuuid': self.partuuid, - 'parttype' : self.parttype, + 'parttype': self.parttype, 'uuid': self.uuid, 'fstype': self.fstype, 'fsver': self.fsver, @@ -1102,13 +1424,24 @@ def _clean_field(name: str, clean_type: CleanType) -> str: return name.replace('_percentage', '%').replace('_', '-') -def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None) -> List[LsblkInfo]: +def _fetch_lsblk_info( + dev_path: Optional[Union[Path, str]] = None, + reverse: bool = False, + full_dev_path: bool = False, + retry: int = 3 +) -> List[LsblkInfo]: fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] cmd = ['lsblk', '--json', '--bytes', '--output', '+' + ','.join(fields)] if dev_path: cmd.append(str(dev_path)) + if reverse: + cmd.append('--inverse') + + if full_dev_path: + cmd.append('--paths') + try: result = SysCommand(cmd).decode() except SysCallError as err: @@ -1132,8 +1465,12 @@ def _fetch_lsblk_info(dev_path: Optional[Union[Path, str]] = None) -> List[Lsblk return [LsblkInfo.from_json(device) for device in blockdevices] -def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: - if infos := _fetch_lsblk_info(dev_path): +def get_lsblk_info( + dev_path: Union[Path, str], + reverse: bool = False, + full_dev_path: bool = False +) -> LsblkInfo: + if infos := _fetch_lsblk_info(dev_path, reverse=reverse, full_dev_path=full_dev_path): return infos[0] raise DiskError(f'lsblk failed to retrieve information for "{dev_path}"') @@ -1142,6 +1479,7 @@ def get_lsblk_info(dev_path: Union[Path, str]) -> LsblkInfo: def get_all_lsblk_info() -> List[LsblkInfo]: return _fetch_lsblk_info() + def get_lsblk_by_mountpoint(mountpoint: Path, as_prefix: bool = False) -> List[LsblkInfo]: def _check(infos: List[LsblkInfo]) -> List[LsblkInfo]: devices = [] diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py new file mode 100644 index 00000000..a7d9ccc3 --- /dev/null +++ b/archinstall/lib/disk/disk_menu.py @@ -0,0 +1,140 @@ +from typing import Dict, Optional, Any, TYPE_CHECKING, List + +from . import DiskLayoutConfiguration, DiskLayoutType +from .device_model import LvmConfiguration +from ..disk import ( + DeviceModification +) +from ..interactions import select_disk_config +from ..interactions.disk_conf import select_lvm_config +from ..menu import ( + Selector, + AbstractSubMenu +) +from ..output import FormattedOutput + +if TYPE_CHECKING: + _: Any + + +class DiskLayoutConfigurationMenu(AbstractSubMenu): + def __init__( + self, + disk_layout_config: Optional[DiskLayoutConfiguration], + data_store: Dict[str, Any], + advanced: bool = False + ): + self._disk_layout_config = disk_layout_config + self._advanced = advanced + + super().__init__(data_store=data_store, preview_size=0.5) + + def setup_selection_menu_options(self): + self._menu_options['disk_config'] = \ + Selector( + _('Partitioning'), + lambda x: self._select_disk_layout_config(x), + display_func=lambda x: self._display_disk_layout(x), + preview_func=self._prev_disk_layouts, + default=self._disk_layout_config, + enabled=True + ) + self._menu_options['lvm_config'] = \ + Selector( + _('Logical Volume Management (LVM)'), + lambda x: self._select_lvm_config(x), + display_func=lambda x: self.defined_text if x else '', + preview_func=self._prev_lvm_config, + default=self._disk_layout_config.lvm_config if self._disk_layout_config else None, + dependencies=[self._check_dep_lvm], + enabled=True + ) + + def run(self, allow_reset: bool = True) -> Optional[DiskLayoutConfiguration]: + super().run(allow_reset=allow_reset) + + disk_layout_config: Optional[DiskLayoutConfiguration] = self._data_store.get('disk_config', None) + + if disk_layout_config: + disk_layout_config.lvm_config = self._data_store.get('lvm_config', None) + + return disk_layout_config + + def _check_dep_lvm(self) -> bool: + disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default: + return True + + return False + + def _select_disk_layout_config( + self, + preset: Optional[DiskLayoutConfiguration] + ) -> Optional[DiskLayoutConfiguration]: + disk_config = select_disk_config(preset, advanced_option=self._advanced) + + if disk_config != preset: + self._menu_options['lvm_config'].set_current_selection(None) + + return disk_config + + def _select_lvm_config(self, preset: Optional[LvmConfiguration]) -> Optional[LvmConfiguration]: + disk_config: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + if disk_config: + return select_lvm_config(disk_config, preset=preset) + return preset + + def _display_disk_layout(self, current_value: Optional[DiskLayoutConfiguration] = None) -> str: + if current_value: + return current_value.config_type.display_msg() + return '' + + def _prev_disk_layouts(self) -> Optional[str]: + disk_layout_conf: Optional[DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_layout_conf: + device_mods: List[DeviceModification] = \ + list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) + + if device_mods: + output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) + output_btrfs = '' + + for mod in device_mods: + # create partition table + partition_table = FormattedOutput.as_table(mod.partitions) + + output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' + output_partition += partition_table + '\n' + + # create btrfs table + btrfs_partitions = list( + filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions) + ) + for partition in btrfs_partitions: + output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + + output = output_partition + output_btrfs + return output.rstrip() + + return None + + def _prev_lvm_config(self) -> Optional[str]: + lvm_config: Optional[LvmConfiguration] = self._menu_options['lvm_config'].current_selection + + if lvm_config: + output = '{}: {}\n'.format(str(_('Configuration')), lvm_config.config_type.display_msg()) + + for vol_gp in lvm_config.vol_groups: + pv_table = FormattedOutput.as_table(vol_gp.pvs) + output += '{}:\n{}'.format(str(_('Physical volumes')), pv_table) + + output += f'\nVolume Group: {vol_gp.name}' + + lvm_volumes = FormattedOutput.as_table(vol_gp.volumes) + output += '\n\n{}:\n{}'.format(str(_('Volumes')), lvm_volumes) + + return output + + return None diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index c3a1c32f..b0e292ce 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Dict, Optional, Any, TYPE_CHECKING, List +from . import LvmConfiguration, LvmVolume from ..disk import ( DeviceModification, DiskLayoutConfiguration, @@ -40,31 +41,41 @@ class DiskEncryptionMenu(AbstractSubMenu): super().__init__(data_store=data_store) def setup_selection_menu_options(self): + self._menu_options['encryption_type'] = \ + Selector( + _('Encryption type'), + func=lambda preset: select_encryption_type(self._disk_config, preset), + display_func=lambda x: EncryptionType.type_to_text(x) if x else None, + default=self._preset.encryption_type, + enabled=True, + ) self._menu_options['encryption_password'] = \ Selector( _('Encryption password'), lambda x: select_encrypted_password(), + dependencies=[self._check_dep_enc_type], display_func=lambda x: secret(x) if x else '', default=self._preset.encryption_password, enabled=True ) - self._menu_options['encryption_type'] = \ - Selector( - _('Encryption type'), - func=lambda preset: select_encryption_type(preset), - display_func=lambda x: EncryptionType.type_to_text(x) if x else None, - dependencies=['encryption_password'], - default=self._preset.encryption_type, - enabled=True - ) self._menu_options['partitions'] = \ Selector( _('Partitions'), func=lambda preset: select_partitions_to_encrypt(self._disk_config.device_modifications, preset), display_func=lambda x: f'{len(x)} {_("Partitions")}' if x else None, - dependencies=['encryption_password'], + dependencies=[self._check_dep_partitions], default=self._preset.partitions, - preview_func=self._prev_disk_layouts, + preview_func=self._prev_partitions, + enabled=True + ) + self._menu_options['lvm_vols'] = \ + Selector( + _('LVM volumes'), + func=lambda preset: self._select_lvm_vols(preset), + display_func=lambda x: f'{len(x)} {_("LVM volumes")}' if x else None, + dependencies=[self._check_dep_lvm_vols], + default=self._preset.lvm_volumes, + preview_func=self._prev_lvm_vols, enabled=True ) self._menu_options['HSM'] = \ @@ -73,19 +84,54 @@ class DiskEncryptionMenu(AbstractSubMenu): func=lambda preset: select_hsm(preset), display_func=lambda x: self._display_hsm(x), preview_func=self._prev_hsm, - dependencies=['encryption_password'], + dependencies=[self._check_dep_enc_type], default=self._preset.hsm_device, enabled=True ) + def _select_lvm_vols(self, preset: List[LvmVolume]) -> List[LvmVolume]: + if self._disk_config.lvm_config: + return select_lvm_vols_to_encrypt(self._disk_config.lvm_config, preset=preset) + return [] + + def _check_dep_enc_type(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type != EncryptionType.NoEncryption: + return True + return False + + def _check_dep_partitions(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]: + return True + return False + + def _check_dep_lvm_vols(self) -> bool: + enc_type: Optional[EncryptionType] = self._menu_options['encryption_type'].current_selection + if enc_type and enc_type == EncryptionType.LuksOnLvm: + return True + return False + def run(self, allow_reset: bool = True) -> Optional[DiskEncryption]: super().run(allow_reset=allow_reset) - if self._data_store.get('encryption_password', None): + enc_type = self._data_store.get('encryption_type', None) + enc_password = self._data_store.get('encryption_password', None) + enc_partitions = self._data_store.get('partitions', None) + enc_lvm_vols = self._data_store.get('lvm_vols', None) + + if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions: + enc_lvm_vols = [] + + if enc_type == EncryptionType.LuksOnLvm: + enc_partitions = [] + + if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols): return DiskEncryption( - encryption_password=self._data_store.get('encryption_password', None), - encryption_type=self._data_store['encryption_type'], - partitions=self._data_store.get('partitions', None), + encryption_password=enc_password, + encryption_type=enc_type, + partitions=enc_partitions, + lvm_volumes=enc_lvm_vols, hsm_device=self._data_store.get('HSM', None) ) @@ -97,7 +143,7 @@ class DiskEncryptionMenu(AbstractSubMenu): return None - def _prev_disk_layouts(self) -> Optional[str]: + def _prev_partitions(self) -> Optional[str]: partitions: Optional[List[PartitionModification]] = self._menu_options['partitions'].current_selection if partitions: output = str(_('Partitions to be encrypted')) + '\n' @@ -106,6 +152,15 @@ class DiskEncryptionMenu(AbstractSubMenu): return None + def _prev_lvm_vols(self) -> Optional[str]: + volumes: Optional[List[PartitionModification]] = self._menu_options['lvm_vols'].current_selection + if volumes: + output = str(_('LVM volumes to be encrypted')) + '\n' + output += FormattedOutput.as_table(volumes) + return output.rstrip() + + return None + def _prev_hsm(self) -> Optional[str]: try: Fido2.get_fido2_devices() @@ -123,13 +178,19 @@ class DiskEncryptionMenu(AbstractSubMenu): return None -def select_encryption_type(preset: EncryptionType) -> Optional[EncryptionType]: +def select_encryption_type(disk_config: DiskLayoutConfiguration, preset: EncryptionType) -> Optional[EncryptionType]: title = str(_('Select disk encryption option')) - options = [ - EncryptionType.type_to_text(EncryptionType.Luks) - ] + + if disk_config.lvm_config: + options = [ + EncryptionType.type_to_text(EncryptionType.LvmOnLuks), + EncryptionType.type_to_text(EncryptionType.LuksOnLvm) + ] + else: + options = [EncryptionType.type_to_text(EncryptionType.Luks)] preset_value = EncryptionType.type_to_text(preset) + choice = Menu(title, options, preset_values=preset_value).run() match choice.type_: @@ -197,3 +258,31 @@ def select_partitions_to_encrypt( case MenuSelectionType.Selection: return choice.multi_value return [] + + +def select_lvm_vols_to_encrypt( + lvm_config: LvmConfiguration, + preset: List[LvmVolume] +) -> List[LvmVolume]: + volumes: List[LvmVolume] = lvm_config.get_all_volumes() + + if volumes: + title = str(_('Select which LVM volumes to encrypt')) + partition_table = FormattedOutput.as_table(volumes) + + choice = TableMenu( + title, + table_data=(volumes, partition_table), + preset=preset, + multi=True + ).run() + + match choice.type_: + case MenuSelectionType.Reset: + return [] + case MenuSelectionType.Skip: + return preset + case MenuSelectionType.Selection: + return choice.multi_value + + return [] diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 49904c17..5a139534 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -4,7 +4,7 @@ import getpass from pathlib import Path from typing import List -from .device_model import PartitionModification, Fido2Device +from .device_model import Fido2Device from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes from ..output import error, info from ..exceptions import SysCallError @@ -72,16 +72,16 @@ class Fido2: def fido2_enroll( cls, hsm_device: Fido2Device, - part_mod: PartitionModification, + dev_path: Path, password: str ): - worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {part_mod.dev_path}", peek_output=True) + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}", peek_output=True) pw_inputted = False pin_inputted = False while worker.is_alive(): if pw_inputted is False: - if bytes(f"please enter current passphrase for disk {part_mod.dev_path}", 'UTF-8') in worker._trace_log.lower(): + if bytes(f"please enter current passphrase for disk {dev_path}", 'UTF-8') in worker._trace_log.lower(): worker.write(bytes(password, 'UTF-8')) pw_inputted = True elif pin_inputted is False: diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 9c6e6d35..5c11896e 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -3,13 +3,21 @@ from __future__ import annotations import signal import sys import time -from typing import Any, Optional, TYPE_CHECKING +from pathlib import Path +from typing import Any, Optional, TYPE_CHECKING, List, Dict, Set -from .device_model import DiskLayoutConfiguration, DiskLayoutType, PartitionTable, FilesystemType, DiskEncryption from .device_handler import device_handler +from .device_model import ( + DiskLayoutConfiguration, DiskLayoutType, PartitionTable, + FilesystemType, DiskEncryption, LvmVolumeGroup, + Size, Unit, SectorSize, PartitionModification, EncryptionType, + LvmVolume, LvmConfiguration +) from ..hardware import SysInfo -from ..output import debug +from ..luks import Luks2 from ..menu import Menu +from ..output import debug, info +from ..general import SysCommand if TYPE_CHECKING: _: Any @@ -52,13 +60,288 @@ class FilesystemHandler: for mod in device_mods: device_handler.partition(mod, partition_table=partition_table) - device_handler.format(mod, enc_conf=self._enc_config) - for part_mod in mod.partitions: - if part_mod.is_create_or_modify(): + if self._disk_config.lvm_config: + for mod in device_mods: + if boot_part := mod.get_boot_partition(): + debug(f'Formatting boot partition: {boot_part.dev_path}') + self._format_partitions( + [boot_part], + mod.device_path + ) + + self.perform_lvm_operations() + else: + for mod in device_mods: + self._format_partitions( + mod.partitions, + mod.device_path + ) + + for part_mod in mod.partitions: if part_mod.fs_type == FilesystemType.Btrfs: device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config) + def _format_partitions( + self, + partitions: List[PartitionModification], + device_path: Path + ): + """ + Format can be given an overriding path, for instance /dev/null to test + the formatting functionality and in essence the support for the given filesystem. + """ + + # don't touch existing partitions + create_or_modify_parts = [p for p in partitions if p.is_create_or_modify()] + + self._validate_partitions(create_or_modify_parts) + + # make sure all devices are unmounted + device_handler.umount_all_existing(device_path) + + for part_mod in create_or_modify_parts: + # partition will be encrypted + if self._enc_config is not None and part_mod in self._enc_config.partitions: + device_handler.format_encrypted( + part_mod.safe_dev_path, + part_mod.mapper_name, + part_mod.safe_fs_type, + self._enc_config + ) + else: + device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path) + + # synchronize with udev before using lsblk + SysCommand('udevadm settle') + + lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path) + + part_mod.partn = lsblk_info.partn + part_mod.partuuid = lsblk_info.partuuid + part_mod.uuid = lsblk_info.uuid + + def _validate_partitions(self, partitions: List[PartitionModification]): + checks = { + # verify that all partitions have a path set (which implies that they have been created) + lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'), + # crypto luks is not a valid file system type + lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError( + 'Crypto luks cannot be set as a filesystem type'), + # file system type must be set + lambda x: x.fs_type is None: ValueError('File system type must be set for modification') + } + + for check, exc in checks.items(): + found = next(filter(check, partitions), None) + if found is not None: + raise exc + + def perform_lvm_operations(self): + info('Setting up LVM config...') + + if not self._disk_config.lvm_config: + return + + if self._enc_config: + self._setup_lvm_encrypted( + self._disk_config.lvm_config, + self._enc_config + ) + else: + self._setup_lvm(self._disk_config.lvm_config) + self._format_lvm_vols(self._disk_config.lvm_config) + + def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption): + if enc_config.encryption_type == EncryptionType.LvmOnLuks: + enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False) + + self._setup_lvm(lvm_config, enc_mods) + self._format_lvm_vols(lvm_config) + + # export the lvm group safely otherwise the Luks cannot be closed + self._safely_close_lvm(lvm_config) + + for luks in enc_mods.values(): + luks.lock() + elif enc_config.encryption_type == EncryptionType.LuksOnLvm: + self._setup_lvm(lvm_config) + enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False) + self._format_lvm_vols(lvm_config, enc_vols) + + for luks in enc_vols.values(): + luks.lock() + + self._safely_close_lvm(lvm_config) + + def _safely_close_lvm(self, lvm_config: LvmConfiguration): + for vg in lvm_config.vol_groups: + for vol in vg.volumes: + device_handler.lvm_vol_change(vol, False) + + device_handler.lvm_export_vg(vg) + + def _setup_lvm( + self, + lvm_config: LvmConfiguration, + enc_mods: Dict[PartitionModification, Luks2] = {} + ): + self._lvm_create_pvs(lvm_config, enc_mods) + + for vg in lvm_config.vol_groups: + pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods) + + device_handler.lvm_vg_create(pv_dev_paths, vg.name) + + # figure out what the actual available size in the group is + vg_info = device_handler.lvm_group_info(vg.name) + + if not vg_info: + raise ValueError('Unable to fetch VG info') + + # the actual available LVM Group size will be smaller than the + # total PVs size due to reserved metadata storage etc. + # so we'll have a look at the total avail. size, check the delta + # to the desired sizes and subtract some equally from the actually + # created volume + avail_size = vg_info.vg_size + desired_size = sum([vol.length for vol in vg.volumes], Size(0, Unit.B, SectorSize.default())) + + delta = desired_size - avail_size + max_vol_offset = delta.convert(Unit.B) + + max_vol = max(vg.volumes, key=lambda x: x.length) + + for lv in vg.volumes: + offset = max_vol_offset if lv == max_vol else None + + debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}') + device_handler.lvm_vol_create(vg.name, lv, offset) + + while True: + debug('Fetching LVM volume info') + lv_info = device_handler.lvm_vol_info(lv.name) + if lv_info is not None: + break + + time.sleep(1) + + self._lvm_vol_handle_e2scrub(vg) + + def _format_lvm_vols( + self, + lvm_config: LvmConfiguration, + enc_vols: Dict[LvmVolume, Luks2] = {} + ): + for vol in lvm_config.get_all_volumes(): + if enc_vol := enc_vols.get(vol, None): + if not enc_vol.mapper_dev: + raise ValueError('No mapper device defined') + path = enc_vol.mapper_dev + else: + path = vol.safe_dev_path + + # wait a bit otherwise the mkfs will fail as it can't + # find the mapper device yet + device_handler.format(vol.fs_type, path) + + if vol.fs_type == FilesystemType.Btrfs: + device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options) + + def _lvm_create_pvs( + self, + lvm_config: LvmConfiguration, + enc_mods: Dict[PartitionModification, Luks2] = {} + ): + pv_paths: Set[Path] = set() + + for vg in lvm_config.vol_groups: + pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods) + + device_handler.lvm_pv_create(pv_paths) + + def _get_all_pv_dev_paths( + self, + pvs: List[PartitionModification], + enc_mods: Dict[PartitionModification, Luks2] = {} + ) -> Set[Path]: + pv_paths: Set[Path] = set() + + for pv in pvs: + if enc_pv := enc_mods.get(pv, None): + if mapper := enc_pv.mapper_dev: + pv_paths.add(mapper) + else: + pv_paths.add(pv.safe_dev_path) + + return pv_paths + + def _encrypt_lvm_vols( + self, + lvm_config: LvmConfiguration, + enc_config: DiskEncryption, + lock_after_create: bool = True + ) -> Dict[LvmVolume, Luks2]: + enc_vols: Dict[LvmVolume, Luks2] = {} + + for vol in lvm_config.get_all_volumes(): + if vol in enc_config.lvm_volumes: + luks_handler = device_handler.encrypt( + vol.safe_dev_path, + vol.mapper_name, + enc_config.encryption_password, + lock_after_create + ) + + enc_vols[vol] = luks_handler + + return enc_vols + + def _encrypt_partitions( + self, + enc_config: DiskEncryption, + lock_after_create: bool = True + ) -> Dict[PartitionModification, Luks2]: + enc_mods: Dict[PartitionModification, Luks2] = {} + + for mod in self._disk_config.device_modifications: + partitions = mod.partitions + + # don't touch existing partitions + filtered_part = [p for p in partitions if not p.exists()] + + self._validate_partitions(filtered_part) + + # make sure all devices are unmounted + device_handler.umount_all_existing(mod.device_path) + + enc_mods = {} + + for part_mod in filtered_part: + if part_mod in enc_config.partitions: + luks_handler = device_handler.encrypt( + part_mod.safe_dev_path, + part_mod.mapper_name, + enc_config.encryption_password, + lock_after_create=lock_after_create + ) + + enc_mods[part_mod] = luks_handler + + return enc_mods + + def _lvm_vol_handle_e2scrub(self, vol_gp: LvmVolumeGroup): + # from arch wiki: + # If a logical volume will be formatted with ext4, leave at least 256 MiB + # free space in the volume group to allow using e2scrub + if any([vol.fs_type == FilesystemType.Ext4 for vol in vol_gp.volumes]): + largest_vol = max(vol_gp.volumes, key=lambda x: x.length) + + device_handler.lvm_vol_reduce( + largest_vol.safe_dev_path, + Size(256, Unit.MiB, SectorSize.default()) + ) + def _do_countdown(self) -> bool: SIG_TRIGGER = False diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 823605e3..330f61a3 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -2,7 +2,7 @@ from __future__ import annotations import re from pathlib import Path -from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple +from typing import Any, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption @@ -38,21 +38,6 @@ class PartitioningList(ListManager): display_actions = list(self._actions.values()) super().__init__(prompt, device_partitions, display_actions[:2], display_actions[3:]) - def reformat(self, data: List[PartitionModification]) -> Dict[str, Optional[PartitionModification]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[PartitionModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, partition: PartitionModification) -> str: return str(_('Partition')) @@ -258,7 +243,6 @@ class PartitioningList(ListManager): while True: value = TextInput(prompt).run().strip() size: Optional[Size] = None - if not value: size = default else: diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index 48afa829..ea77149d 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -1,9 +1,8 @@ from pathlib import Path -from typing import Dict, List, Optional, Any, TYPE_CHECKING +from typing import List, Optional, Any, TYPE_CHECKING from .device_model import SubvolumeModification from ..menu import TextInput, ListManager -from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -18,21 +17,6 @@ class SubvolumeMenu(ListManager): ] super().__init__(prompt, btrfs_subvols, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[SubvolumeModification]) -> Dict[str, Optional[SubvolumeModification]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[SubvolumeModification]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, subvol in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = subvol - - return display_data - def selected_action_display(self, subvolume: SubvolumeModification) -> str: return str(subvolume.name) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index e65915db..1b5e779b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -14,7 +14,6 @@ from .models.audio_configuration import Audio, AudioConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration -from .storage import storage from .configuration import save_config from .interactions import add_number_of_parallel_downloads from .interactions import ask_additional_packages_to_install @@ -30,7 +29,6 @@ from .interactions import select_additional_repositories from .interactions import select_kernel from .utils.util import format_cols from .interactions import ask_ntp -from .interactions.disk_conf import select_disk_config if TYPE_CHECKING: _: Any @@ -38,7 +36,6 @@ if TYPE_CHECKING: class GlobalMenu(AbstractMenu): def __init__(self, data_store: Dict[str, Any]): - self._defined_text = str(_('Defined')) super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) def setup_selection_menu_options(self): @@ -54,20 +51,20 @@ class GlobalMenu(AbstractMenu): _('Locales'), lambda preset: self._locale_selection(preset), preview_func=self._prev_locale, - display_func=lambda x: self._defined_text if x else '') + display_func=lambda x: self.defined_text if x else '') self._menu_options['mirror_config'] = \ Selector( _('Mirrors'), lambda preset: self._mirror_configuration(preset), - display_func=lambda x: self._defined_text if x else '', + display_func=lambda x: self.defined_text if x else '', preview_func=self._prev_mirror_config ) self._menu_options['disk_config'] = \ Selector( _('Disk configuration'), lambda preset: self._select_disk_config(preset), - preview_func=self._prev_disk_layouts, - display_func=lambda x: self._display_disk_layout(x), + preview_func=self._prev_disk_config, + display_func=lambda x: self.defined_text if x else '', ) self._menu_options['disk_encryption'] = \ Selector( @@ -75,7 +72,8 @@ class GlobalMenu(AbstractMenu): lambda preset: self._disk_encryption(preset), preview_func=self._prev_disk_encryption, display_func=lambda x: self._display_disk_encryption(x), - dependencies=['disk_config']) + dependencies=['disk_config'] + ) self._menu_options['swap'] = \ Selector( _('Swap'), @@ -140,7 +138,7 @@ class GlobalMenu(AbstractMenu): Selector( _('Additional packages'), lambda preset: ask_additional_packages_to_install(preset), - display_func=lambda x: self._defined_text if x else '', + display_func=lambda x: self.defined_text if x else '', preview_func=self._prev_additional_pkgs, default=[]) self._menu_options['additional-repositories'] = \ @@ -247,14 +245,17 @@ class GlobalMenu(AbstractMenu): return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: - mods: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection - if not mods: + if not disk_config: # this should not happen as the encryption menu has the disk_config as dependency raise ValueError('No disk layout specified') + if not disk.DiskEncryption.validate_enc(disk_config): + return None + data_store: Dict[str, Any] = {} - disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() + disk_encryption = disk.DiskEncryptionMenu(disk_config, data_store, preset=preset).run() return disk_encryption def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: @@ -287,44 +288,35 @@ class GlobalMenu(AbstractMenu): return format_cols(packages, None) return None - def _prev_disk_layouts(self) -> Optional[str]: + def _prev_disk_config(self) -> Optional[str]: selector = self._menu_options['disk_config'] disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection + output = '' if disk_layout_conf: - device_mods: List[disk.DeviceModification] = \ - list(filter(lambda x: len(x.partitions) > 0, disk_layout_conf.device_modifications)) - - if device_mods: - output_partition = '{}: {}\n'.format(str(_('Configuration')), disk_layout_conf.config_type.display_msg()) - output_btrfs = '' + output += str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) - for mod in device_mods: - # create partition table - partition_table = FormattedOutput.as_table(mod.partitions) - - output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' - output_partition += partition_table + '\n' - - # create btrfs table - btrfs_partitions = list( - filter(lambda p: len(p.btrfs_subvols) > 0, mod.partitions) - ) - for partition in btrfs_partitions: - output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + if disk_layout_conf.lvm_config: + output += '\n{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg()) - output = output_partition + output_btrfs - return output.rstrip() + if output: + return output return None - def _display_disk_layout(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: + def _display_disk_config(self, current_value: Optional[disk.DiskLayoutConfiguration] = None) -> str: if current_value: return current_value.config_type.display_msg() return '' def _prev_disk_encryption(self) -> Optional[str]: + disk_config: Optional[disk.DiskLayoutConfiguration] = self._menu_options['disk_config'].current_selection + + if disk_config and not disk.DiskEncryption.validate_enc(disk_config): + return str(_('LVM disk encryption with more than 2 partitions is currently not supported')) + encryption: Optional[disk.DiskEncryption] = self._menu_options['disk_encryption'].current_selection + if encryption: enc_type = disk.EncryptionType.type_to_text(encryption.encryption_type) output = str(_('Encryption type')) + f': {enc_type}\n' @@ -332,6 +324,8 @@ class GlobalMenu(AbstractMenu): if encryption.partitions: output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n' + elif encryption.lvm_volumes: + output += 'LVM volumes: {} selected'.format(len(encryption.lvm_volumes)) + '\n' if encryption.hsm_device: output += f'HSM: {encryption.hsm_device.manufacturer}' @@ -425,10 +419,8 @@ class GlobalMenu(AbstractMenu): self, preset: Optional[disk.DiskLayoutConfiguration] = None ) -> Optional[disk.DiskLayoutConfiguration]: - disk_config = select_disk_config( - preset, - storage['arguments'].get('advanced', False) - ) + data_store: Dict[str, Any] = {} + disk_config = disk.DiskLayoutConfigurationMenu(preset, data_store).run() if disk_config != preset: self._menu_options['disk_encryption'].set_current_selection(None) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 37121118..8292a3be 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -52,7 +52,7 @@ class Installer: `Installer()` is the wrapper for most basic installation steps. It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things. """ - self.base_packages = base_packages or __packages__[:3] + self._base_packages = base_packages or __packages__[:3] self.kernels = kernels or ['linux'] self._disk_config = disk_config @@ -64,11 +64,11 @@ class Installer: self.helper_flags: Dict[str, Any] = {'base': False, 'bootloader': None} for kernel in self.kernels: - self.base_packages.append(kernel) + self._base_packages.append(kernel) # If using accessibility tools in the live environment, append those to the packages list if accessibility_tools_in_use(): - self.base_packages.extend(__accessibility_packages__) + self._base_packages.extend(__accessibility_packages__) self.post_base_install: List[Callable] = [] @@ -90,6 +90,8 @@ class Installer: self._fstab_entries: List[str] = [] self._zram_enabled = False + self._disable_fstrim = False + self.pacman = Pacman(self.target, storage['arguments'].get('silent', False)) def __enter__(self) -> 'Installer': @@ -198,31 +200,71 @@ class Installer: self._verify_service_stop() def mount_ordered_layout(self): - info('Mounting partitions in order') + debug('Mounting ordered layout') + + luks_handlers: Dict[Any, Luks2] = {} + + match self._disk_encryption.encryption_type: + case disk.EncryptionType.NoEncryption: + self._mount_lvm_layout() + case disk.EncryptionType.Luks: + luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) + case disk.EncryptionType.LvmOnLuks: + luks_handlers = self._prepare_luks_partitions(self._disk_encryption.partitions) + self._import_lvm() + self._mount_lvm_layout(luks_handlers) + case disk.EncryptionType.LuksOnLvm: + self._import_lvm() + luks_handlers = self._prepare_luks_lvm(self._disk_encryption.lvm_volumes) + self._mount_lvm_layout(luks_handlers) + + # mount all regular partitions + self._mount_partition_layout(luks_handlers) + + def _mount_partition_layout(self, luks_handlers: Dict[Any, Luks2]): + debug('Mounting partition layout') + + # do not mount any PVs part of the LVM configuration + pvs = [] + if self._disk_config.lvm_config: + pvs = self._disk_config.lvm_config.get_all_pvs() for mod in self._disk_config.device_modifications: + not_pv_part_mods = list(filter(lambda x: x not in pvs, mod.partitions)) + # partitions have to mounted in the right order on btrfs the mountpoint will # be empty as the actual subvolumes are getting mounted instead so we'll use # '/' just for sorting - sorted_part_mods = sorted(mod.partitions, key=lambda x: x.mountpoint or Path('/')) - - enc_partitions = [] - if self._disk_encryption.encryption_type is not disk.EncryptionType.NoEncryption: - enc_partitions = list(set(sorted_part_mods) & set(self._disk_encryption.partitions)) - - # attempt to decrypt all luks partitions - luks_handlers = self._prepare_luks_partitions(enc_partitions) + sorted_part_mods = sorted(not_pv_part_mods, key=lambda x: x.mountpoint or Path('/')) for part_mod in sorted_part_mods: if luks_handler := luks_handlers.get(part_mod): - # mount encrypted partition self._mount_luks_partition(part_mod, luks_handler) else: - # partition is not encrypted self._mount_partition(part_mod) - def _prepare_luks_partitions(self, partitions: List[disk.PartitionModification]) -> Dict[ - disk.PartitionModification, Luks2]: + def _mount_lvm_layout(self, luks_handlers: Dict[Any, Luks2] = {}): + lvm_config = self._disk_config.lvm_config + + if not lvm_config: + debug('No lvm config defined to be mounted') + return + + debug('Mounting LVM layout') + + for vg in lvm_config.vol_groups: + sorted_vol = sorted(vg.volumes, key=lambda x: x.mountpoint or Path('/')) + + for vol in sorted_vol: + if luks_handler := luks_handlers.get(vol): + self._mount_luks_volume(vol, luks_handler) + else: + self._mount_lvm_vol(vol) + + def _prepare_luks_partitions( + self, + partitions: List[disk.PartitionModification] + ) -> Dict[disk.PartitionModification, Luks2]: return { part_mod: disk.device_handler.unlock_luks2_dev( part_mod.dev_path, @@ -233,6 +275,33 @@ class Installer: if part_mod.mapper_name and part_mod.dev_path } + def _import_lvm(self): + lvm_config = self._disk_config.lvm_config + + if not lvm_config: + debug('No lvm config defined to be imported') + return + + for vg in lvm_config.vol_groups: + disk.device_handler.lvm_import_vg(vg) + + for vol in vg.volumes: + disk.device_handler.lvm_vol_change(vol, True) + + def _prepare_luks_lvm( + self, + lvm_volumes: List[disk.LvmVolume] + ) -> Dict[disk.LvmVolume, Luks2]: + return { + vol: disk.device_handler.unlock_luks2_dev( + vol.dev_path, + vol.mapper_name, + self._disk_encryption.encryption_password + ) + for vol in lvm_volumes + if vol.mapper_name and vol.dev_path + } + def _mount_partition(self, part_mod: disk.PartitionModification): # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint and part_mod.dev_path: @@ -246,14 +315,32 @@ class Installer: part_mod.mount_options ) + def _mount_lvm_vol(self, volume: disk.LvmVolume): + if volume.fs_type != disk.FilesystemType.Btrfs: + if volume.mountpoint and volume.dev_path: + target = self.target / volume.relative_mountpoint + disk.device_handler.mount(volume.dev_path, target, options=volume.mount_options) + + if volume.fs_type == disk.FilesystemType.Btrfs and volume.dev_path: + self._mount_btrfs_subvol(volume.dev_path, volume.btrfs_subvols, volume.mount_options) + def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2): - # it would be none if it's btrfs as the subvolumes will have the mountpoints defined - if part_mod.mountpoint and luks_handler.mapper_dev: - target = self.target / part_mod.relative_mountpoint - disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) + if part_mod.fs_type != disk.FilesystemType.Btrfs: + if part_mod.mountpoint and luks_handler.mapper_dev: + target = self.target / part_mod.relative_mountpoint + disk.device_handler.mount(luks_handler.mapper_dev, target, options=part_mod.mount_options) if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: - self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols) + self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols, part_mod.mount_options) + + def _mount_luks_volume(self, volume: disk.LvmVolume, luks_handler: Luks2): + if volume.fs_type != disk.FilesystemType.Btrfs: + if volume.mountpoint and luks_handler.mapper_dev: + target = self.target / volume.relative_mountpoint + disk.device_handler.mount(luks_handler.mapper_dev, target, options=volume.mount_options) + + if volume.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev: + self._mount_btrfs_subvol(luks_handler.mapper_dev, volume.btrfs_subvols, volume.mount_options) def _mount_btrfs_subvol( self, @@ -262,13 +349,23 @@ class Installer: mount_options: List[str] = [] ): for subvol in subvolumes: - disk.device_handler.mount( - dev_path, - self.target / subvol.relative_mountpoint, - options=mount_options + [f'subvol={subvol.name}'] - ) + mountpoint = self.target / subvol.relative_mountpoint + mount_options = mount_options + [f'subvol={subvol.name}'] + disk.device_handler.mount(dev_path, mountpoint, options=mount_options) def generate_key_files(self): + match self._disk_encryption.encryption_type: + case disk.EncryptionType.Luks: + self._generate_key_files_partitions() + case disk.EncryptionType.LuksOnLvm: + self._generate_key_file_lvm_volumes() + case disk.EncryptionType.LvmOnLuks: + # currently LvmOnLuks only supports a single + # partitioning layout (boot + partition) + # so we won't need any keyfile generation atm + pass + + def _generate_key_files_partitions(self): for part_mod in self._disk_encryption.partitions: gen_enc_file = self._disk_encryption.should_generate_encryption_file(part_mod) @@ -279,14 +376,36 @@ class Installer: ) if gen_enc_file and not part_mod.is_root(): - info(f'Creating key-file: {part_mod.dev_path}') + debug(f'Creating key-file: {part_mod.dev_path}') luks_handler.create_keyfile(self.target) if part_mod.is_root() and not gen_enc_file: if self._disk_encryption.hsm_device: disk.Fido2.fido2_enroll( self._disk_encryption.hsm_device, - part_mod, + part_mod.safe_dev_path, + self._disk_encryption.encryption_password + ) + + def _generate_key_file_lvm_volumes(self): + for vol in self._disk_encryption.lvm_volumes: + gen_enc_file = self._disk_encryption.should_generate_encryption_file(vol) + + luks_handler = Luks2( + vol.safe_dev_path, + mapper_name=vol.mapper_name, + password=self._disk_encryption.encryption_password + ) + + if gen_enc_file and not vol.is_root(): + info(f'Creating key-file: {vol.dev_path}') + luks_handler.create_keyfile(self.target) + + if vol.is_root() and not gen_enc_file: + if self._disk_encryption.hsm_device: + disk.Fido2.fido2_enroll( + self._disk_encryption.hsm_device, + vol.safe_dev_path, self._disk_encryption.encryption_password ) @@ -393,7 +512,7 @@ class Installer: for entry in self._fstab_entries: fp.write(f'{entry}\n') - def set_hostname(self, hostname: str, *args: str, **kwargs: str) -> None: + def set_hostname(self, hostname: str): with open(f'{self.target}/etc/hostname', 'w') as fh: fh.write(hostname + '\n') @@ -444,7 +563,7 @@ class Installer: (self.target / 'etc/locale.conf').write_text(f'LANG={lang_value}\n') return True - def set_timezone(self, zone: str, *args: str, **kwargs: str) -> bool: + def set_timezone(self, zone: str) -> bool: if not zone: return True if not len(zone): @@ -532,7 +651,7 @@ class Installer: if enable_services: # If we haven't installed the base yet (function called pre-maturely) if self.helper_flags.get('base', False) is False: - self.base_packages.append('iwd') + self._base_packages.append('iwd') # This function will be called after minimal_installation() # as a hook for post-installs. This hook is only needed if @@ -608,51 +727,98 @@ class Installer: return vendor.get_ucode() return None - def minimal_installation( - self, - testing: bool = False, - multilib: bool = False, - mkinitcpio: bool = True, - hostname: str = 'archinstall', - locale_config: LocaleConfiguration = LocaleConfiguration.default() - ): - _disable_fstrim = False + def _handle_partition_installation(self): + pvs = [] + if self._disk_config.lvm_config: + pvs = self._disk_config.lvm_config.get_all_pvs() + for mod in self._disk_config.device_modifications: for part in mod.partitions: - if part.fs_type is not None: - if (pkg := part.fs_type.installation_pkg) is not None: - self.base_packages.append(pkg) - if (module := part.fs_type.installation_module) is not None: + if part in pvs or part.fs_type is None: + continue + + if (pkg := part.fs_type.installation_pkg) is not None: + self._base_packages.append(pkg) + if (module := part.fs_type.installation_module) is not None: + self._modules.append(module) + if (binary := part.fs_type.installation_binary) is not None: + self._binaries.append(binary) + + # https://github.com/archlinux/archinstall/issues/1837 + if part.fs_type.fs_type_mount == 'btrfs': + self._disable_fstrim = True + + # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. + if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if 'fsck' in self._hooks: + self._hooks.remove('fsck') + + if part in self._disk_encryption.partitions: + if self._disk_encryption.hsm_device: + # Required by mkinitcpio to add support for fido2-device options + self.pacman.strap('libfido2') + + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') + else: + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') + + def _handle_lvm_installation(self): + if not self._disk_config.lvm_config: + return + + self.add_additional_packages('lvm2') + self._hooks.insert(self._hooks.index('filesystems') - 1, 'lvm2') + + for vg in self._disk_config.lvm_config.vol_groups: + for vol in vg.volumes: + if vol.fs_type is not None: + if (pkg := vol.fs_type.installation_pkg) is not None: + self._base_packages.append(pkg) + if (module := vol.fs_type.installation_module) is not None: self._modules.append(module) - if (binary := part.fs_type.installation_binary) is not None: + if (binary := vol.fs_type.installation_binary) is not None: self._binaries.append(binary) - # https://github.com/archlinux/archinstall/issues/1837 - if part.fs_type.fs_type_mount == 'btrfs': - _disable_fstrim = True + if vol.fs_type.fs_type_mount == 'btrfs': + self._disable_fstrim = True # There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed. - if part.fs_type.fs_type_mount == 'ntfs3' and part.mountpoint == self.target: + if vol.fs_type.fs_type_mount == 'ntfs3' and vol.mountpoint == self.target: if 'fsck' in self._hooks: self._hooks.remove('fsck') - if part in self._disk_encryption.partitions: - if self._disk_encryption.hsm_device: - # Required by mkinitcpio to add support for fido2-device options - self.pacman.strap('libfido2') + if self._disk_encryption.encryption_type in [disk.EncryptionType.LvmOnLuks, disk.EncryptionType.LuksOnLvm]: + if self._disk_encryption.hsm_device: + # Required by mkinitcpio to add support for fido2-device options + self.pacman.strap('libfido2') + + if 'sd-encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('lvm2') - 1, 'sd-encrypt') + else: + if 'encrypt' not in self._hooks: + self._hooks.insert(self._hooks.index('lvm2') - 1, 'encrypt') - if 'sd-encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'sd-encrypt') - else: - if 'encrypt' not in self._hooks: - self._hooks.insert(self._hooks.index('filesystems'), 'encrypt') + def minimal_installation( + self, + testing: bool = False, + multilib: bool = False, + mkinitcpio: bool = True, + hostname: str = 'archinstall', + locale_config: LocaleConfiguration = LocaleConfiguration.default() + ): + if self._disk_config.lvm_config: + self._handle_lvm_installation() + else: + self._handle_partition_installation() if not SysInfo.has_uefi(): - self.base_packages.append('grub') + self._base_packages.append('grub') if ucode := self._get_microcode(): (self.target / 'boot' / ucode).unlink(missing_ok=True) - self.base_packages.append(ucode.stem) + self._base_packages.append(ucode.stem) else: debug('Archinstall will not install any ucode.') @@ -673,7 +839,7 @@ class Installer: pacman_conf.apply() - self.pacman.strap(self.base_packages) + self.pacman.strap(self._base_packages) self.helper_flags['base-strapped'] = True pacman_conf.persist() @@ -685,7 +851,7 @@ class Installer: # https://github.com/archlinux/archinstall/issues/880 # https://github.com/archlinux/archinstall/issues/1837 # https://github.com/archlinux/archinstall/issues/1841 - if not _disable_fstrim: + if not self._disable_fstrim: self.enable_periodic_trim() # TODO: Support locale and timezone @@ -742,13 +908,24 @@ class Installer: return boot return None - def _get_root_partition(self) -> Optional[disk.PartitionModification]: - for mod in self._disk_config.device_modifications: - if root := mod.get_root_partition(): - return root + def _get_root(self) -> Optional[disk.PartitionModification | disk.LvmVolume]: + if self._disk_config.lvm_config: + return self._disk_config.lvm_config.get_root_volume() + else: + for mod in self._disk_config.device_modifications: + if root := mod.get_root_partition(): + return root return None - def _get_kernel_params( + def _get_luks_uuid_from_mapper_dev(self, mapper_dev_path: Path) -> str: + lsblk_info = disk.get_lsblk_info(mapper_dev_path, reverse=True, full_dev_path=True) + + if not lsblk_info.children or not lsblk_info.children[0].uuid: + raise ValueError('Unable to determine UUID of luks superblock') + + return lsblk_info.children[0].uuid + + def _get_kernel_params_partition( self, root_partition: disk.PartitionModification, id_root: bool = True, @@ -784,20 +961,74 @@ class Installer: debug(f'Identifying root partition by UUID: {root_partition.uuid}') kernel_parameters.append(f'root=UUID={root_partition.uuid}') + return kernel_parameters + + def _get_kernel_params_lvm( + self, + lvm: disk.LvmVolume + ) -> List[str]: + kernel_parameters = [] + + match self._disk_encryption.encryption_type: + case disk.EncryptionType.LvmOnLuks: + if not lvm.vg_name: + raise ValueError(f'Unable to determine VG name for {lvm.name}') + + pv_seg_info = disk.device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name) + + if not pv_seg_info: + raise ValueError(f'Unable to determine PV segment info for {lvm.vg_name}/{lvm.name}') + + uuid = self._get_luks_uuid_from_mapper_dev(pv_seg_info.pv_name) + + if self._disk_encryption.hsm_device: + debug(f'LvmOnLuks, encrypted root partition, HSM, identifying by UUID: {uuid}') + kernel_parameters.append(f'rd.luks.name={uuid}=cryptlvm root={lvm.safe_dev_path}') + else: + debug(f'LvmOnLuks, encrypted root partition, identifying by UUID: {uuid}') + kernel_parameters.append(f'cryptdevice=UUID={uuid}:cryptlvm root={lvm.safe_dev_path}') + case disk.EncryptionType.LuksOnLvm: + uuid = self._get_luks_uuid_from_mapper_dev(lvm.mapper_path) + + if self._disk_encryption.hsm_device: + debug(f'LuksOnLvm, encrypted root partition, HSM, identifying by UUID: {uuid}') + kernel_parameters.append(f'rd.luks.name={uuid}=root root=/dev/mapper/root') + else: + debug(f'LuksOnLvm, encrypted root partition, identifying by UUID: {uuid}') + kernel_parameters.append(f'cryptdevice=UUID={uuid}:root root=/dev/mapper/root') + case disk.EncryptionType.NoEncryption: + debug(f'Identifying root lvm by mapper device: {lvm.dev_path}') + kernel_parameters.append(f'root={lvm.safe_dev_path}') + + return kernel_parameters + + def _get_kernel_params( + self, + root: disk.PartitionModification | disk.LvmVolume, + id_root: bool = True, + partuuid: bool = True + ) -> List[str]: + kernel_parameters = [] + + if isinstance(root, disk.LvmVolume): + kernel_parameters = self._get_kernel_params_lvm(root) + else: + kernel_parameters = self._get_kernel_params_partition(root, id_root, partuuid) + # Zswap should be disabled when using zram. # https://github.com/archlinux/archinstall/issues/881 if self._zram_enabled: kernel_parameters.append('zswap.enabled=0') if id_root: - for sub_vol in root_partition.btrfs_subvols: + for sub_vol in root.btrfs_subvols: if sub_vol.is_root(): kernel_parameters.append(f'rootflags=subvol={sub_vol.name}') break kernel_parameters.append('rw') - kernel_parameters.append(f'rootfstype={root_partition.safe_fs_type.fs_type_mount}') + kernel_parameters.append(f'rootfstype={root.safe_fs_type.fs_type_mount}') kernel_parameters.extend(self._kernel_params) debug(f'kernel parameters: {" ".join(kernel_parameters)}') @@ -807,10 +1038,12 @@ class Installer: def _add_systemd_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification], uki_enabled: bool = False ): + debug('Installing systemd bootloader') + self.pacman.strap('efibootmgr') if not SysInfo.has_uefi(): @@ -882,7 +1115,7 @@ class Installer: f'# Created on: {self.init_time}' ) - options = 'options ' + ' '.join(self._get_kernel_params(root_partition)) + options = 'options ' + ' '.join(self._get_kernel_params(root)) for kernel in self.kernels: for variant in ("", "-fallback"): @@ -904,15 +1137,17 @@ class Installer: def _add_grub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification] ): + debug('Installing grub bootloader') + self.pacman.strap('grub') # no need? grub_default = self.target / 'etc/default/grub' config = grub_default.read_text() - kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False)) + kernel_parameters = ' '.join(self._get_kernel_params(root, False, False)) config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1) grub_default.write_text(config) @@ -934,7 +1169,7 @@ class Installer: info(f"GRUB EFI partition: {efi_partition.dev_path}") - self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? boot_dir_arg = [] if boot_partition.mountpoint and boot_partition.mountpoint != boot_dir: @@ -988,8 +1223,10 @@ class Installer: self, boot_partition: disk.PartitionModification, efi_partition: Optional[disk.PartitionModification], - root_partition: disk.PartitionModification + root: disk.PartitionModification | disk.LvmVolume ): + debug('Installing limine bootloader') + self.pacman.strap('limine') info(f"Limine boot partition: {boot_partition.dev_path}") @@ -1052,7 +1289,7 @@ Exec = /bin/sh -c "{hook_command}" hook_path = hooks_dir / '99-limine.hook' hook_path.write_text(hook_contents) - kernel_params = ' '.join(self._get_kernel_params(root_partition)) + kernel_params = ' '.join(self._get_kernel_params(root)) config_contents = 'TIMEOUT=5\n' for kernel in self.kernels: @@ -1075,9 +1312,11 @@ Exec = /bin/sh -c "{hook_command}" def _add_efistub_bootloader( self, boot_partition: disk.PartitionModification, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, uki_enabled: bool = False ): + debug('Installing efistub bootloader') + self.pacman.strap('efibootmgr') if not SysInfo.has_uefi(): @@ -1092,7 +1331,7 @@ Exec = /bin/sh -c "{hook_command}" entries = ( 'initrd=/initramfs-{kernel}.img', - *self._get_kernel_params(root_partition) + *self._get_kernel_params(root) ) cmdline = [' '.join(entries)] @@ -1122,7 +1361,7 @@ Exec = /bin/sh -c "{hook_command}" def _config_uki( self, - root_partition: disk.PartitionModification, + root: disk.PartitionModification | disk.LvmVolume, efi_partition: Optional[disk.PartitionModification] ): if not efi_partition or not efi_partition.mountpoint: @@ -1130,7 +1369,7 @@ Exec = /bin/sh -c "{hook_command}" # Set up kernel command line with open(self.target / 'etc/kernel/cmdline', 'w') as cmdline: - kernel_parameters = self._get_kernel_params(root_partition) + kernel_parameters = self._get_kernel_params(root) cmdline.write(' '.join(kernel_parameters) + '\n') diff_mountpoint = None @@ -1191,37 +1430,33 @@ Exec = /bin/sh -c "{hook_command}" efi_partition = self._get_efi_partition() boot_partition = self._get_boot_partition() - root_partition = self._get_root_partition() + root = self._get_root() if boot_partition is None: raise ValueError(f'Could not detect boot at mountpoint {self.target}') - if root_partition is None: + if root is None: raise ValueError(f'Could not detect root at mountpoint {self.target}') info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}') if uki_enabled: - self._config_uki(root_partition, efi_partition) + self._config_uki(root, efi_partition) match bootloader: case Bootloader.Systemd: - self._add_systemd_bootloader(boot_partition, root_partition, efi_partition, uki_enabled) + self._add_systemd_bootloader(boot_partition, root, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root_partition, efi_partition) + self._add_grub_bootloader(boot_partition, root, efi_partition) case Bootloader.Efistub: - self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled) + self._add_efistub_bootloader(boot_partition, root, uki_enabled) case Bootloader.Limine: - self._add_limine_bootloader(boot_partition, efi_partition, root_partition) + self._add_limine_bootloader(boot_partition, efi_partition, root) def add_additional_packages(self, packages: Union[str, List[str]]) -> bool: return self.pacman.strap(packages) - def _enable_users(self, service: str, users: List[User]): - for user in users: - self.arch_chroot(f'systemctl enable --user {service}', run_as=user.username) - - def enable_sudo(self, entity: str, group :bool = False): + def enable_sudo(self, entity: str, group: bool = False): info(f'Enabling sudo permissions for {entity}') sudoers_dir = f"{self.target}/etc/sudoers.d" @@ -1237,7 +1472,7 @@ Exec = /bin/sh -c "{hook_command}" # We count how many files are there already so we know which number to prefix the file with num_of_rules_already = len(os.listdir(sudoers_dir)) - file_num_str = "{:02d}".format(num_of_rules_already) # We want 00_user1, 01_user2, etc + file_num_str = "{:02d}".format(num_of_rules_already) # We want 00_user1, 01_user2, etc # Guarantees that entity str does not contain invalid characters for a linux file name: # \ / : * ? " < > | @@ -1293,7 +1528,7 @@ Exec = /bin/sh -c "{hook_command}" if sudo and self.enable_sudo(user): self.helper_flags['user'] = True - def user_set_pw(self, user :str, password :str) -> bool: + def user_set_pw(self, user: str, password: str) -> bool: info(f'Setting password for {user}') if user == 'root': @@ -1310,7 +1545,7 @@ Exec = /bin/sh -c "{hook_command}" except SysCallError: return False - def user_set_shell(self, user :str, shell :str) -> bool: + def user_set_shell(self, user: str, shell: str) -> bool: info(f'Setting shell for {user} to {shell}') try: @@ -1319,7 +1554,7 @@ Exec = /bin/sh -c "{hook_command}" except SysCallError: return False - def chown(self, owner :str, path :str, options :List[str] = []) -> bool: + def chown(self, owner: str, path: str, options: List[str] = []) -> bool: cleaned_path = path.replace('\'', '\\\'') try: SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c 'chown {' '.join(options)} {owner} {cleaned_path}'") diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 9d0042d6..f80af9ca 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -58,7 +58,7 @@ def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: case MenuSelectionType.Reset: return [] case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: - selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_device_info: List[disk._DeviceInfo] = choice.single_value selected_devices = [] for device in devices: @@ -73,7 +73,6 @@ def get_default_partition_layout( filesystem_type: Optional[disk.FilesystemType] = None, advanced_option: bool = False ) -> List[disk.DeviceModification]: - if len(devices) == 1: device_modification = suggest_single_disk_layout( devices[0], @@ -133,7 +132,7 @@ def select_disk_config( case MenuSelectionType.Reset: return None case MenuSelectionType.Selection: if choice.single_value == pre_mount_mode: - output = "You will use whatever drive-setup is mounted at the specified directory\n" + output = 'You will use whatever drive-setup is mounted at the specified directory\n' output += "WARNING: Archinstall won't check the suitability of this setup\n" try: @@ -151,7 +150,6 @@ def select_disk_config( ) preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - devices = select_devices(preset_devices) if not devices: @@ -177,6 +175,36 @@ def select_disk_config( return None +def select_lvm_config( + disk_config: disk.DiskLayoutConfiguration, + preset: Optional[disk.LvmConfiguration] = None, +) -> Optional[disk.LvmConfiguration]: + default_mode = disk.LvmLayoutType.Default.display_msg() + + options = [default_mode] + + preset_value = preset.config_type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select a LVM option'), + options, + allow_reset=True, + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + if choice.single_value == default_mode: + return suggest_lvm_layout(disk_config) + return preset + + def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.PartitionModification: flags = [disk.PartitionFlag.Boot] if using_gpt: @@ -199,7 +227,7 @@ def _boot_partition(sector_size: disk.SectorSize, using_gpt: bool) -> disk.Parti ) -def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: +def select_main_filesystem_format(advanced_options: bool = False) -> disk.FilesystemType: options = { 'btrfs': disk.FilesystemType.Btrfs, 'ext4': disk.FilesystemType.Ext4, @@ -250,7 +278,6 @@ def suggest_single_disk_layout( prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() using_subvolumes = choice.value == Menu.yes() - mount_options = select_mount_options() device_modification = disk.DeviceModification(device, wipe=True) @@ -288,7 +315,11 @@ def suggest_single_disk_layout( root_start = boot_partition.start + boot_partition.length # Set a size for / (/root) - if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + if ( + using_subvolumes + or device_size_gib < min_size_to_allow_home_part + or not using_home_partition + ): root_length = device.device_info.total_size - root_start else: root_length = min(device.device_info.total_size, root_partition_size) @@ -305,6 +336,7 @@ def suggest_single_disk_layout( fs_type=filesystem_type, mount_options=mount_options ) + device_modification.add_partition(root_partition) if using_subvolumes: @@ -388,9 +420,9 @@ def suggest_multi_disk_layout( device_paths = ', '.join([str(d.device_info.path) for d in devices]) - debug(f"Suggesting multi-disk-layout for devices: {device_paths}") - debug(f"/root: {root_device.device_info.path}") - debug(f"/home: {home_device.device_info.path}") + debug(f'Suggesting multi-disk-layout for devices: {device_paths}') + debug(f'/root: {root_device.device_info.path}') + debug(f'/home: {home_device.device_info.path}') root_device_modification = disk.DeviceModification(root_device, wipe=True) home_device_modification = disk.DeviceModification(home_device, wipe=True) @@ -444,3 +476,85 @@ def suggest_multi_disk_layout( home_device_modification.add_partition(home_partition) return [root_device_modification, home_device_modification] + + +def suggest_lvm_layout( + disk_config: disk.DiskLayoutConfiguration, + filesystem_type: Optional[disk.FilesystemType] = None, + vg_grp_name: str = 'ArchinstallVg', +) -> disk.LvmConfiguration: + if disk_config.config_type != disk.DiskLayoutType.Default: + raise ValueError('LVM suggested volumes are only available for default partitioning') + + using_subvolumes = False + btrfs_subvols = [] + home_volume = True + mount_options = [] + + if not filesystem_type: + filesystem_type = select_main_filesystem_format() + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + using_subvolumes = choice.value == Menu.yes() + + mount_options = select_mount_options() + + if using_subvolumes: + btrfs_subvols = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')), + ] + + home_volume = False + + boot_part: Optional[disk.PartitionModification] = None + other_part: List[disk.PartitionModification] = [] + + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.is_boot(): + boot_part = part + else: + other_part.append(part) + + if not boot_part: + raise ValueError('Unable to find boot partition in partition modifications') + + total_vol_available = sum( + [p.length for p in other_part], + disk.Size(0, disk.Unit.B, disk.SectorSize.default()), + ) + root_vol_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) + home_vol_size = total_vol_available - root_vol_size + + lvm_vol_group = disk.LvmVolumeGroup(vg_grp_name, pvs=other_part, ) + + root_vol = disk.LvmVolume( + status=disk.LvmVolumeStatus.Create, + name='root', + fs_type=filesystem_type, + length=root_vol_size, + mountpoint=Path('/'), + btrfs_subvols=btrfs_subvols, + mount_options=mount_options + ) + + lvm_vol_group.volumes.append(root_vol) + + if home_volume: + home_vol = disk.LvmVolume( + status=disk.LvmVolumeStatus.Create, + name='home', + fs_type=filesystem_type, + length=home_vol_size, + mountpoint=Path('/home'), + ) + + lvm_vol_group.volumes.append(home_vol) + + return disk.LvmConfiguration(disk.LvmLayoutType.Default, [lvm_vol_group]) diff --git a/archinstall/lib/interactions/manage_users_conf.py b/archinstall/lib/interactions/manage_users_conf.py index ca912283..886f85b6 100644 --- a/archinstall/lib/interactions/manage_users_conf.py +++ b/archinstall/lib/interactions/manage_users_conf.py @@ -1,12 +1,11 @@ from __future__ import annotations import re -from typing import Any, Dict, TYPE_CHECKING, List, Optional +from typing import Any, TYPE_CHECKING, List, Optional from .utils import get_password from ..menu import Menu, ListManager from ..models.users import User -from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -26,21 +25,6 @@ class UserList(ListManager): ] super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[User]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, user: User) -> str: return user.username diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index c917420e..50e15cee 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -60,7 +60,7 @@ class Luks2: iter_time: int = 10000, key_file: Optional[Path] = None ) -> Path: - info(f'Luks2 encrypting: {self.luks_dev_path}') + debug(f'Luks2 encrypting: {self.luks_dev_path}') byte_password = self._password_bytes() @@ -87,12 +87,15 @@ class Luks2: 'luksFormat', str(self.luks_dev_path), ]) + debug(f'cryptsetup format: {cryptsetup_args}') + # Retry formatting the volume because archinstall can some times be too quick # which generates a "Device /dev/sdX does not exist or access denied." between # setting up partitions and us trying to encrypt it. for retry_attempt in range(storage['DISK_RETRY_ATTEMPTS'] + 1): try: - SysCommand(cryptsetup_args) + result = SysCommand(cryptsetup_args).decode() + debug(f'cryptsetup luksFormat output: {result}') break except SysCallError as err: time.sleep(storage['DISK_TIMEOUTS']) @@ -106,10 +109,13 @@ class Luks2: self.lock() # Then try again to set up the crypt-device - SysCommand(cryptsetup_args) + result = SysCommand(cryptsetup_args).decode() + debug(f'cryptsetup luksFormat output: {result}') else: raise DiskError(f'Could not encrypt volume "{self.luks_dev_path}": {err}') + self.key_file = key_file + return key_file def _get_luks_uuid(self) -> str: @@ -152,7 +158,15 @@ class Luks2: while Path(self.luks_dev_path).exists() is False and time.time() - wait_timer < 10: time.sleep(0.025) - SysCommand(f'/usr/bin/cryptsetup open {self.luks_dev_path} {self.mapper_name} --key-file {key_file} --type luks2') + result = SysCommand( + '/usr/bin/cryptsetup open ' + f'{self.luks_dev_path} ' + f'{self.mapper_name} ' + f'--key-file {key_file} ' + f'--type luks2' + ).decode() + + debug(f'cryptsetup open output: {result}') if not self.mapper_dev or not self.mapper_dev.is_symlink(): raise DiskError(f'Failed to open luks2 device: {self.luks_dev_path}') @@ -199,8 +213,8 @@ class Luks2: key_file.parent.mkdir(parents=True, exist_ok=True) - with open(key_file, "w") as keyfile: - keyfile.write(generate_password(length=512)) + pwd = generate_password(length=512) + key_file.write_text(pwd) key_file.chmod(0o400) @@ -208,7 +222,7 @@ class Luks2: self._crypttab(crypttab_path, kf_path, options=["luks", "key-slot=1"]) def _add_key(self, key_file: Path): - info(f'Adding additional key-file {key_file}') + debug(f'Adding additional key-file {key_file}') command = f'/usr/bin/cryptsetup -q -v luksAddKey {self.luks_dev_path} {key_file}' worker = SysCommandWorker(command, environment_vars={'LC_ALL': 'C'}) @@ -228,7 +242,7 @@ class Luks2: key_file: Path, options: List[str] ) -> None: - info(f'Adding crypttab entry for key {key_file}') + debug(f'Adding crypttab entry for key {key_file}') with open(crypttab_path, 'a') as crypttab: opt = ','.join(options) diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 14db98ca..ee55f5c9 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -10,6 +10,7 @@ from ..translationhandler import TranslationHandler, Language if TYPE_CHECKING: _: Any + class Selector: def __init__( self, @@ -68,42 +69,19 @@ class Selector: :param no_store: A boolean which determines that the field should or shouldn't be stored in the data storage :type no_store: bool """ - self._description = description - self.func = func self._display_func = display_func - self._current_selection = default + self._no_store = no_store + + self.description = description + self.func = func + self.current_selection = default self.enabled = enabled - self._dependencies = dependencies - self._dependencies_not = dependencies_not + self.dependencies = dependencies + self.dependencies_not = dependencies_not self.exec_func = exec_func - self._preview_func = preview_func + self.preview_func = preview_func self.mandatory = mandatory - self._no_store = no_store - self._default = default - - @property - def default(self) -> Any: - return self._default - - @property - def description(self) -> str: - return self._description - - @property - def dependencies(self) -> List: - return self._dependencies - - @property - def dependencies_not(self) -> List: - return self._dependencies_not - - @property - def current_selection(self) -> Optional[Any]: - return self._current_selection - - @property - def preview_func(self): - return self._preview_func + self.default = default def do_store(self) -> bool: return self._no_store is False @@ -112,45 +90,45 @@ class Selector: self.enabled = status def update_description(self, description: str): - self._description = description + self.description = description def menu_text(self, padding: int = 0) -> str: - if self._description == '': # special menu option for __separator__ + if self.description == '': # special menu option for __separator__ return '' current = '' if self._display_func: - current = self._display_func(self._current_selection) + current = self._display_func(self.current_selection) else: - if self._current_selection is not None: - current = str(self._current_selection) + if self.current_selection is not None: + current = str(self.current_selection) if current: padding += 5 - description = unicode_ljust(str(self._description), padding, ' ') + description = unicode_ljust(str(self.description), padding, ' ') current = current else: - description = self._description + description = self.description current = '' return f'{description} {current}' def set_current_selection(self, current: Optional[Any]): - self._current_selection = current + self.current_selection = current def has_selection(self) -> bool: - if not self._current_selection: + if not self.current_selection: return False return True def get_selection(self) -> Any: - return self._current_selection + return self.current_selection def is_empty(self) -> bool: - if self._current_selection is None: + if self.current_selection is None: return True - elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0: + elif isinstance(self.current_selection, (str, list, dict)) and len(self.current_selection) == 0: return True return False @@ -197,6 +175,8 @@ class AbstractMenu: self._sync_all() self._populate_default_values() + self.defined_text = str(_('Defined')) + @property def last_choice(self): return self._last_choice @@ -382,9 +362,10 @@ class AbstractMenu: result = None if selector.func is not None: - presel_val = self.option(config_name).get_selection() - result = selector.func(presel_val) + cur_value = self.option(config_name).get_selection() + result = selector.func(cur_value) self._menu_options[config_name].set_current_selection(result) + if selector.do_store(): self._data_store[config_name] = result @@ -398,19 +379,23 @@ class AbstractMenu: return True def _verify_selection_enabled(self, selection_name: str) -> bool: - """ general """ if selection := self._menu_options.get(selection_name, None): if not selection.enabled: return False if len(selection.dependencies) > 0: - for d in selection.dependencies: - if not self._verify_selection_enabled(d) or self._menu_options[d].is_empty(): - return False + for dep in selection.dependencies: + if isinstance(dep, str): + if not self._verify_selection_enabled(dep) or self._menu_options[dep].is_empty(): + return False + elif callable(dep): # callable dependency eval + return dep() + else: + raise ValueError(f'Unsupported dependency: {selection_name}') if len(selection.dependencies_not) > 0: - for d in selection.dependencies_not: - if not self._menu_options[d].is_empty(): + for dep in selection.dependencies_not: + if not self._menu_options[dep].is_empty(): return False return True @@ -454,8 +439,8 @@ class AbstractMenu: class AbstractSubMenu(AbstractMenu): - def __init__(self, data_store: Dict[str, Any] = {}): - super().__init__(data_store=data_store) + def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2): + super().__init__(data_store=data_store, preview_size=preview_size) self._menu_options['__separator__'] = Selector('') self._menu_options['back'] = \ diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 54fb6a1b..de18791c 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -3,6 +3,7 @@ from os import system from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List from .menu import Menu +from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -127,18 +128,29 @@ class ListManager: if choice.value and choice.value != self._cancel_action: self._data = self.handle_action(choice.value, entry, self._data) - def selected_action_display(self, selection: Any) -> str: + def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]: """ - this will return the value to be displayed in the - "Select an action for '{}'" string + Default implementation of the table to be displayed. + Override if any custom formatting is needed """ - raise NotImplementedError('Please implement me in the child class') + table = FormattedOutput.as_table(data) + rows = table.split('\n') - def reformat(self, data: List[Any]) -> Dict[str, Optional[Any]]: + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Any]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, entry in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = entry + + return display_data + + def selected_action_display(self, selection: Any) -> str: """ - this should return a dictionary of display string to actual data entry - mapping; if the value for a given display string is None it will be used - in the header value (useful when displaying tables) + this will return the value to be displayed in the + "Select an action for '{}'" string """ raise NotImplementedError('Please implement me in the child class') diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index f14b855d..38301d3a 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -66,7 +66,7 @@ class Menu(TerminalMenu): sort: bool = True, preset_values: Optional[Union[str, List[str]]] = None, cursor_index: Optional[int] = None, - preview_command: Optional[Callable] = None, + preview_command: Optional[Callable[[Any], str | None]] = None, preview_size: float = 0.0, preview_title: str = 'Info', header: Union[List[str], str] = [], @@ -228,7 +228,11 @@ class Menu(TerminalMenu): default_str = str(_('(default)')) return f'{self._default_option} {default_str}' - def _show_preview(self, preview_command: Optional[Callable], selection: str) -> Optional[str]: + def _show_preview( + self, + preview_command: Optional[Callable[[Any], str | None]], + selection: str + ) -> Optional[str]: if selection == self.back(): return None diff --git a/archinstall/lib/menu/table_selection_menu.py b/archinstall/lib/menu/table_selection_menu.py index 4cff7216..fec6ae59 100644 --- a/archinstall/lib/menu/table_selection_menu.py +++ b/archinstall/lib/menu/table_selection_menu.py @@ -19,6 +19,7 @@ class TableMenu(Menu): preview_size: float = 0.0, allow_reset: bool = True, allow_reset_warning_msg: Optional[str] = None, + skip: bool = True ): """ param title: Text that will be displayed above the menu @@ -81,7 +82,8 @@ class TableMenu(Menu): preview_title=preview_title, extra_bottom_space=extra_bottom_space, allow_reset=allow_reset, - allow_reset_warning_msg=allow_reset_warning_msg + allow_reset_warning_msg=allow_reset_warning_msg, + skip=skip ) def _preset_values(self, preset: List[Any]) -> List[str]: diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 61f3c568..c9094669 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -121,21 +121,6 @@ class CustomMirrorList(ListManager): ] super().__init__(prompt, custom_mirrors, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[CustomMirror]) -> Dict[str, Any]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[CustomMirror]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, user in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = user - - return display_data - def selected_action_display(self, mirror: CustomMirror) -> str: return mirror.name diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index b1fc8fd9..385ff500 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -104,7 +104,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - info('Starting installation') + info('Starting installation...') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 3c9a5876..4513b6f2 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -82,7 +82,7 @@ def perform_installation(mountpoint: Path): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - info('Starting installation') + info('Starting installation...') disk_config: disk.DiskLayoutConfiguration = archinstall.arguments['disk_config'] # Retrieve list of additional repositories and set boolean values appropriately -- cgit v1.2.3-54-g00ecf From 8f5bc523db80729901d8179ed30209576a06f4ca Mon Sep 17 00:00:00 2001 From: Hao Xiang Date: Tue, 16 Apr 2024 18:56:44 +0800 Subject: Fix null fs_type of partition (#2458) (#2459) --- archinstall/lib/disk/device_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 1cd3d674..f8873495 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -114,7 +114,7 @@ class DiskLayoutConfiguration: for partition in entry.get('partitions', []): device_partition = PartitionModification( status=ModificationStatus(partition['status']), - fs_type=FilesystemType(partition['fs_type']), + fs_type=FilesystemType(partition['fs_type']) if partition.get('fs_type') else None, start=Size.parse_args(partition['start']), length=Size.parse_args(partition['size']), mount_options=partition['mount_options'], @@ -663,7 +663,7 @@ class PartitionModification: type: PartitionType start: Size length: Size - fs_type: Optional[FilesystemType] + fs_type: Optional[FilesystemType] = None mountpoint: Optional[Path] = None mount_options: List[str] = field(default_factory=list) flags: List[PartitionFlag] = field(default_factory=list) @@ -815,7 +815,7 @@ class PartitionModification: 'type': self.type.value, 'start': self.start.json(), 'size': self.length.json(), - 'fs_type': self.fs_type.value if self.fs_type else '', + 'fs_type': self.fs_type.value if self.fs_type else None, 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, 'flags': [f.name for f in self.flags], -- cgit v1.2.3-54-g00ecf From db798eec717a58538cffb712a6689434333dbd82 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 16 Apr 2024 22:05:19 +1000 Subject: Fix 2445 - handle no free spaces and deleted parittions (#2448) --- archinstall/lib/disk/device_model.py | 8 ++-- archinstall/lib/disk/partitioning_menu.py | 68 +++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 10 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index f8873495..50de3ebc 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -696,6 +696,10 @@ class PartitionModification: def __hash__(self): return hash(self._obj_id) + @property + def end(self) -> Size: + return self.start + self.length + @property def obj_id(self) -> str: if hasattr(self, '_obj_id'): @@ -827,14 +831,12 @@ class PartitionModification: """ Called for displaying data in table format """ - end = self.start + self.length - part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, 'Start': self.start.format_size(Unit.sectors, self.start.sector_size, include_unit=False), - 'End': end.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'End': self.end.format_size(Unit.sectors, self.start.sector_size, include_unit=False), 'Size': self.length.format_highest(), 'FS type': self.fs_type.value if self.fs_type else 'Unknown', 'Mountpoint': self.mountpoint if self.mountpoint else '', diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 330f61a3..fb1eb74b 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -3,9 +3,13 @@ from __future__ import annotations import re from pathlib import Path from typing import Any, TYPE_CHECKING, List, Optional, Tuple +from dataclasses import dataclass -from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ +from .device_model import ( + PartitionModification, FilesystemType, BDevice, + Size, Unit, PartitionType, PartitionFlag, ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption +) from ..hardware import SysInfo from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn @@ -15,6 +19,12 @@ if TYPE_CHECKING: _: Any +@dataclass +class DefaultFreeSector: + start: Size + end: Size + + class PartitioningList(ListManager): """ subclass of ListManager for the managing of user accounts @@ -268,21 +278,27 @@ class PartitioningList(ListManager): prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n' print(prompt) - largest_free_area: DeviceGeometry = max(device_info.free_space_regions, key=lambda r: r.get_length()) + default_free_sector = self._find_default_free_space() + + if not default_free_sector: + default_free_sector = DefaultFreeSector( + Size(0, Unit.sectors, self._device.device_info.sector_size), + Size(0, Unit.sectors, self._device.device_info.sector_size) + ) # prompt until a valid start sector was entered - default_start = Size(largest_free_area.start, Unit.sectors, device_info.sector_size) - start_prompt = str(_('Enter start (default: sector {}): ')).format(largest_free_area.start) + start_prompt = str(_('Enter start (default: sector {}): ')).format(default_free_sector.start.value) + start_size = self._enter_size( device_info.sector_size, device_info.total_size, start_prompt, - default_start, + default_free_sector.start, None ) - if start_size.value == largest_free_area.start: - end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size) + if start_size.value == default_free_sector.start.value and default_free_sector.end.value != 0: + end_size = default_free_sector.end else: end_size = device_info.total_size @@ -298,6 +314,44 @@ class PartitioningList(ListManager): return start_size, end_size + def _find_default_free_space(self) -> Optional[DefaultFreeSector]: + device_info = self._device.device_info + + largest_free_area: Optional[DeviceGeometry] = None + largest_deleted_area: Optional[PartitionModification] = None + + if len(device_info.free_space_regions) > 0: + largest_free_area = max(device_info.free_space_regions, key=lambda r: r.get_length()) + + deleted_partitions = list(filter(lambda x: x.status == ModificationStatus.Delete, self._data)) + if len(deleted_partitions) > 0: + largest_deleted_area = max(deleted_partitions, key=lambda p: p.length) + + def _free_space(space: DeviceGeometry) -> DefaultFreeSector: + start = Size(space.start, Unit.sectors, device_info.sector_size) + end = Size(space.end, Unit.sectors, device_info.sector_size) + return DefaultFreeSector(start, end) + + def _free_deleted(space: PartitionModification) -> DefaultFreeSector: + start = space.start.convert(Unit.sectors, self._device.device_info.sector_size) + end = space.end.convert(Unit.sectors, self._device.device_info.sector_size) + return DefaultFreeSector(start, end) + + if not largest_deleted_area and largest_free_area: + return _free_space(largest_free_area) + elif not largest_free_area and largest_deleted_area: + return _free_deleted(largest_deleted_area) + elif not largest_deleted_area and not largest_free_area: + return None + elif largest_free_area and largest_deleted_area: + free_space = _free_space(largest_free_area) + if free_space.start > largest_deleted_area.start: + return free_space + else: + return _free_deleted(largest_deleted_area) + + return None + def _create_new_partition(self) -> PartitionModification: fs_type = self._prompt_partition_fs_type() -- cgit v1.2.3-54-g00ecf From cd587792e1e8f429f2dcd3c8e9f745d80cb28f38 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 16 Apr 2024 22:06:03 +1000 Subject: Fix master merge conflicts from LVM branch (#2463) --- archinstall/lib/disk/device_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 50de3ebc..7edaa5b6 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -1429,8 +1429,7 @@ def _clean_field(name: str, clean_type: CleanType) -> str: def _fetch_lsblk_info( dev_path: Optional[Union[Path, str]] = None, reverse: bool = False, - full_dev_path: bool = False, - retry: int = 3 + full_dev_path: bool = False ) -> List[LsblkInfo]: fields = [_clean_field(f, CleanType.Lsblk) for f in LsblkInfo.fields()] cmd = ['lsblk', '--json', '--bytes', '--output', '+' + ','.join(fields)] -- cgit v1.2.3-54-g00ecf From beeb9d7fde8db7ed0dc9fb10cf7eec27f33b4a82 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 23 Apr 2024 17:02:07 +1000 Subject: Fix 2191 - Handle unknown device/partition type (#2470) --- archinstall/lib/disk/device_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 7edaa5b6..5736b423 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -19,7 +19,6 @@ from ..exceptions import DiskError, SysCallError from ..general import SysCommand from ..output import debug, error from ..storage import storage -from ..output import info if TYPE_CHECKING: _: Any @@ -449,8 +448,11 @@ class _DeviceInfo: device = disk.device if device.type == 18: device_type = 'loop' - else: + elif device.type in parted.devices: device_type = parted.devices[device.type] + else: + debug(f'Device code not unknown: {device.type}') + device_type = parted.devices[parted.DEVICE_UNKNOWN] sector_size = SectorSize(device.sectorSize, Unit.B) free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] @@ -568,7 +570,7 @@ class PartitionType(Enum): if code == parted.PARTITION_NORMAL: return PartitionType.Primary else: - info(f'Partition code not supported: {code}') + debug(f'Partition code not supported: {code}') return PartitionType._Unknown def get_partition_code(self) -> Optional[int]: -- cgit v1.2.3-54-g00ecf From bf368a94f59ba1d1d5c2df9450c086f1e6ed4020 Mon Sep 17 00:00:00 2001 From: correctmost <134317971+correctmost@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:32:57 +0000 Subject: Fix debug message for unknown device type (#2472) --- archinstall/lib/disk/device_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 5736b423..bf9df44e 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -451,7 +451,7 @@ class _DeviceInfo: elif device.type in parted.devices: device_type = parted.devices[device.type] else: - debug(f'Device code not unknown: {device.type}') + debug(f'Device code unknown: {device.type}') device_type = parted.devices[parted.DEVICE_UNKNOWN] sector_size = SectorSize(device.sectorSize, Unit.B) -- cgit v1.2.3-54-g00ecf From fd23efdcfb35d8b864b06331d9022d93dd5be712 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 10 May 2024 07:06:06 +1000 Subject: Fix 2479 - remove unnecessary partiton info field (#2485) --- archinstall/lib/disk/device_model.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'archinstall/lib/disk') diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index bf9df44e..f98d05fb 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -671,10 +671,6 @@ class PartitionModification: flags: List[PartitionFlag] = field(default_factory=list) btrfs_subvols: List[SubvolumeModification] = field(default_factory=list) - # only set when modification was created from an existing - # partition info object to be able to reference it back - part_info: Optional[_PartitionInfo] = None - # only set if the device was created or exists dev_path: Optional[Path] = None partn: Optional[int] = None @@ -745,8 +741,7 @@ class PartitionModification: uuid=partition_info.uuid, flags=partition_info.flags, mountpoint=mountpoint, - btrfs_subvols=subvol_mods, - part_info=partition_info + btrfs_subvols=subvol_mods ) @property -- cgit v1.2.3-54-g00ecf