From c3862c5779194f5e93f9fd2518bb15706c93ad2b Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 11 Nov 2022 19:40:05 +1100 Subject: New encryption menu (#1520) * New encryption menu Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum --- archinstall/lib/configuration.py | 14 +- archinstall/lib/disk/encryption.py | 162 +++++++ archinstall/lib/disk/filesystem.py | 23 +- archinstall/lib/disk/helpers.py | 6 - archinstall/lib/disk/partition.py | 1 - archinstall/lib/hsm/__init__.py | 5 +- archinstall/lib/hsm/fido.py | 137 +++--- archinstall/lib/installer.py | 36 +- archinstall/lib/menu/abstract_menu.py | 493 ++++++++++++++++++++ archinstall/lib/menu/global_menu.py | 94 ++-- archinstall/lib/menu/list_manager.py | 2 +- archinstall/lib/menu/menu.py | 43 +- archinstall/lib/menu/selection_menu.py | 497 --------------------- archinstall/lib/menu/table_selection_menu.py | 107 +++++ archinstall/lib/models/disk_encryption.py | 43 ++ archinstall/lib/user_interaction/__init__.py | 2 +- archinstall/lib/user_interaction/disk_conf.py | 10 +- archinstall/lib/user_interaction/general_conf.py | 26 +- archinstall/lib/user_interaction/locale_conf.py | 4 +- archinstall/lib/user_interaction/network_conf.py | 10 +- .../lib/user_interaction/partitioning_conf.py | 42 +- archinstall/lib/user_interaction/save_conf.py | 2 +- archinstall/lib/user_interaction/system_conf.py | 22 +- 23 files changed, 1039 insertions(+), 742 deletions(-) create mode 100644 archinstall/lib/disk/encryption.py create mode 100644 archinstall/lib/menu/abstract_menu.py delete mode 100644 archinstall/lib/menu/selection_menu.py create mode 100644 archinstall/lib/menu/table_selection_menu.py create mode 100644 archinstall/lib/models/disk_encryption.py (limited to 'archinstall/lib') diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 2a43174d..ce782f6c 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -5,15 +5,18 @@ import logging import pathlib from typing import Optional, Dict +from .hsm.fido import Fido2 +from .models.disk_encryption import DiskEncryption from .storage import storage from .general import JSON, UNSAFE_JSON from .output import log from .exceptions import RequirementError -from .hsm import get_fido2_devices + def configuration_sanity_check(): - if storage['arguments'].get('HSM'): - if not get_fido2_devices(): + disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') + if disk_encryption.hsm_device: + if not Fido2.get_fido2_devices(): raise RequirementError( f"In order to use HSM to pair with the disk encryption," + f" one needs to be accessible through /dev/hidraw* and support" @@ -21,6 +24,7 @@ def configuration_sanity_check(): + f" 'systemd-cryptenroll --fido2-device=list'." ) + class ConfigurationOutput: def __init__(self, config: Dict): """ @@ -39,8 +43,8 @@ class ConfigurationOutput: self._user_creds_file = "user_credentials.json" self._disk_layout_file = "user_disk_layout.json" - self._sensitive = ['!users', '!encryption-password'] - self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run'] + self._sensitive = ['!users'] + self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run', 'disk_encryption'] self._process_config() diff --git a/archinstall/lib/disk/encryption.py b/archinstall/lib/disk/encryption.py new file mode 100644 index 00000000..67f656c8 --- /dev/null +++ b/archinstall/lib/disk/encryption.py @@ -0,0 +1,162 @@ +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'{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]: + selector = self._menu_options['partitions'] + if selector.has_selection(): + partitions: List[Any] = selector.current_selection + output = str(_('Partitions to be encrypted')) + '\n' + output += current_partition_layout(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: List[Any]) -> List[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: + return choice.value # type: ignore + + return [] diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index af5879aa..bdfa502a 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -5,6 +5,8 @@ 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 @@ -107,33 +109,22 @@ class Filesystem: continue 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',[]) - if partition.get('encrypted', False): + disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') + + if partition in disk_encryption.partitions: if not partition['device_instance']: raise DiskError(f"Internal error caused us to loose the partition. Please report this issue upstream!") - if not partition.get('!password'): - if not storage['arguments'].get('!encryption-password'): - if storage['arguments'] == 'silent': - raise ValueError(f"Missing encryption password for {partition['device_instance']}") - - from ..user_interaction import get_password - - prompt = str(_('Enter a encryption password for {}').format(partition['device_instance'])) - storage['arguments']['!encryption-password'] = get_password(prompt) - - partition['!password'] = storage['arguments']['!encryption-password'] - 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}" - partition['device_instance'].encrypt(password=partition['!password']) + 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, partition['!password'], auto_unmount=True) as unlocked_device: + 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']}") diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 256f7abb..a5164b76 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -469,12 +469,6 @@ def disk_layouts() -> Optional[Dict[str, Any]]: return None -def encrypted_partitions(blockdevices :Dict[str, Any]) -> bool: - for blockdevice in blockdevices.values(): - for partition in blockdevice.get('partitions', []): - if partition.get('encrypted', False): - yield partition - 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']: diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 04d33453..9febf102 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -91,7 +91,6 @@ class Partition: self._path = path self._part_id = part_id self._target_mountpoint = mountpoint - self._encrypted = None self._encrypted = encrypted self._wipe = False self._type = 'primary' diff --git a/archinstall/lib/hsm/__init__.py b/archinstall/lib/hsm/__init__.py index c0888b04..a3f64019 100644 --- a/archinstall/lib/hsm/__init__.py +++ b/archinstall/lib/hsm/__init__.py @@ -1,4 +1 @@ -from .fido import ( - get_fido2_devices, - fido2_enroll -) \ No newline at end of file +from .fido import Fido2 diff --git a/archinstall/lib/hsm/fido.py b/archinstall/lib/hsm/fido.py index 49f36957..4cd956a3 100644 --- a/archinstall/lib/hsm/fido.py +++ b/archinstall/lib/hsm/fido.py @@ -1,57 +1,92 @@ -import typing -import pathlib import getpass import logging + +from dataclasses import dataclass +from pathlib import Path +from typing import List + from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes from ..disk.partition import Partition from ..general import log -def get_fido2_devices() -> typing.Dict[str, typing.Dict[str, str]]: - """ - 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. - """ - worker = clear_vt100_escape_codes(SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8')) - - MANUFACTURER_POS = 0 - PRODUCT_POS = 0 - devices = {} - for line in worker.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[path] = { - 'manufacturer' : manufacturer, - 'product' : product - } - - return devices - -def fido2_enroll(hsm_device_path :pathlib.Path, partition :Partition, password :str) -> bool: - worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device_path} {partition.real_device}", peak_output=True) - pw_inputted = False - pin_inputted = False - while worker.is_alive(): - if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower(): - worker.write(bytes(password, 'UTF-8')) - pw_inputted = True - - elif pin_inputted is False and 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") \ No newline at end of file + +@dataclass +class Fido2Device: + path: Path + manufacturer: str + product: str + + +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, partition :Partition, password :str): + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device.path} {partition.real_device}", peak_output=True) + pw_inputted = False + pin_inputted = False + + while worker.is_alive(): + if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(password, 'UTF-8')) + pw_inputted = True + + elif pin_inputted is False and 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/installer.py b/archinstall/lib/installer.py index 18f6e244..1926f593 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -15,16 +15,16 @@ from .hardware import has_uefi, is_vm, cpu_vendor from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout from .disk.helpers import findmnt from .mirrors import use_mirrors +from .models.disk_encryption import DiskEncryption from .plugins import plugins from .storage import storage -# from .user_interaction import * from .output import log from .profiles import Profile from .disk.partition import get_mount_fs_type from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError -from .hsm import fido2_enroll from .models.users import User from .models.subvolume import Subvolume +from .hsm import Fido2 if TYPE_CHECKING: _: Any @@ -135,6 +135,8 @@ class Installer: self._zram_enabled = False + self._disk_encryption: DiskEncryption = storage['arguments'].get('disk_encryption') + 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. @@ -196,7 +198,7 @@ class Installer: def _create_keyfile(self,luks_handle , partition :dict, password :str): """ roiutine to create keyfiles, so it can be moved elsewhere """ - if partition.get('generate-encryption-key-file'): + if self._disk_encryption.generate_encryption_file(partition): if not (cryptkey_dir := pathlib.Path(f"{self.target}/etc/cryptsetup-keys.d")).exists(): cryptkey_dir.mkdir(parents=True) # Once we store the key as ../xyzloop.key systemd-cryptsetup can automatically load this key @@ -244,26 +246,20 @@ class Installer: mount_queue = {} # we manage the encrypted partititons - for partition in [entry for entry in list_part if entry.get('encrypted', False)]: + for partition in self._disk_encryption.partitions: # open the luks device and all associate stuff - if not (password := partition.get('!password', None)) and storage['arguments'].get('!encryption-password'): - password = storage['arguments'].get('!encryption-password') - elif not password: - raise RequirementError(f"Missing partition encryption password in layout: {partition}") - loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" # note that we DON'T auto_unmount (i.e. close the encrypted device so it can be used - with (luks_handle := luks2(partition['device_instance'], loopdev, password, auto_unmount=False)) as unlocked_device: - if partition.get('generate-encryption-key-file', False) and not self._has_root(partition): - list_luks_handles.append([luks_handle, partition, password]) + with (luks_handle := luks2(partition['device_instance'], loopdev, self._disk_encryption.encryption_password, auto_unmount=False)) as unlocked_device: + if self._disk_encryption.generate_encryption_file(partition) and not self._has_root(partition): + list_luks_handles.append([luks_handle, partition, self._disk_encryption.encryption_password]) # this way all the requesrs will be to the dm_crypt device and not to the physical partition partition['device_instance'] = unlocked_device - if self._has_root(partition) and partition.get('generate-encryption-key-file', False) is False: - if storage['arguments'].get('HSM'): - hsm_device_path = storage['arguments']['HSM'] - fido2_enroll(hsm_device_path, partition['device_instance'], password) + if self._has_root(partition) and self._disk_encryption.generate_encryption_file(partition) is False: + if self._disk_encryption.hsm_device: + Fido2.fido2_enroll(self._disk_encryption.hsm_device, partition['device_instance'], self._disk_encryption.encryption_password) btrfs_subvolumes = [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', [])] @@ -650,7 +646,7 @@ class Installer: mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") mkinit.write(f"FILES=({' '.join(self.FILES)})\n") - if not storage['arguments'].get('HSM'): + if not self._disk_encryption.hsm_device: # For now, if we don't use HSM we revert to the old # way of setting up encryption hooks for mkinitcpio. # This is purely for stability reasons, we're going away from this. @@ -694,7 +690,7 @@ class Installer: self.HOOKS.remove('fsck') if self.detect_encryption(partition): - if storage['arguments'].get('HSM'): + if self._disk_encryption.hsm_device: # Required bby mkinitcpio to add support for fido2-device options self.pacstrap('libfido2') @@ -758,7 +754,7 @@ class Installer: # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') - if storage['arguments'].get('HSM'): + if self._disk_encryption.hsm_device: # TODO: # A bit of a hack, but we need to get vconsole.conf in there # before running `mkinitcpio` because it expects it in HSM mode. @@ -886,7 +882,7 @@ class Installer: kernel_options = f"options" - if storage['arguments'].get('HSM'): + if self._disk_encryption.hsm_device: # Note: lsblk UUID must be used, not PARTUUID for sd-encrypt to work kernel_options += f" rd.luks.name={real_device.uuid}=luksdev" # Note: tpm2-device and fido2-device don't play along very well: diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py new file mode 100644 index 00000000..61466e3e --- /dev/null +++ b/archinstall/lib/menu/abstract_menu.py @@ -0,0 +1,493 @@ +from __future__ import annotations + +import logging +import sys +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 ..translationhandler import TranslationHandler, Language +from ..user_interaction.general_conf import select_archinstall_language + +if TYPE_CHECKING: + _: Any + + +class Selector: + def __init__( + self, + description :str, + func :Callable = None, + display_func :Callable = None, + default :Any = None, + enabled :bool = False, + dependencies :List = [], + dependencies_not :List = [], + exec_func :Callable = None, + preview_func :Callable = None, + mandatory :bool = False, + no_store :bool = False + ): + """ + Create a new menu selection entry + + :param description: Text that will be displayed as the menu entry + :type description: str + + :param func: Function that is called when the menu entry is selected + :type func: Callable + + :param display_func: After specifying a setting for a menu item it is displayed + on the right side of the item as is; with this function one can modify the entry + to be displayed; e.g. when specifying a password one can display **** instead + :type display_func: Callable + + :param default: Default value for this menu entry + :type default: Any + + :param enabled: Specify if this menu entry should be displayed + :type enabled: bool + + :param dependencies: Specify dependencies for this menu entry; if the dependencies + are not set yet, then this item is not displayed; e.g. disk_layout depends on selectiong + harddrive(s) first + :type dependencies: list + + :param dependencies_not: These are the exclusive options; the menu item will only be + displayed if non of the entries in the list have been specified + :type dependencies_not: list + + :param exec_func: A function with the name and the result of the selection as input parameter and which returns boolean. + Can be used for any action deemed necessary after selection. If it returns True, exits the menu loop, if False, + menu returns to the selection screen. If not specified it is assumed the return is False + :type exec_func: Callable + + :param preview_func: A callable which invokws a preview screen + :type preview_func: Callable + + :param mandatory: A boolean which determines that the field is mandatory, i.e. menu can not be exited if it is not set + :type mandatory: bool + + :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.enabled = enabled + self._dependencies = dependencies + self._dependencies_not = dependencies_not + self.exec_func = exec_func + self._preview_func = preview_func + self.mandatory = mandatory + self._no_store = no_store + + @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): + return self._current_selection + + @property + def preview_func(self): + return self._preview_func + + def do_store(self) -> bool: + return self._no_store is False + + def set_enabled(self, status :bool = True): + self.enabled = status + + def update_description(self, description :str): + self._description = description + + def menu_text(self, padding: int = 0) -> str: + if self._description == '': # special menu option for __separator__ + return '' + + current = '' + + if self._display_func: + current = self._display_func(self._current_selection) + else: + if self._current_selection is not None: + current = str(self._current_selection) + + if current: + padding += 5 + description = str(self._description).ljust(padding, ' ') + current = str(_('set: {}').format(current)) + else: + description = self._description + current = '' + + return f'{description} {current}' + + def set_current_selection(self, current :Optional[str]): + self._current_selection = current + + def has_selection(self) -> bool: + if not self._current_selection: + return False + return True + + def get_selection(self) -> Any: + return self._current_selection + + def is_empty(self) -> bool: + if self._current_selection is None: + return True + elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0: + return True + return False + + def is_enabled(self) -> bool: + return self.enabled + + def is_mandatory(self) -> bool: + return self.mandatory + + def set_mandatory(self, status :bool = True): + self.mandatory = status + if status and not self.is_enabled(): + self.set_enabled(True) + + +class AbstractMenu: + def __init__(self, data_store: Dict[str, Any] = None, auto_cursor=False, preview_size :float = 0.2): + """ + Create a new selection menu. + + :param data_store: Area (Dict) where the resulting data will be held. At least an entry for each option. Default area is self._data_store (not preset in the call, due to circular references + :type data_store: Dict + + :param auto_cursor: Boolean which determines if the cursor stays on the first item (false) or steps each invocation of a selection entry (true) + :type auto_cursor: bool + + :param preview_size. Size in fractions of screen size of the preview window + ;type preview_size: float (range 0..1) + + """ + self._enabled_order :List[str] = [] + self._translation_handler = TranslationHandler() + self.is_context_mgr = False + self._data_store = data_store if data_store is not None else {} + self.auto_cursor = auto_cursor + self._menu_options: Dict[str, Selector] = {} + self._setup_selection_menu_options() + self.preview_size = preview_size + self._last_choice = None + + @property + def last_choice(self): + return self._last_choice + + def __enter__(self, *args :Any, **kwargs :Any) -> AbstractMenu: + self.is_context_mgr = True + return self + + def __exit__(self, *args :Any, **kwargs :Any) -> None: + # 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') + print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues") + raise args[1] + + for key in self._menu_options: + selector = self._menu_options[key] + if key and key not in self._data_store: + self._data_store[key] = selector.current_selection + + self.exit_callback() + + @property + def translation_handler(self) -> TranslationHandler: + return self._translation_handler + + def _setup_selection_menu_options(self): + """ Define the menu options. + Menu options can be defined here in a subclass or done per program calling self.set_option() + """ + return + + def pre_callback(self, selector_name): + """ will be called before each action in the menu """ + return + + def post_callback(self, selection_name: str = None, value: Any = None): + """ will be called after each action in the menu """ + return True + + def exit_callback(self): + """ will be called at the end of the processing of the menu """ + return + + def synch(self, selector_name :str, omit_if_set :bool = False,omit_if_disabled :bool = False): + """ loads menu options with data_store value """ + arg = self._data_store.get(selector_name, None) + # don't display the menu option if it was defined already + if arg is not None and omit_if_set: + return + + if not self.option(selector_name).is_enabled() and omit_if_disabled: + return + + if arg is not None: + self._menu_options[selector_name].set_current_selection(arg) + + def _update_enabled_order(self, selector_name: str): + self._enabled_order.append(selector_name) + + def enable(self, selector_name :str, omit_if_set :bool = False , mandatory :bool = False): + """ activates menu options """ + if self._menu_options.get(selector_name, None): + self._menu_options[selector_name].set_enabled(True) + self._update_enabled_order(selector_name) + + if mandatory: + self._menu_options[selector_name].set_mandatory(True) + self.synch(selector_name,omit_if_set) + else: + print(f'No selector found: {selector_name}') + sys.exit(1) + + def _preview_display(self, selection_name: str) -> Optional[str]: + config_name, selector = self._find_selection(selection_name) + if preview := selector.preview_func: + return preview() + return None + + def _get_menu_text_padding(self, entries: List[Selector]): + return max([len(str(selection.description)) for selection in entries]) + + def _find_selection(self, selection_name: str) -> Tuple[str, Selector]: + enabled_menus = self._menus_to_enable() + padding = self._get_menu_text_padding(list(enabled_menus.values())) + option = [(k, v) for k, v in self._menu_options.items() if v.menu_text(padding).strip() == selection_name.strip()] + + if len(option) != 1: + raise ValueError(f'Selection not found: {selection_name}') + config_name = option[0][0] + selector = option[0][1] + return config_name, selector + + def run(self, allow_reset: bool = False): + """ Calls the Menu framework""" + # we synch all the options just in case + for item in self.list_options(): + self.synch(item) + + self.post_callback() # as all the values can vary i have to exec this callback + cursor_pos = None + + while True: + # Before continuing, set the preferred keyboard layout/language in the current terminal. + # This will just help the user with the next following questions. + self._set_kb_language() + enabled_menus = self._menus_to_enable() + + padding = self._get_menu_text_padding(list(enabled_menus.values())) + menu_options = [m.menu_text(padding) for m in enabled_menus.values()] + + warning_msg = str(_('All settings will be reset, are you sure?')) + + selection = Menu( + _('Set/Modify the below options'), + menu_options, + sort=False, + cursor_index=cursor_pos, + preview_command=self._preview_display, + preview_size=self.preview_size, + skip_empty_entries=True, + skip=False, + allow_reset=allow_reset, + allow_reset_warning_msg=warning_msg + ).run() + + match selection.type_: + case MenuSelectionType.Reset: + self._data_store = {} + return + case MenuSelectionType.Selection: + value: str = selection.value # type: ignore + + if self.auto_cursor: + cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails + + # in case the new position lands on a "placeholder" we'll skip them as well + while True: + if cursor_pos >= len(menu_options): + cursor_pos = 0 + if len(menu_options[cursor_pos]) > 0: + break + cursor_pos += 1 + + value = value.strip() + + # if this calls returns false, we exit the menu + # we allow for an callback for special processing on realeasing control + if not self._process_selection(value): + break + + # we get the last action key + actions = {str(v.description):k for k,v in self._menu_options.items()} + self._last_choice = actions[selection.value.strip()] # type: ignore + + if not self.is_context_mgr: + self.__exit__() + + def _process_selection(self, selection_name :str) -> bool: + """ determines and executes the selection y + Can / Should be extended to handle specific selection issues + Returns true if the menu shall continue, False if it has ended + """ + # find the selected option in our option list + config_name, selector = self._find_selection(selection_name) + return self.exec_option(config_name, selector) + + def exec_option(self, config_name :str, p_selector :Selector = None) -> bool: + """ processes the execution of a given menu entry + - pre process callback + - selection function + - post process callback + - exec action + returns True if the loop has to continue, false if the loop can be closed + """ + if not p_selector: + selector = self.option(config_name) + else: + selector = p_selector + + self.pre_callback(config_name) + + result = None + if selector.func: + presel_val = self.option(config_name).get_selection() + result = selector.func(presel_val) + self._menu_options[config_name].set_current_selection(result) + if selector.do_store(): + self._data_store[config_name] = result + exec_ret_val = selector.exec_func(config_name,result) if selector.exec_func else False + self.post_callback(config_name,result) + + if exec_ret_val and self._check_mandatory_status(): + return False + return True + + def _set_kb_language(self): + """ general for ArchInstall""" + # Before continuing, set the preferred keyboard layout/language in the current terminal. + # This will just help the user with the next following questions. + if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']): + set_keyboard_language(self._data_store['keyboard-layout']) + + 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 + + if len(selection.dependencies_not) > 0: + for d in selection.dependencies_not: + if not self._menu_options[d].is_empty(): + return False + return True + + raise ValueError(f'No selection found: {selection_name}') + + def _menus_to_enable(self) -> dict: + """ general """ + enabled_menus = {} + + for name, selection in self._menu_options.items(): + if self._verify_selection_enabled(name): + enabled_menus[name] = selection + + # sort the enabled menu by the order we enabled them in + # we'll add the entries that have been enabled via the selector constructor at the top + enabled_keys = [i for i in enabled_menus.keys() if i not in self._enabled_order] + # and then we add the ones explicitly enabled by the enable function + enabled_keys += [i for i in self._enabled_order if i in enabled_menus.keys()] + + ordered_menus = {k: enabled_menus[k] for k in enabled_keys} + + return ordered_menus + + def option(self,name :str) -> Selector: + # TODO check inexistent name + return self._menu_options[name] + + def list_options(self) -> Iterator: + """ Iterator to retrieve the enabled menu option names + """ + for item in self._menu_options: + yield item + + def list_enabled_options(self) -> Iterator: + """ Iterator to retrieve the enabled menu options at a given time. + The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated + """ + for item in self._menu_options: + if item in self._menus_to_enable(): + yield item + + def set_option(self, name :str, selector :Selector): + self._menu_options[name] = selector + self.synch(name) + + def _check_mandatory_status(self) -> bool: + for field in self._menu_options: + option = self._menu_options[field] + if option.is_mandatory() and not option.has_selection(): + return False + return True + + def set_mandatory(self, field :str, status :bool): + self.option(field).set_mandatory(status) + + def mandatory_overview(self) -> Tuple[int, int]: + mandatory_fields = 0 + mandatory_waiting = 0 + for field, option in self._menu_options.items(): + if option.is_mandatory(): + mandatory_fields += 1 + if not option.has_selection(): + mandatory_waiting += 1 + return mandatory_fields, mandatory_waiting + + def _select_archinstall_language(self, preset_value: Language) -> Language: + language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) + self._translation_handler.activate(language) + return language + + +class AbstractSubMenu(AbstractMenu): + def __init__(self, data_store: Dict[str, Any] = None): + super().__init__(data_store=data_store) + + self._menu_options['__separator__'] = Selector('') + self._menu_options['back'] = \ + Selector( + _('Back'), + no_store=True, + enabled=True, + exec_func=lambda n, v: True, + ) diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index 444ba7ee..0d348227 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -3,12 +3,13 @@ from __future__ import annotations from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING import archinstall -from ..disk import encrypted_partitions +from ..disk.encryption import DiskEncryptionMenu from ..general import SysCommand, secret from ..hardware import has_uefi from ..menu import Menu -from ..menu.selection_menu import Selector, GeneralMenu +from ..menu.abstract_menu import Selector, AbstractMenu from ..models import NetworkConfiguration +from ..models.disk_encryption import DiskEncryption, EncryptionType from ..models.users import User from ..output import FormattedOutput from ..profiles import is_desktop_profile, Profile @@ -25,7 +26,6 @@ from ..user_interaction import ask_to_configure_network from ..user_interaction import get_password, ask_for_a_timezone, save_config from ..user_interaction import select_additional_repositories from ..user_interaction import select_disk_layout -from ..user_interaction import select_encrypted_partitions from ..user_interaction import select_harddrives from ..user_interaction import select_kernel from ..user_interaction import select_language @@ -39,7 +39,7 @@ if TYPE_CHECKING: _: Any -class GlobalMenu(GeneralMenu): +class GlobalMenu(AbstractMenu): def __init__(self,data_store): self._disk_check = True super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) @@ -91,18 +91,13 @@ class GlobalMenu(GeneralMenu): preview_func=self._prev_disk_layouts, display_func=lambda x: self._display_disk_layout(x), dependencies=['harddrives']) - self._menu_options['!encryption-password'] = \ + self._menu_options['disk_encryption'] = \ Selector( - _('Encryption password'), - lambda x: self._select_encrypted_password(), - display_func=lambda x: secret(x) if x else 'None', - dependencies=['harddrives']) - self._menu_options['HSM'] = Selector( - description=_('Use HSM to unlock encrypted drive'), - func=lambda preset: self._select_hsm(preset), - dependencies=['!encryption-password'], - default=None - ) + _('Disk encryption'), + lambda preset: self._disk_encryption(preset), + preview_func=self._prev_disk_encryption, + display_func=lambda x: self._display_disk_encryption(x), + dependencies=['disk_layouts']) self._menu_options['swap'] = \ Selector( _('Swap'), @@ -209,28 +204,6 @@ class GlobalMenu(GeneralMenu): def post_callback(self,name :str = None ,result :Any = None): self._update_install_text(name, result) - def exit_callback(self): - if self._data_store.get('harddrives', None) and self._data_store.get('!encryption-password', None): - # 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). - if len(list(encrypted_partitions(storage['arguments'].get('disk_layouts', [])))) == 0: - for blockdevice in storage['arguments']['disk_layouts']: - if storage['arguments']['disk_layouts'][blockdevice].get('partitions'): - for partition_index in select_encrypted_partitions( - title=_('Select which partitions to encrypt:'), - partitions=storage['arguments']['disk_layouts'][blockdevice]['partitions'], - filter_=(lambda p: p['mountpoint'] != '/boot') - ): - - partition = storage['arguments']['disk_layouts'][blockdevice]['partitions'][partition_index] - partition['encrypted'] = True - partition['!password'] = storage['arguments']['!encryption-password'] - - # We make sure generate-encryption-key-file is set on additional partitions - # other than the root partition. Otherwise they won't unlock properly #1279 - if partition['mountpoint'] != '/': - partition['generate-encryption-key-file'] = True - def _install_text(self): missing = len(self._missing_configs()) if missing > 0: @@ -246,6 +219,20 @@ class GlobalMenu(GeneralMenu): else: return str(cur_value) + def _disk_encryption(self, preset: Optional[DiskEncryption]) -> Optional[DiskEncryption]: + data_store: Dict[str, Any] = {} + + selector = self._menu_options['disk_layouts'] + + if selector.has_selection(): + layouts: Dict[str, Dict[str, Any]] = selector.current_selection + else: + # this should not happen as the encryption menu has the disk layout as dependency + raise ValueError('No disk layout specified') + + disk_encryption = DiskEncryptionMenu(data_store, preset, layouts).run() + return disk_encryption + def _prev_network_config(self) -> Optional[str]: selector = self._menu_options['nic'] if selector.has_selection(): @@ -283,6 +270,30 @@ class GlobalMenu(GeneralMenu): return f'{total_nr} {_("Partitions")}' return '' + def _prev_disk_encryption(self) -> Optional[str]: + selector = self._menu_options['disk_encryption'] + if selector.has_selection(): + encryption: DiskEncryption = selector.current_selection + + enc_type = EncryptionType.type_to_text(encryption.encryption_type) + output = str(_('Encryption type')) + f': {enc_type}\n' + output += str(_('Password')) + f': {secret(encryption.encryption_password)}\n' + + if encryption.partitions: + output += 'Partitions: {} selected'.format(len(encryption.partitions)) + '\n' + + if encryption.hsm_device: + output += f'HSM: {encryption.hsm_device.manufacturer}' + + return output + + return None + + def _display_disk_encryption(self, current_value: Optional[DiskEncryption]) -> str: + if current_value: + return EncryptionType.type_to_text(current_value.encryption_type) + return '' + def _prev_install_missing_config(self) -> Optional[str]: if missing := self._missing_configs(): text = str(_('Missing configurations:\n')) @@ -327,11 +338,10 @@ class GlobalMenu(GeneralMenu): password = get_password(prompt=prompt) return password - def _select_encrypted_password(self) -> Optional[str]: - if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): - return passwd - else: - return None + # def _select_encrypted_password(self) -> Optional[str]: + # if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))): + # return passwd + # return None def _select_ntp(self, preset :bool = True) -> bool: ntp = ask_ntp(preset) diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index ae3a6eb5..1e09d987 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -104,7 +104,7 @@ class ListManager: return options, header def _run_actions_on_entry(self, entry: Any): - options = self.filter_options(entry,self._sub_menu_actions) + [self._cancel_action] + options = self.filter_options(entry, self._sub_menu_actions) + [self._cancel_action] display_value = self.selected_action_display(entry) prompt = _("Select an action for '{}'").format(display_value) diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 773ff1de..09685c55 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -18,8 +18,8 @@ if TYPE_CHECKING: class MenuSelectionType(Enum): Selection = auto() - Esc = auto() - Ctrl_c = auto() + Skip = auto() + Reset = auto() @dataclass @@ -56,8 +56,8 @@ class Menu(TerminalMenu): preview_size: float = 0.0, preview_title: str = 'Info', header :Union[List[str],str] = None, - raise_error_on_interrupt :bool = False, - raise_error_warning_msg :str = '', + allow_reset :bool = False, + allow_reset_warning_msg :str = '', clear_screen: bool = True, show_search_hint: bool = True, cycle_cursor: bool = True, @@ -150,17 +150,10 @@ class Menu(TerminalMenu): self._skip = skip self._default_option = default_option self._multi = multi - self._raise_error_on_interrupt = raise_error_on_interrupt - self._raise_error_warning_msg = raise_error_warning_msg + self._raise_error_on_interrupt = allow_reset + self._raise_error_warning_msg = allow_reset_warning_msg self._preview_command = preview_command - menu_title = f'\n{title}\n\n' - - if header: - if not isinstance(header,(list,tuple)): - header = [header] - menu_title += '\n'.join(header) - action_info = '' if skip: action_info += str(_('ESC to skip')) @@ -173,7 +166,15 @@ class Menu(TerminalMenu): action_info += ', ' if len(action_info) > 0 else '' action_info += str(_('TAB to select')) - menu_title += action_info + '\n' + if action_info: + action_info += '\n\n' + + menu_title = f'\n{action_info}{title}\n' + + if header: + if not isinstance(header,(list,tuple)): + header = [header] + menu_title += '\n' + '\n'.join(header) if default_option: # if a default value was specified we move that one @@ -215,7 +216,7 @@ class Menu(TerminalMenu): try: idx = self.show() except KeyboardInterrupt: - return MenuSelection(type_=MenuSelectionType.Ctrl_c) + 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: @@ -234,7 +235,7 @@ class Menu(TerminalMenu): result = check_default(self._menu_options[idx]) return MenuSelection(type_=MenuSelectionType.Selection, value=result) else: - return MenuSelection(type_=MenuSelectionType.Esc) + return MenuSelection(type_=MenuSelectionType.Skip) def _preview_wrapper(self, preview_command: Optional[Callable], current_selection: str) -> Optional[str]: if preview_command: @@ -246,15 +247,15 @@ class Menu(TerminalMenu): def run(self) -> MenuSelection: ret = self._show() - if ret.type_ == MenuSelectionType.Ctrl_c: + if ret.type_ == MenuSelectionType.Reset: if self._raise_error_on_interrupt and len(self._raise_error_warning_msg) > 0: response = Menu(self._raise_error_warning_msg, Menu.yes_no(), skip=False).run() if response.value == Menu.no(): return self.run() - - if ret.type_ is not MenuSelectionType.Selection and not self._skip: - system('clear') - return self.run() + elif ret.type_ is MenuSelectionType.Skip: + if not self._skip: + system('clear') + return self.run() return ret diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py deleted file mode 100644 index 8a08812c..00000000 --- a/archinstall/lib/menu/selection_menu.py +++ /dev/null @@ -1,497 +0,0 @@ -from __future__ import annotations - -import logging -import sys -import pathlib -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 ..translationhandler import TranslationHandler, Language -from ..hsm.fido import get_fido2_devices - -from ..user_interaction.general_conf import select_archinstall_language - -if TYPE_CHECKING: - _: Any - - -class Selector: - def __init__( - self, - description :str, - func :Callable = None, - display_func :Callable = None, - default :Any = None, - enabled :bool = False, - dependencies :List = [], - dependencies_not :List = [], - exec_func :Callable = None, - preview_func :Callable = None, - mandatory :bool = False, - no_store :bool = False - ): - """ - Create a new menu selection entry - - :param description: Text that will be displayed as the menu entry - :type description: str - - :param func: Function that is called when the menu entry is selected - :type func: Callable - - :param display_func: After specifying a setting for a menu item it is displayed - on the right side of the item as is; with this function one can modify the entry - to be displayed; e.g. when specifying a password one can display **** instead - :type display_func: Callable - - :param default: Default value for this menu entry - :type default: Any - - :param enabled: Specify if this menu entry should be displayed - :type enabled: bool - - :param dependencies: Specify dependencies for this menu entry; if the dependencies - are not set yet, then this item is not displayed; e.g. disk_layout depends on selectiong - harddrive(s) first - :type dependencies: list - - :param dependencies_not: These are the exclusive options; the menu item will only be - displayed if non of the entries in the list have been specified - :type dependencies_not: list - - :param exec_func: A function with the name and the result of the selection as input parameter and which returns boolean. - Can be used for any action deemed necessary after selection. If it returns True, exits the menu loop, if False, - menu returns to the selection screen. If not specified it is assumed the return is False - :type exec_func: Callable - - :param preview_func: A callable which invokws a preview screen - :type preview_func: Callable - - :param mandatory: A boolean which determines that the field is mandatory, i.e. menu can not be exited if it is not set - :type mandatory: bool - - :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.enabled = enabled - self._dependencies = dependencies - self._dependencies_not = dependencies_not - self.exec_func = exec_func - self._preview_func = preview_func - self.mandatory = mandatory - self._no_store = no_store - - @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): - return self._current_selection - - @property - def preview_func(self): - return self._preview_func - - def do_store(self) -> bool: - return self._no_store is False - - def set_enabled(self, status :bool = True): - self.enabled = status - - def update_description(self, description :str): - self._description = description - - def menu_text(self, padding: int = 0) -> str: - if self._description == '': # special menu option for __separator__ - return '' - - current = '' - - if self._display_func: - current = self._display_func(self._current_selection) - else: - if self._current_selection is not None: - current = str(self._current_selection) - - if current: - padding += 5 - description = str(self._description).ljust(padding, ' ') - current = str(_('set: {}').format(current)) - else: - description = self._description - current = '' - - return f'{description} {current}' - - def set_current_selection(self, current :Optional[str]): - self._current_selection = current - - def has_selection(self) -> bool: - if not self._current_selection: - return False - return True - - def get_selection(self) -> Any: - return self._current_selection - - def is_empty(self) -> bool: - if self._current_selection is None: - return True - elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0: - return True - return False - - def is_enabled(self) -> bool: - return self.enabled - - def is_mandatory(self) -> bool: - return self.mandatory - - def set_mandatory(self, status :bool = True): - self.mandatory = status - if status and not self.is_enabled(): - self.set_enabled(True) - -class GeneralMenu: - def __init__(self, data_store :dict = None, auto_cursor=False, preview_size :float = 0.2): - """ - Create a new selection menu. - - :param data_store: Area (Dict) where the resulting data will be held. At least an entry for each option. Default area is self._data_store (not preset in the call, due to circular references - :type data_store: Dict - - :param auto_cursor: Boolean which determines if the cursor stays on the first item (false) or steps each invocation of a selection entry (true) - :type auto_cursor: bool - - :param preview_size. Size in fractions of screen size of the preview window - ;type preview_size: float (range 0..1) - - """ - self._enabled_order :List[str] = [] - self._translation_handler = TranslationHandler() - self.is_context_mgr = False - self._data_store = data_store if data_store is not None else {} - self.auto_cursor = auto_cursor - self._menu_options: Dict[str, Selector] = {} - self._setup_selection_menu_options() - self.preview_size = preview_size - self._last_choice = None - - @property - def last_choice(self): - return self._last_choice - - def __enter__(self, *args :Any, **kwargs :Any) -> GeneralMenu: - self.is_context_mgr = True - return self - - def __exit__(self, *args :Any, **kwargs :Any) -> None: - # 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') - print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues") - raise args[1] - - for key in self._menu_options: - sel = self._menu_options[key] - if key and key not in self._data_store: - self._data_store[key] = sel._current_selection - - self.exit_callback() - - @property - def translation_handler(self) -> TranslationHandler: - return self._translation_handler - - def _setup_selection_menu_options(self): - """ Define the menu options. - Menu options can be defined here in a subclass or done per program calling self.set_option() - """ - return - - def pre_callback(self, selector_name): - """ will be called before each action in the menu """ - return - - def post_callback(self, selection_name: str = None, value: Any = None): - """ will be called after each action in the menu """ - return True - - def exit_callback(self): - """ will be called at the end of the processing of the menu """ - return - - def synch(self, selector_name :str, omit_if_set :bool = False,omit_if_disabled :bool = False): - """ loads menu options with data_store value """ - arg = self._data_store.get(selector_name, None) - # don't display the menu option if it was defined already - if arg is not None and omit_if_set: - return - - if not self.option(selector_name).is_enabled() and omit_if_disabled: - return - - if arg is not None: - self._menu_options[selector_name].set_current_selection(arg) - - def _update_enabled_order(self, selector_name: str): - self._enabled_order.append(selector_name) - - def enable(self, selector_name :str, omit_if_set :bool = False , mandatory :bool = False): - """ activates menu options """ - if self._menu_options.get(selector_name, None): - self._menu_options[selector_name].set_enabled(True) - self._update_enabled_order(selector_name) - - if mandatory: - self._menu_options[selector_name].set_mandatory(True) - self.synch(selector_name,omit_if_set) - else: - print(f'No selector found: {selector_name}') - sys.exit(1) - - def _preview_display(self, selection_name: str) -> Optional[str]: - config_name, selector = self._find_selection(selection_name) - if preview := selector.preview_func: - return preview() - return None - - def _get_menu_text_padding(self, entries: List[Selector]): - return max([len(str(selection.description)) for selection in entries]) - - def _find_selection(self, selection_name: str) -> Tuple[str, Selector]: - enabled_menus = self._menus_to_enable() - padding = self._get_menu_text_padding(list(enabled_menus.values())) - option = [(k, v) for k, v in self._menu_options.items() if v.menu_text(padding).strip() == selection_name.strip()] - - if len(option) != 1: - raise ValueError(f'Selection not found: {selection_name}') - config_name = option[0][0] - selector = option[0][1] - return config_name, selector - - def run(self): - """ Calls the Menu framework""" - # we synch all the options just in case - for item in self.list_options(): - self.synch(item) - - self.post_callback() # as all the values can vary i have to exec this callback - cursor_pos = None - - while True: - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - self._set_kb_language() - enabled_menus = self._menus_to_enable() - - padding = self._get_menu_text_padding(list(enabled_menus.values())) - menu_options = [m.menu_text(padding) for m in enabled_menus.values()] - - selection = Menu( - _('Set/Modify the below options'), - menu_options, - sort=False, - cursor_index=cursor_pos, - preview_command=self._preview_display, - preview_size=self.preview_size, - skip_empty_entries=True, - skip=False - ).run() - - if selection.type_ == MenuSelectionType.Selection: - value = selection.value - - if self.auto_cursor: - cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails - - # in case the new position lands on a "placeholder" we'll skip them as well - while True: - if cursor_pos >= len(menu_options): - cursor_pos = 0 - if len(menu_options[cursor_pos]) > 0: - break - cursor_pos += 1 - - value = value.strip() - - # if this calls returns false, we exit the menu - # we allow for an callback for special processing on realeasing control - if not self._process_selection(value): - break - - # we get the last action key - actions = {str(v.description):k for k,v in self._menu_options.items()} - self._last_choice = actions[selection.value.strip()] - - if not self.is_context_mgr: - self.__exit__() - - def _process_selection(self, selection_name :str) -> bool: - """ determines and executes the selection y - Can / Should be extended to handle specific selection issues - Returns true if the menu shall continue, False if it has ended - """ - # find the selected option in our option list - config_name, selector = self._find_selection(selection_name) - return self.exec_option(config_name, selector) - - def exec_option(self, config_name :str, p_selector :Selector = None) -> bool: - """ processes the execution of a given menu entry - - pre process callback - - selection function - - post process callback - - exec action - returns True if the loop has to continue, false if the loop can be closed - """ - if not p_selector: - selector = self.option(config_name) - else: - selector = p_selector - - self.pre_callback(config_name) - - result = None - if selector.func: - presel_val = self.option(config_name).get_selection() - result = selector.func(presel_val) - self._menu_options[config_name].set_current_selection(result) - if selector.do_store(): - self._data_store[config_name] = result - exec_ret_val = selector.exec_func(config_name,result) if selector.exec_func else False - self.post_callback(config_name,result) - - if exec_ret_val and self._check_mandatory_status(): - return False - return True - - def _set_kb_language(self): - """ general for ArchInstall""" - # Before continuing, set the preferred keyboard layout/language in the current terminal. - # This will just help the user with the next following questions. - if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']): - set_keyboard_language(self._data_store['keyboard-layout']) - - 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 - - if len(selection.dependencies_not) > 0: - for d in selection.dependencies_not: - if not self._menu_options[d].is_empty(): - return False - return True - - raise ValueError(f'No selection found: {selection_name}') - - def _menus_to_enable(self) -> dict: - """ general """ - enabled_menus = {} - - for name, selection in self._menu_options.items(): - if self._verify_selection_enabled(name): - enabled_menus[name] = selection - - # sort the enabled menu by the order we enabled them in - # we'll add the entries that have been enabled via the selector constructor at the top - enabled_keys = [i for i in enabled_menus.keys() if i not in self._enabled_order] - # and then we add the ones explicitly enabled by the enable function - enabled_keys += [i for i in self._enabled_order if i in enabled_menus.keys()] - - ordered_menus = {k: enabled_menus[k] for k in enabled_keys} - - return ordered_menus - - def option(self,name :str) -> Selector: - # TODO check inexistent name - return self._menu_options[name] - - def list_options(self) -> Iterator: - """ Iterator to retrieve the enabled menu option names - """ - for item in self._menu_options: - yield item - - def list_enabled_options(self) -> Iterator: - """ Iterator to retrieve the enabled menu options at a given time. - The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated - """ - for item in self._menu_options: - if item in self._menus_to_enable(): - yield item - - def set_option(self, name :str, selector :Selector): - self._menu_options[name] = selector - self.synch(name) - - def _check_mandatory_status(self) -> bool: - for field in self._menu_options: - option = self._menu_options[field] - if option.is_mandatory() and not option.has_selection(): - return False - return True - - def set_mandatory(self, field :str, status :bool): - self.option(field).set_mandatory(status) - - def mandatory_overview(self) -> Tuple[int, int]: - mandatory_fields = 0 - mandatory_waiting = 0 - for field, option in self._menu_options.items(): - if option.is_mandatory(): - mandatory_fields += 1 - if not option.has_selection(): - mandatory_waiting += 1 - return mandatory_fields, mandatory_waiting - - def _select_archinstall_language(self, preset_value: Language) -> Language: - language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) - self._translation_handler.activate(language) - return language - - def _select_hsm(self, preset :Optional[pathlib.Path] = None) -> Optional[pathlib.Path]: - title = _('Select which partitions to mark for formatting:') - title += '\n' - - fido_devices = get_fido2_devices() - - indexes = [] - for index, path in enumerate(fido_devices.keys()): - title += f"{index}: {path} ({fido_devices[path]['manufacturer']} - {fido_devices[path]['product']})" - indexes.append(f"{index}|{fido_devices[path]['product']}") - - title += '\n' - - choice = Menu(title, indexes, multi=False).run() - - match choice.type_: - case MenuSelectionType.Esc: return preset - case MenuSelectionType.Selection: - selection: Any = choice.value - index = int(selection.split('|',1)[0]) - return pathlib.Path(list(fido_devices.keys())[index]) - - return None diff --git a/archinstall/lib/menu/table_selection_menu.py b/archinstall/lib/menu/table_selection_menu.py new file mode 100644 index 00000000..09cd6ee2 --- /dev/null +++ b/archinstall/lib/menu/table_selection_menu.py @@ -0,0 +1,107 @@ +from typing import Any, Tuple, List, Dict, Optional + +from .menu import MenuSelectionType, MenuSelection +from ..output import FormattedOutput +from ..menu import Menu + + +class TableMenu(Menu): + def __init__( + self, + title: str, + data: List[Any] = [], + table_data: Optional[Tuple[List[Any], str]] = None, + custom_menu_options: List[str] = [], + default: Any = None, + multi: bool = False + ): + """ + param title: Text that will be displayed above the menu + :type title: str + + param data: List of objects that will be displayed as rows + :type data: List + + param table_data: Tuple containing a list of objects and the corresponding + Table representation of the data as string; this can be used in case the table + has to be crafted in a more sophisticated manner + :type table_data: Optional[Tuple[List[Any], str]] + + param custom_options: List of custom options that will be displayed under the table + :type custom_menu_options: List + """ + if not data and not table_data: + raise ValueError('Either "data" or "table_data" must be provided') + + self._custom_options = custom_menu_options + self._multi = multi + + if multi: + header_padding = 7 + else: + header_padding = 2 + + if len(data): + table_text = FormattedOutput.as_table(data) + rows = table_text.split('\n') + table = self._create_table(data, rows, header_padding=header_padding) + elif table_data is not None: + # we assume the table to be + # h1 | h2 + # ----------- + # r1 | r2 + data = table_data[0] + rows = table_data[1].split('\n') + table = self._create_table(data, rows, header_padding=header_padding) + + self._options, header = self._prepare_selection(table) + + super().__init__( + title, + self._options, + header=header, + skip_empty_entries=True, + show_search_hint=False, + allow_reset=True, + multi=multi, + default_option=default + ) + + def run(self) -> MenuSelection: + choice = super().run() + + match choice.type_: + case MenuSelectionType.Selection: + if self._multi: + choice.value = [self._options[val] for val in choice.value] # type: ignore + else: + choice.value = self._options[choice.value] # type: ignore + + return choice + + def _create_table(self, data: List[Any], rows: List[str], header_padding: int = 2) -> Dict[str, Any]: + # these are the header rows of the table and do not map to any data 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 + padding = ' ' * header_padding + display_data = {f'{padding}{rows[0]}': None, f'{padding}{rows[1]}': None} + + for row, entry in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = entry + + return display_data + + def _prepare_selection(self, table: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: + # header rows are mapped to None so make sure to exclude those from the selectable data + options = {key: val for key, val in table.items() if val is not None} + header = '' + + if len(options) > 0: + table_header = [key for key, val in table.items() if val is None] + header = '\n'.join(table_header) + + custom = {key: None for key in self._custom_options} + options.update(custom) + + return options, header diff --git a/archinstall/lib/models/disk_encryption.py b/archinstall/lib/models/disk_encryption.py new file mode 100644 index 00000000..80627767 --- /dev/null +++ b/archinstall/lib/models/disk_encryption.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import Optional, List, Dict, TYPE_CHECKING, Any + +from archinstall.lib.hsm.fido import Fido2Device + +if TYPE_CHECKING: + _: Any + + +class EncryptionType(Enum): + Partition = auto() + # FullDiskEncryption = auto() + + @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[str] = field(default_factory=list) + hsm_device: Optional[Fido2Device] = None + + def generate_encryption_file(self, partition) -> bool: + return partition in self.partitions and partition['mountpoint'] != '/' + diff --git a/archinstall/lib/user_interaction/__init__.py b/archinstall/lib/user_interaction/__init__.py index a1ca2652..2bc46759 100644 --- a/archinstall/lib/user_interaction/__init__.py +++ b/archinstall/lib/user_interaction/__init__.py @@ -4,7 +4,7 @@ from .backwards_compatible_conf import generic_select, generic_multi_select from .locale_conf import select_locale_lang, select_locale_enc from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap from .network_conf import ask_to_configure_network -from .partitioning_conf import select_partition, select_encrypted_partitions +from .partitioning_conf import select_partition from .general_conf import (ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, select_profile, select_archinstall_language, ask_additional_packages_to_install, select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads) diff --git a/archinstall/lib/user_interaction/disk_conf.py b/archinstall/lib/user_interaction/disk_conf.py index b5ed6967..554d13ef 100644 --- a/archinstall/lib/user_interaction/disk_conf.py +++ b/archinstall/lib/user_interaction/disk_conf.py @@ -45,13 +45,13 @@ def select_disk_layout(preset: Optional[Dict[str, Any]], block_devices: list, ad choice = Menu( _('Select what you wish to do with the selected block devices'), modes, - raise_error_on_interrupt=True, - raise_error_warning_msg=warning + allow_reset=True, + allow_reset_warning_msg=warning ).run() match choice.type_: - case MenuSelectionType.Esc: return preset - case MenuSelectionType.Ctrl_c: return None + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None case MenuSelectionType.Selection: if choice.value == wipe_mode: return get_default_partition_layout(block_devices, advanced_options) @@ -77,7 +77,7 @@ def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> Optional[BlockDevice]: choice = Menu(title, drives).run() - if choice.type_ == MenuSelectionType.Esc: + if choice.type_ == MenuSelectionType.Skip: return None drive = dict_o_disks[choice.value] diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index efd746a4..76631a98 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -48,7 +48,7 @@ def ask_for_a_timezone(preset: str = None) -> str: ).run() match choice.type_: - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: return choice.value @@ -60,7 +60,7 @@ def ask_for_audio_selection(desktop: bool = True, preset: str = None) -> str: choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() match choice.type_: - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: return choice.value @@ -107,12 +107,12 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: list(mirrors.keys()), preset_values=preselected, multi=True, - raise_error_on_interrupt=True + allow_reset=True ).run() match selected_mirror.type_: - case MenuSelectionType.Ctrl_c: return {} - case MenuSelectionType.Esc: return preset_values + case MenuSelectionType.Reset: return {} + case MenuSelectionType.Skip: return preset_values case _: return {selected: mirrors[selected] for selected in selected_mirror.value} @@ -134,7 +134,7 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag ).run() match choice.type_: - case MenuSelectionType.Esc: + case MenuSelectionType.Skip: return preset_value case MenuSelectionType.Selection: return options[choice.value] @@ -163,21 +163,21 @@ def select_profile(preset) -> Optional[Profile]: selection = Menu( title=title, p_options=list(options.keys()), - raise_error_on_interrupt=True, - raise_error_warning_msg=warning + allow_reset=True, + allow_reset_warning_msg=warning ).run() match selection.type_: case MenuSelectionType.Selection: return options[selection.value] if selection.value is not None else None - case MenuSelectionType.Ctrl_c: + case MenuSelectionType.Reset: storage['profile_minimal'] = False storage['_selected_servers'] = [] storage['_desktop_profile'] = None storage['arguments']['desktop-environment'] = None storage['arguments']['gfx_driver_packages'] = None return None - case MenuSelectionType.Esc: + case MenuSelectionType.Skip: return None @@ -259,10 +259,10 @@ def select_additional_repositories(preset: List[str]) -> List[str]: sort=False, multi=True, preset_values=preset, - raise_error_on_interrupt=True + allow_reset=True ).run() match choice.type_: - case MenuSelectionType.Esc: return preset - case MenuSelectionType.Ctrl_c: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] case MenuSelectionType.Selection: return choice.value diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py index 15720064..bbbe070b 100644 --- a/archinstall/lib/user_interaction/locale_conf.py +++ b/archinstall/lib/user_interaction/locale_conf.py @@ -23,7 +23,7 @@ def select_locale_lang(preset: str = None) -> str: match selected_locale.type_: case MenuSelectionType.Selection: return selected_locale.value - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset def select_locale_enc(preset: str = None) -> str: @@ -39,4 +39,4 @@ def select_locale_enc(preset: str = None) -> str: match selected_locale.type_: case MenuSelectionType.Selection: return selected_locale.value - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index 557e8ed8..5e637f23 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -71,7 +71,7 @@ class ManualNetworkConfig(ListManager): available = set(all_ifaces) - set(existing_ifaces) choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - if choice.type_ == MenuSelectionType.Esc: + if choice.type_ == MenuSelectionType.Skip: return None return choice.value @@ -154,13 +154,13 @@ def ask_to_configure_network( list(network_options.values()), cursor_index=cursor_idx, sort=False, - raise_error_on_interrupt=True, - raise_error_warning_msg=warning + allow_reset=True, + allow_reset_warning_msg=warning ).run() match choice.type_: - case MenuSelectionType.Esc: return preset - case MenuSelectionType.Ctrl_c: return None + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None if choice.value == network_options['none']: return None diff --git a/archinstall/lib/user_interaction/partitioning_conf.py b/archinstall/lib/user_interaction/partitioning_conf.py index f2e6b881..cff76dc2 100644 --- a/archinstall/lib/user_interaction/partitioning_conf.py +++ b/archinstall/lib/user_interaction/partitioning_conf.py @@ -119,7 +119,7 @@ def select_partition( choice = Menu(title, partition_indexes, multi=multiple).run() - if choice.type_ == MenuSelectionType.Esc: + if choice.type_ == MenuSelectionType.Skip: return None if isinstance(choice.value, list): @@ -150,7 +150,6 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, delete_all_partitions = str(_('Clear/Delete all partitions')) assign_mount_point = str(_('Assign mount-point for a partition')) mark_formatted = str(_('Mark/Unmark a partition to be formatted (wipes data)')) - mark_encrypted = str(_('Mark/Unmark a partition as encrypted')) mark_compressed = str(_('Mark/Unmark a partition as compressed (btrfs only)')) mark_bootable = str(_('Mark/Unmark a partition as bootable (automatic for /boot)')) set_filesystem_partition = str(_('Set desired filesystem for a partition')) @@ -167,7 +166,6 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, delete_all_partitions, assign_mount_point, mark_formatted, - mark_encrypted, mark_bootable, mark_compressed, set_filesystem_partition, @@ -207,7 +205,7 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() - if fs_choice.type_ == MenuSelectionType.Esc: + if fs_choice.type_ == MenuSelectionType.Skip: continue prompt = str(_('Enter the start sector (percentage or block number, default: {}): ')).format( @@ -322,15 +320,6 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, # Negate the current wipe marking block_device_struct["partitions"][partition]['wipe'] = not block_device_struct["partitions"][partition].get('wipe', False) - elif task == mark_encrypted: - title = _('{}\n\nSelect which partition to mark as encrypted').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - # Negate the current encryption marking - block_device_struct["partitions"][partition]['encrypted'] = \ - not block_device_struct["partitions"][partition].get('encrypted', False) - elif task == mark_bootable: title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) partition = select_partition(title, block_device_struct["partitions"]) @@ -371,30 +360,3 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result return block_device_struct - - -def select_encrypted_partitions( - title :str, - partitions :List[Partition], - multiple :bool = True, - filter_ :Callable = None -) -> Optional[int, List[int]]: - partition_indexes = _get_partitions(partitions, filter_) - - if len(partition_indexes) == 0: - return None - - # show current partition layout: - if len(partitions): - title += current_partition_layout(partitions, with_idx=True) + '\n' - - choice = Menu(title, partition_indexes, multi=multiple).run() - - if choice.type_ == MenuSelectionType.Esc: - return None - - if isinstance(choice.value, list): - for partition_index in choice.value: - yield int(partition_index) - else: - yield (partition_index) diff --git a/archinstall/lib/user_interaction/save_conf.py b/archinstall/lib/user_interaction/save_conf.py index f542bc9b..d60ef995 100644 --- a/archinstall/lib/user_interaction/save_conf.py +++ b/archinstall/lib/user_interaction/save_conf.py @@ -55,7 +55,7 @@ def save_config(config: Dict): preview_command=preview ).run() - if choice.type_ == MenuSelectionType.Esc: + if choice.type_ == MenuSelectionType.Skip: return while True: diff --git a/archinstall/lib/user_interaction/system_conf.py b/archinstall/lib/user_interaction/system_conf.py index 8b574b2c..42a6cec7 100644 --- a/archinstall/lib/user_interaction/system_conf.py +++ b/archinstall/lib/user_interaction/system_conf.py @@ -32,13 +32,13 @@ def select_kernel(preset: List[str] = None) -> List[str]: sort=True, multi=True, preset_values=preset, - raise_error_on_interrupt=True, - raise_error_warning_msg=warning + allow_reset=True, + allow_reset_warning_msg=warning ).run() match choice.type_: - case MenuSelectionType.Esc: return preset - case MenuSelectionType.Ctrl_c: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return [] case MenuSelectionType.Selection: return choice.value @@ -62,13 +62,13 @@ def select_harddrives(preset: List[str] = []) -> List[str]: list(options.keys()), preset_values=preset, multi=True, - raise_error_on_interrupt=True, - raise_error_warning_msg=warning + allow_reset=True, + allow_reset_warning_msg=warning ).run() match selected_harddrive.type_: - case MenuSelectionType.Ctrl_c: return [] - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: return [options[i] for i in selected_harddrive.value] @@ -132,7 +132,7 @@ def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> st ).run() match selection.type_: - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: bootloader = 'grub-install' if selection.value == Menu.yes() else bootloader else: # We use the common names for the bootloader as the selection, and map it back to the expected values. @@ -141,7 +141,7 @@ def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> st value = '' match selection.type_: - case MenuSelectionType.Esc: value = preset_val + case MenuSelectionType.Skip: value = preset_val case MenuSelectionType.Selection: value = selection.value if value != "": @@ -165,5 +165,5 @@ def ask_for_swap(preset: bool = True) -> bool: choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() match choice.type_: - case MenuSelectionType.Esc: return preset + case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True -- cgit v1.2.3-54-g00ecf