archinstall/archinstall/lib/global_menu.py

552 lines
16 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Any
from archinstall.tui import MenuItem, MenuItemGroup
from . import disk
from .configuration import save_config
from .general import secret
from .hardware import SysInfo
from .interactions import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_for_additional_users,
ask_for_audio_selection,
ask_for_bootloader,
ask_for_swap,
ask_for_uki,
ask_hostname,
ask_ntp,
ask_to_configure_network,
select_additional_repositories,
select_kernel,
)
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .menu import AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu
from .models import NetworkConfiguration, NicType
from .models.audio_configuration import AudioConfiguration
from .models.bootloader import Bootloader
from .models.users import User
from .output import FormattedOutput
from .profile.profile_menu import ProfileConfiguration
from .translationhandler import Language, translation_handler
from .utils.util import format_cols, get_password
if TYPE_CHECKING:
from collections.abc import Callable
from archinstall.lib.translationhandler import DeferredTranslation
_: Callable[[str], DeferredTranslation]
class GlobalMenu(AbstractMenu):
def __init__(self, data_store: dict[str, Any]):
self._data_store = data_store
if 'archinstall-language' not in data_store:
data_store['archinstall-language'] = translation_handler.get_language_by_abbr('en')
menu_optioons = self._get_menu_options(data_store)
self._item_group = MenuItemGroup(
menu_optioons,
sort_items=False,
checkmarks=True
)
super().__init__(self._item_group, data_store)
def _get_menu_options(self, data_store: dict[str, Any]) -> list[MenuItem]:
return [
MenuItem(
text=str(_('Archinstall language')),
action=lambda x: self._select_archinstall_language(x),
display_action=lambda x: x.display_name if x else '',
key='archinstall-language'
),
MenuItem(
text=str(_('Locales')),
action=lambda x: self._locale_selection(x),
preview_action=self._prev_locale,
key='locale_config'
),
MenuItem(
text=str(_('Mirrors')),
action=lambda x: self._mirror_configuration(x),
preview_action=self._prev_mirror_config,
key='mirror_config'
),
MenuItem(
text=str(_('Disk configuration')),
action=lambda x: self._select_disk_config(x),
preview_action=self._prev_disk_config,
mandatory=True,
key='disk_config'
),
MenuItem(
text=str(_('Disk encryption')),
action=lambda x: self._disk_encryption(x),
preview_action=self._prev_disk_encryption,
key='disk_encryption',
dependencies=['disk_config']
),
MenuItem(
text=str(_('Swap')),
value=True,
action=lambda x: ask_for_swap(x),
preview_action=self._prev_swap,
key='swap',
),
MenuItem(
text=str(_('Bootloader')),
value=Bootloader.get_default(),
action=lambda x: self._select_bootloader(x),
preview_action=self._prev_bootloader,
mandatory=True,
key='bootloader',
),
MenuItem(
text=str(_('Unified kernel images')),
value=False,
action=lambda x: ask_for_uki(x),
preview_action=self._prev_uki,
key='uki',
),
MenuItem(
text=str(_('Hostname')),
value='archlinux',
action=lambda x: ask_hostname(x),
preview_action=self._prev_hostname,
key='hostname',
),
MenuItem(
text=str(_('Root password')),
action=lambda x: self._set_root_password(x),
preview_action=self._prev_root_pwd,
key='!root-password',
),
MenuItem(
text=str(_('User account')),
action=lambda x: self._create_user_account(x),
preview_action=self._prev_users,
key='!users'
),
MenuItem(
text=str(_('Profile')),
action=lambda x: self._select_profile(x),
preview_action=self._prev_profile,
key='profile_config'
),
MenuItem(
text=str(_('Audio')),
action=lambda x: ask_for_audio_selection(x),
preview_action=self._prev_audio,
key='audio_config'
),
MenuItem(
text=str(_('Kernels')),
value=['linux'],
action=lambda x: select_kernel(x),
preview_action=self._prev_kernel,
mandatory=True,
key='kernels'
),
MenuItem(
text=str(_('Network configuration')),
action=lambda x: ask_to_configure_network(x),
value={},
preview_action=self._prev_network_config,
key='network_config'
),
MenuItem(
text=str(_('Parallel Downloads')),
action=lambda x: add_number_of_parallel_downloads(x),
value=0,
preview_action=self._prev_parallel_dw,
key='parallel downloads'
),
MenuItem(
text=str(_('Additional packages')),
action=lambda x: ask_additional_packages_to_install(x),
value=[],
preview_action=self._prev_additional_pkgs,
key='packages'
),
MenuItem(
text=str(_('Optional repositories')),
action=lambda x: select_additional_repositories(x),
value=[],
preview_action=self._prev_additional_repos,
key='additional-repositories'
),
MenuItem(
text=str(_('Timezone')),
action=lambda x: ask_for_a_timezone(x),
value='UTC',
preview_action=self._prev_tz,
key='timezone'
),
MenuItem(
text=str(_('Automatic time sync (NTP)')),
action=lambda x: ask_ntp(x),
value=True,
preview_action=self._prev_ntp,
key='ntp'
),
MenuItem(
text=''
),
MenuItem(
text=str(_('Save configuration')),
action=lambda x: self._safe_config(),
key='save_config'
),
MenuItem(
text=str(_('Install')),
preview_action=self._prev_install_invalid_config,
key='install'
),
MenuItem(
text=str(_('Abort')),
action=lambda x: exit(1),
key='abort'
)
]
def _safe_config(self) -> None:
data: dict[str, Any] = {}
for item in self._item_group.items:
if item.key is not None:
data[item.key] = item.value
save_config(data)
def _missing_configs(self) -> list[str]:
def check(s) -> bool:
item = self._item_group.find_by_key(s)
return item.has_value()
def has_superuser() -> bool:
item = self._item_group.find_by_key('!users')
if item.has_value():
users = item.value
if users:
return any([u.sudo for u in users])
return False
missing = set()
for item in self._item_group.items:
if item.key in ['!root-password', '!users']:
if not check('!root-password') and not has_superuser():
missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
)
elif item.mandatory:
if not check(item.key):
missing.add(item.text)
return list(missing)
def _is_config_valid(self) -> bool:
"""
Checks the validity of the current configuration.
"""
if len(self._missing_configs()) != 0:
return False
return self._validate_bootloader() is None
def _select_archinstall_language(self, preset: Language) -> Language:
from .interactions.general_conf import select_archinstall_language
language = select_archinstall_language(translation_handler.translated_languages, preset)
translation_handler.activate(language)
self._upate_lang_text()
return language
def _upate_lang_text(self) -> None:
"""
The options for the global menu are generated with a static text;
each entry of the menu needs to be updated with the new translation
"""
new_options = self._get_menu_options(self._data_store)
for o in new_options:
if o.key is not None:
self._item_group.find_by_key(o.key).text = o.text
def _disk_encryption(self, preset: disk.DiskEncryption | None) -> disk.DiskEncryption | None:
disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
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
disk_encryption = disk.DiskEncryptionMenu(disk_config, preset=preset).run()
return disk_encryption
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
locale_config = LocaleMenu(preset).run()
return locale_config
def _prev_locale(self, item: MenuItem) -> str | None:
if not item.value:
return None
config: LocaleConfiguration = item.value
return config.preview()
def _prev_network_config(self, item: MenuItem) -> str | None:
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = FormattedOutput.as_table(network_config.nics)
else:
output = f'{_('Network configuration')}:\n{network_config.type.display_msg()}'
return output
return None
def _prev_additional_pkgs(self, item: MenuItem) -> str | None:
if item.value:
return format_cols(item.value, None)
return None
def _prev_additional_repos(self, item: MenuItem) -> str | None:
if item.value:
repos = ', '.join(item.value)
return f'{_("Additional repositories")}: {repos}'
return None
def _prev_tz(self, item: MenuItem) -> str | None:
if item.value:
return f'{_("Timezone")}: {item.value}'
return None
def _prev_ntp(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{_("NTP")}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_disk_config(self, item: MenuItem) -> str | None:
disk_layout_conf: disk.DiskLayoutConfiguration | None = item.value
if disk_layout_conf:
output = str(_('Configuration type: {}')).format(disk_layout_conf.config_type.display_msg()) + '\n'
if disk_layout_conf.config_type == disk.DiskLayoutType.Pre_mount:
output += str(_('Mountpoint')) + ': ' + str(disk_layout_conf.mountpoint)
if disk_layout_conf.lvm_config:
output += '{}: {}'.format(str(_('LVM configuration type')), disk_layout_conf.lvm_config.config_type.display_msg())
return output
return None
def _prev_swap(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{_("Swap on zram")}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_uki(self, item: MenuItem) -> str | None:
if item.value is not None:
output = f'{_('Unified kernel images')}: '
output += str(_('Enabled')) if item.value else str(_('Disabled'))
return output
return None
def _prev_hostname(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{_("Hostname")}: {item.value}'
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{_("Root password")}: {secret(item.value)}'
return None
def _prev_audio(self, item: MenuItem) -> str | None:
if item.value is not None:
config: AudioConfiguration = item.value
return f'{_("Audio")}: {config.audio.value}'
return None
def _prev_parallel_dw(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{_("Parallel Downloads")}: {item.value}'
return None
def _prev_kernel(self, item: MenuItem) -> str | None:
if item.value:
kernel = ', '.join(item.value)
return f'{_("Kernel")}: {kernel}'
return None
def _prev_bootloader(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{_("Bootloader")}: {item.value.value}'
return None
def _prev_disk_encryption(self, item: MenuItem) -> str | None:
disk_config: disk.DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
enc_config: disk.DiskEncryption | None = item.value
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'))
if enc_config:
enc_type = disk.EncryptionType.type_to_text(enc_config.encryption_type)
output = str(_('Encryption type')) + f': {enc_type}\n'
output += str(_('Password')) + f': {secret(enc_config.encryption_password)}\n'
if enc_config.partitions:
output += 'Partitions: {} selected'.format(len(enc_config.partitions)) + '\n'
elif enc_config.lvm_volumes:
output += 'LVM volumes: {} selected'.format(len(enc_config.lvm_volumes)) + '\n'
if enc_config.hsm_device:
output += f'HSM: {enc_config.hsm_device.manufacturer}'
return output
return None
def _validate_bootloader(self) -> str | None:
"""
Checks the selected bootloader is valid for the selected filesystem
type of the boot partition.
Returns [`None`] if the bootloader is valid, otherwise returns a
string with the error message.
XXX: The caller is responsible for wrapping the string with the translation
shim if necessary.
"""
bootloader = self._item_group.find_by_key('bootloader').value
boot_partition: disk.PartitionModification | None = None
if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications:
if boot_partition := layout.get_boot_partition():
break
else:
return "No disk layout selected"
if boot_partition is None:
return "Boot partition not found"
if bootloader == Bootloader.Limine:
if boot_partition.fs_type != disk.FilesystemType.Fat32:
return "Limine does not support booting from filesystems other than FAT32"
return None
def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
if missing := self._missing_configs():
text = str(_('Missing configurations:\n'))
for m in missing:
text += f'- {m}\n'
return text[:-1] # remove last new line
if error := self._validate_bootloader():
return str(_(f"Invalid configuration: {error}"))
return None
def _prev_users(self, item: MenuItem) -> str | None:
users: list[User] | None = item.value
if users:
return FormattedOutput.as_table(users)
return None
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value
if profile_config and profile_config.profile:
output = str(_('Profiles')) + ': '
if profile_names := profile_config.profile.current_selection_names():
output += ', '.join(profile_names) + '\n'
else:
output += profile_config.profile.name + '\n'
if profile_config.gfx_driver:
output += str(_('Graphics driver')) + ': ' + profile_config.gfx_driver.value + '\n'
if profile_config.greeter:
output += str(_('Greeter')) + ': ' + profile_config.greeter.value + '\n'
return output
return None
def _set_root_password(self, preset: str | None = None) -> str | None:
password = get_password(text=str(_('Root password')), allow_skip=True)
return password
def _select_disk_config(
self,
preset: disk.DiskLayoutConfiguration | None = None
) -> disk.DiskLayoutConfiguration | None:
disk_config = disk.DiskLayoutConfigurationMenu(preset).run()
if disk_config != preset:
self._menu_item_group.find_by_key('disk_encryption').value = None
return disk_config
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
bootloader = ask_for_bootloader(preset)
if bootloader:
uki = self._item_group.find_by_key('uki')
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
uki.value = False
uki.enabled = False
else:
uki.enabled = True
return bootloader
def _select_profile(self, current_profile: ProfileConfiguration | None):
from .profile.profile_menu import ProfileMenu
profile_config = ProfileMenu(preset=current_profile).run()
return profile_config
def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
preset = [] if preset is None else preset
users = ask_for_additional_users(defined_users=preset)
return users
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
mirror_configuration = MirrorMenu(preset=preset).run()
return mirror_configuration
def _prev_mirror_config(self, item: MenuItem) -> str | None:
if not item.value:
return None
mirror_config: MirrorConfiguration = item.value
output = ''
if mirror_config.regions:
output += '{}: {}\n\n'.format(str(_('Mirror regions')), mirror_config.regions)
if mirror_config.custom_mirrors:
table = FormattedOutput.as_table(mirror_config.custom_mirrors)
output += '{}\n{}'.format(str(_('Custom mirrors')), table)
return output.strip()