552 lines
16 KiB
Python
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()
|