From 00b0ae7ba439a5a420095175b3bedd52c569db51 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 19 Apr 2023 20:55:42 +1000 Subject: PyParted and a large rewrite of the underlying partitioning (#1604) * Invert mypy files * Add optional pre-commit hooks * New profile structure * Serialize profiles * Use profile instead of classmethod * Custom profile setup * Separator between back * Support profile import via url * Move profiles module * Refactor files * Remove symlink * Add user to docker group * Update schema description * Handle list services * mypy fixes * mypy fixes * Rename profilesv2 to profiles * flake8 * mypy again * Support selecting DM * Fix mypy * Cleanup * Update greeter setting * Update schema * Revert toml changes * Poc external dependencies * Dependency support * New encryption menu * flake8 * Mypy and flake8 * Unify lsblk command * Update bootloader configuration * Git hooks * Fix import * Pyparted * Remove custom font setting * flake8 * Remove default preview * Manual partitioning menu * Update structure * Disk configuration * Update filesystem * luks2 encryption * Everything works until installation * Btrfsutil * Btrfs handling * Update btrfs * Save encryption config * Fix pipewire issue * Update mypy version * Update all pre-commit * Update package versions * Revert audio/pipewire * Merge master PRs * Add master changes * Merge master changes * Small renaming * Pull master changes * Reset disk enc after disk config change * Generate locals * Update naming * Fix imports * Fix broken sync * Fix pre selection on table menu * Profile menu * Update profile * Fix post_install * Added python-pyparted to PKGBUILD, this requires [testing] to be enabled in order to run makepkg. Package still works via python -m build etc. * Swaped around some setuptools logic in pyproject Since we define `package-data` and `packages` there should be no need for: ``` [tool.setuptools.packages.find] where = ["archinstall", "archinstall.*"] ``` * Removed pyproject collisions. Duplicate definitions. * Made sure pyproject.toml includes languages * Add example and update README * Fix pyproject issues * Generate locale * Refactor imports * Simplify imports * Add profile description and package examples * Align code * Fix mypy * Simplify imports * Fix saving config * Fix wrong luks merge * Refactor installation * Fix cdrom device loading * Fix wrongly merged code * Fix imports and greeter * Don't terminate on partprobe error * Use specific path on partprobe from luks * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update github workflow to test archinstall installation * Update sway merge * Generate locales * Update workflow --------- Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> --- examples/config-sample.json | 126 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 11 deletions(-) (limited to 'examples/config-sample.json') diff --git a/examples/config-sample.json b/examples/config-sample.json index dc8693a7..a7c5d537 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -1,28 +1,132 @@ { - "audio": null, - "bootloader": "systemd-bootctl", - "harddrives": [ - "/dev/loop0" - ], - "hostname": "", + "config_version": "2.5.2", + "additional-repositories": [], + "archinstall-language": "English", + "audio": "pipewire", + "bootloader": "Systemd-boot", + "debug": false, + "disk_config": { + "config_type": "default_layout", + "device_modifications": [ + { + "device": "/dev/sda", + "partitions": [ + { + "btrfs": [], + "flags": [ + "Boot" + ], + "fs_type": "fat32", + "length": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 512 + }, + "mount_options": [], + "mountpoint": "/boot", + "obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0", + "start": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 1 + }, + "status": "create", + "type": "primary" + }, + { + "btrfs": [], + "flags": [], + "fs_type": "ext4", + "length": { + "sector_size": null, + "total_size": null, + "unit": "GiB", + "value": 20 + }, + "mount_options": [], + "mountpoint": "/", + "obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208", + "start": { + "sector_size": null, + "total_size": null, + "unit": "MiB", + "value": 513 + }, + "status": "create", + "type": "primary" + }, + { + "btrfs": [], + "flags": [], + "fs_type": "ext4", + "length": { + "sector_size": null, + "total_size": { + "sector_size": null, + "total_size": null, + "unit": "B", + "value": 250148290560 + }, + "unit": "Percent", + "value": 100 + }, + "mount_options": [], + "mountpoint": "/home", + "obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f", + "start": { + "sector_size": null, + "total_size": null, + "unit": "GiB", + "value": 20 + }, + "status": "create", + "type": "primary" + } + ], + "wipe": true + } + ] + }, + "hostname": "archlinux", "kernels": [ "linux" ], "keyboard-layout": "us", "mirror-region": { - "Worldwide": { - "https://mirror.rackspace.com/archlinux/$repo/os/$arch": true + "Australia": { + "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, "nic": { - "type": "NM" + "dhcp": true, + "dns": null, + "gateway": null, + "iface": null, + "ip": null, + "type": "nm" }, + "no_pkg_lookups": false, "ntp": true, + "offline": false, "packages": [], - "profile": null, + "parallel downloads": 0, + "profile_config": { + "gfx_driver": "All open-source (default)", + "greeter": "sddm", + "profile": { + "details": [ + "Kde" + ], + "main": "Desktop" + } + }, "script": "guided", + "silent": false, "swap": true, "sys-encoding": "utf-8", "sys-language": "en_US", - "timezone": "UTC" + "timezone": "UTC", + "version": "2.5.2" } -- cgit v1.2.3-70-g09d2 From 2f273868d416c3309191db8c616aae683d78370a Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 17 Jul 2023 17:27:21 +1000 Subject: Fix network settings loading from config file (#1921) * Fix network config error and simplify code * Update schema and exmaple --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 7 +- archinstall/lib/global_menu.py | 27 ++-- archinstall/lib/installer.py | 18 +-- archinstall/lib/interactions/__init__.py | 2 +- archinstall/lib/interactions/network_conf.py | 172 -------------------- archinstall/lib/interactions/network_menu.py | 159 +++++++++++++++++++ archinstall/lib/models/__init__.py | 6 +- archinstall/lib/models/network_configuration.py | 200 ++++++++++-------------- archinstall/scripts/guided.py | 11 +- archinstall/scripts/swiss.py | 9 +- docs/installing/guided.rst | 8 +- examples/config-sample.json | 20 ++- examples/custom-command-sample.json | 4 +- examples/interactive_installation.py | 7 +- schema.json | 23 +-- 15 files changed, 313 insertions(+), 360 deletions(-) delete mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/network_menu.py (limited to 'examples/config-sample.json') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index af811465..c4b64912 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -225,10 +225,9 @@ def load_config(): if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) - if arguments.get('nic', None) is not None: - handler = models.NetworkConfigurationHandler() - handler.parse_arguments(arguments.get('nic')) - arguments['nic'] = handler.configuration + if arguments.get('network_config', None) is not None: + config = NetworkConfiguration.parse_arg(arguments.get('network_config')) + arguments['network_config'] = config if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None: users = arguments.get('!users', None) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 02b1b0b6..5503d9ce 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -7,7 +7,7 @@ from .general import secret from .locale.locale_menu import LocaleConfiguration, LocaleMenu from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu -from .models import NetworkConfiguration +from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader from .models.users import User from .output import FormattedOutput @@ -142,7 +142,7 @@ class GlobalMenu(AbstractMenu): lambda preset: select_additional_repositories(preset), display_func=lambda x: ', '.join(x) if x else None, default=[]) - self._menu_options['nic'] = \ + self._menu_options['network_config'] = \ Selector( _('Network configuration'), lambda preset: ask_to_configure_network(preset), @@ -221,14 +221,11 @@ class GlobalMenu(AbstractMenu): return _('Install ({} config(s) missing)').format(missing) return _('Install') - def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: - if not cur_value: - return _('Not configured, unavailable unless setup manually') - else: - if isinstance(cur_value, list): - return str(_('Configured {} interfaces')).format(len(cur_value)) - else: - return str(cur_value) + def _display_network_conf(self, config: Optional[NetworkConfiguration]) -> str: + if not config: + return str(_('Not configured, unavailable unless setup manually')) + + return config.type.display_msg() def _disk_encryption(self, preset: Optional[disk.DiskEncryption]) -> Optional[disk.DiskEncryption]: mods: Optional[List[disk.DeviceModification]] = self._menu_options['disk_config'].current_selection @@ -257,11 +254,11 @@ class GlobalMenu(AbstractMenu): return None def _prev_network_config(self) -> Optional[str]: - selector = self._menu_options['nic'] - if selector.has_selection(): - ifaces = selector.current_selection - if isinstance(ifaces, list): - return FormattedOutput.as_table(ifaces) + selector: Optional[NetworkConfiguration] = self._menu_options['network_config'].current_selection + if selector: + if selector.type == NicType.MANUAL: + output = FormattedOutput.as_table(selector.nics) + return output return None def _prev_additional_pkgs(self): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 02d48768..8c5e7648 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -19,7 +19,7 @@ from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .luks import Luks2 from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .models.bootloader import Bootloader -from .models.network_configuration import NetworkConfiguration +from .models.network_configuration import Nic from .models.users import User from .output import log, error, info, warn, debug from . import pacman @@ -458,20 +458,20 @@ class Installer: def drop_to_shell(self) -> None: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) - def configure_nic(self, network_config: NetworkConfiguration) -> None: - conf = network_config.as_systemd_config() + def configure_nic(self, nic: Nic): + conf = nic.as_systemd_config() for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): conf = plugin.on_configure_nic( - network_config.iface, - network_config.dhcp, - network_config.ip, - network_config.gateway, - network_config.dns + nic.iface, + nic.dhcp, + nic.ip, + nic.gateway, + nic.dns ) or conf - with open(f"{self.target}/etc/systemd/network/10-{network_config.iface}.network", "a") as netconf: + with open(f"{self.target}/etc/systemd/network/10-{nic.iface}.network", "a") as netconf: netconf.write(str(conf)) def copy_iso_network_config(self, enable_services :bool = False) -> bool: diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 466cfa0b..53be8e7a 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,5 +1,5 @@ from .manage_users_conf import UserList, ask_for_additional_users -from .network_conf import ManualNetworkConfig, ask_to_configure_network +from .network_menu import ManualNetworkConfig, ask_to_configure_network from .utils import get_password from .disk_conf import ( diff --git a/archinstall/lib/interactions/network_conf.py b/archinstall/lib/interactions/network_conf.py deleted file mode 100644 index 18a834a1..00000000 --- a/archinstall/lib/interactions/network_conf.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -import ipaddress -from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict - -from ..menu import MenuSelectionType, TextInput -from ..models.network_configuration import NetworkConfiguration, NicType - -from ..networking import list_interfaces -from ..output import FormattedOutput, warn -from ..menu import ListManager, Menu - -if TYPE_CHECKING: - _: Any - - -class ManualNetworkConfig(ListManager): - """ - subclass of ListManager for the managing of network configurations - """ - - def __init__(self, prompt: str, ifaces: List[NetworkConfiguration]): - self._actions = [ - str(_('Add interface')), - str(_('Edit interface')), - str(_('Delete interface')) - ] - - super().__init__(prompt, ifaces, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[NetworkConfiguration]) -> Dict[str, Optional[NetworkConfiguration]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[NetworkConfiguration]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, iface in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = iface - - return display_data - - def selected_action_display(self, iface: NetworkConfiguration) -> str: - return iface.iface if iface.iface else '' - - def handle_action(self, action: str, entry: Optional[NetworkConfiguration], data: List[NetworkConfiguration]): - if action == self._actions[0]: # add - iface_name = self._select_iface(data) - if iface_name: - iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) - iface = self._edit_iface(iface) - data += [iface] - elif entry: - if action == self._actions[1]: # edit interface - data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data - - def _select_iface(self, data: List[NetworkConfiguration]) -> Optional[Any]: - all_ifaces = list_interfaces().values() - existing_ifaces = [d.iface for d in data] - available = set(all_ifaces) - set(existing_ifaces) - choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() - - if choice.type_ == MenuSelectionType.Skip: - return None - - return choice.value - - def _edit_iface(self, edit_iface: NetworkConfiguration): - iface_name = edit_iface.iface - modes = ['DHCP (auto detect)', 'IP (static)'] - default_mode = 'DHCP (auto detect)' - - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() - - if mode.value == 'IP (static)': - while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) - ip = TextInput(prompt, edit_iface.ip).run().strip() - # Implemented new check for correct IP/subnet input - try: - ipaddress.ip_interface(ip) - break - except ValueError: - warn("You need to enter a valid IP in IP-config mode") - - # Implemented new check for correct gateway IP address - gateway = None - - while 1: - gateway = TextInput( - _('Enter your gateway (router) IP address or leave blank for none: '), - edit_iface.gateway - ).run().strip() - try: - if len(gateway) > 0: - ipaddress.ip_address(gateway) - break - except ValueError: - warn("You need to enter a valid gateway (router) IP address") - - if edit_iface.dns: - display_dns = ' '.join(edit_iface.dns) - else: - display_dns = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() - - dns = [] - if len(dns_input): - dns = dns_input.split(' ') - - return NetworkConfiguration(NicType.MANUAL, iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) - else: - # this will contain network iface names - return NetworkConfiguration(NicType.MANUAL, iface=iface_name) - - -def ask_to_configure_network( - preset: Union[NetworkConfiguration, List[NetworkConfiguration]] -) -> Optional[NetworkConfiguration | List[NetworkConfiguration]]: - """ - Configure the network on the newly installed system - """ - network_options = { - 'none': str(_('No network configuration')), - 'iso_config': str(_('Copy ISO network configuration to installation')), - 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), - 'manual': str(_('Manual configuration')) - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - - if preset and not isinstance(preset, list): - if preset.type == 'iso_config': - cursor_idx = 0 - elif preset.type == 'network_manager': - cursor_idx = 1 - - warning = str(_('Are you sure you want to reset this setting?')) - - choice = Menu( - _('Select one network interface to configure'), - list(network_options.values()), - cursor_index=cursor_idx, - sort=False, - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Reset: return None - - if choice.value == network_options['none']: - return None - elif choice.value == network_options['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif choice.value == network_options['network_manager']: - return NetworkConfiguration(NicType.NM) - elif choice.value == network_options['manual']: - preset_ifaces = preset if isinstance(preset, list) else [] - return ManualNetworkConfig('Configure interfaces', preset_ifaces).run() - - return preset diff --git a/archinstall/lib/interactions/network_menu.py b/archinstall/lib/interactions/network_menu.py new file mode 100644 index 00000000..14fc5785 --- /dev/null +++ b/archinstall/lib/interactions/network_menu.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import ipaddress +from typing import Any, Optional, TYPE_CHECKING, List, Dict + +from ..menu import MenuSelectionType, TextInput +from ..models.network_configuration import NetworkConfiguration, NicType, Nic + +from ..networking import list_interfaces +from ..output import FormattedOutput, warn +from ..menu import ListManager, Menu + +if TYPE_CHECKING: + _: Any + + +class ManualNetworkConfig(ListManager): + """ + subclass of ListManager for the managing of network configurations + """ + + def __init__(self, prompt: str, preset: List[Nic]): + self._actions = [ + str(_('Add interface')), + str(_('Edit interface')), + str(_('Delete interface')) + ] + super().__init__(prompt, preset, [self._actions[0]], self._actions[1:]) + + def reformat(self, data: List[Nic]) -> Dict[str, Optional[Nic]]: + table = FormattedOutput.as_table(data) + rows = table.split('\n') + + # these are the header rows of the table and do not map to any User obviously + # we're adding 2 spaces as prefix because the menu selector '> ' will be put before + # the selectable rows so the header has to be aligned + display_data: Dict[str, Optional[Nic]] = {f' {rows[0]}': None, f' {rows[1]}': None} + + for row, iface in zip(rows[2:], data): + row = row.replace('|', '\\|') + display_data[row] = iface + + return display_data + + def selected_action_display(self, nic: Nic) -> str: + return nic.iface if nic.iface else '' + + def handle_action(self, action: str, entry: Optional[Nic], data: List[Nic]): + if action == self._actions[0]: # add + iface = self._select_iface(data) + if iface: + nic = Nic(iface=iface) + nic = self._edit_iface(nic) + data += [nic] + elif entry: + if action == self._actions[1]: # edit interface + data = [d for d in data if d.iface != entry.iface] + data.append(self._edit_iface(entry)) + elif action == self._actions[2]: # delete + data = [d for d in data if d != entry] + + return data + + def _select_iface(self, data: List[Nic]) -> Optional[str]: + all_ifaces = list_interfaces().values() + existing_ifaces = [d.iface for d in data] + available = set(all_ifaces) - set(existing_ifaces) + choice = Menu(str(_('Select interface to add')), list(available), skip=True).run() + + if choice.type_ == MenuSelectionType.Skip: + return None + + return choice.single_value + + def _edit_iface(self, edit_nic: Nic) -> Nic: + iface_name = edit_nic.iface + modes = ['DHCP (auto detect)', 'IP (static)'] + default_mode = 'DHCP (auto detect)' + + prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) + mode = Menu(prompt, modes, default_option=default_mode, skip=False).run() + + if mode.value == 'IP (static)': + while 1: + prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + ip = TextInput(prompt, edit_nic.ip).run().strip() + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) + break + except ValueError: + warn("You need to enter a valid IP in IP-config mode") + + # Implemented new check for correct gateway IP address + gateway = None + + while 1: + gateway = TextInput( + _('Enter your gateway (router) IP address or leave blank for none: '), + edit_nic.gateway + ).run().strip() + try: + if len(gateway) > 0: + ipaddress.ip_address(gateway) + break + except ValueError: + warn("You need to enter a valid gateway (router) IP address") + + if edit_nic.dns: + display_dns = ' '.join(edit_nic.dns) + else: + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() + + dns = [] + if len(dns_input): + dns = dns_input.split(' ') + + return Nic(iface=iface_name, ip=ip, gateway=gateway, dns=dns, dhcp=False) + else: + # this will contain network iface names + return Nic(iface=iface_name) + + +def ask_to_configure_network(preset: Optional[NetworkConfiguration]) -> Optional[NetworkConfiguration]: + """ + Configure the network on the newly installed system + """ + options = {n.display_msg(): n for n in NicType} + preset_val = preset.type.display_msg() if preset else None + warning = str(_('Are you sure you want to reset this setting?')) + + choice = Menu( + _('Select one network interface to configure'), + list(options.keys()), + preset_values=preset_val, + sort=False, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Reset: return None + case MenuSelectionType.Selection: + nic_type = options[choice.single_value] + + match nic_type: + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + preset_nics = preset.nics if preset else [] + nics = ManualNetworkConfig('Configure interfaces', preset_nics).run() + if nics: + return NetworkConfiguration(NicType.MANUAL, nics) + + return preset diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 8cc49ea0..7415f63f 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -1,4 +1,8 @@ -from .network_configuration import NetworkConfiguration, NicType, NetworkConfigurationHandler +from .network_configuration import ( + NetworkConfiguration, + NicType, + Nic +) from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index e564b97b..fac7bbef 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -2,56 +2,64 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING, Tuple +from typing import List, Optional, Dict, Any, TYPE_CHECKING, Tuple -from ..output import debug from ..profile import ProfileConfiguration if TYPE_CHECKING: _: Any -class NicType(str, Enum): +class NicType(Enum): ISO = "iso" NM = "nm" MANUAL = "manual" + def display_msg(self) -> str: + match self: + case NicType.ISO: + return str(_('Copy ISO network configuration to installation')) + case NicType.NM: + return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')) + case NicType.MANUAL: + return str(_('Manual configuration')) + @dataclass -class NetworkConfiguration: - type: NicType +class Nic: iface: Optional[str] = None ip: Optional[str] = None dhcp: bool = True gateway: Optional[str] = None dns: List[str] = field(default_factory=list) - def __str__(self): - if self.is_iso(): - return "Copy ISO configuration" - elif self.is_network_manager(): - return "Use NetworkManager" - elif self.is_manual(): - if self.dhcp: - return f'iface={self.iface}, dhcp=auto' - else: - return f'iface={self.iface}, ip={self.ip}, dhcp=staticIp, gateway={self.gateway}, dns={self.dns}' - else: - return 'Unknown type' - def table_data(self) -> Dict[str, Any]: - exclude_fields = ['type'] - data = {} - for k, v in self.__dict__.items(): - if k not in exclude_fields: - if isinstance(v, list) and len(v) == 0: - v = '' - elif v is None: - v = '' - - data[k] = v - - return data + return { + 'iface': self.iface if self.iface else '', + 'ip': self.ip if self.ip else '', + 'dhcp': self.dhcp, + 'gateway': self.gateway if self.gateway else '', + 'dns': self.dns + } + + def __dump__(self) -> Dict[str, Any]: + return { + 'iface': self.iface, + 'ip': self.ip, + 'dhcp': self.dhcp, + 'gateway': self.gateway, + 'dns': self.dns + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> Nic: + return Nic( + iface=arg.get('iface', None), + ip=arg.get('ip', None), + dhcp=arg.get('dhcp', True), + gateway=arg.get('gateway', None), + dns=arg.get('dns', []), + ) def as_systemd_config(self) -> str: match: List[Tuple[str, str]] = [] @@ -80,107 +88,57 @@ class NetworkConfiguration: return config_str - def json(self) -> Dict: - # for json serialization when calling json.dumps(...) on this class - return self.__dict__ - - def is_iso(self) -> bool: - return self.type == NicType.ISO - - def is_network_manager(self) -> bool: - return self.type == NicType.NM - - def is_manual(self) -> bool: - return self.type == NicType.MANUAL - - -class NetworkConfigurationHandler: - def __init__(self, config: Union[None, NetworkConfiguration, List[NetworkConfiguration]] = None): - self._configuration = config - - @property - def configuration(self): - return self._configuration - def config_installer( +@dataclass +class NetworkConfiguration: + type: NicType + nics: List[Nic] = field(default_factory=list) + + def __dump__(self) -> Dict[str, Any]: + config: Dict[str, Any] = {'type': self.type.value} + if self.nics: + config['nics'] = [n.__dump__() for n in self.nics] + + return config + + @staticmethod + def parse_arg(config: Dict[str, Any]) -> Optional[NetworkConfiguration]: + nic_type = config.get('type', None) + if not nic_type: + return None + + match NicType(nic_type): + case NicType.ISO: + return NetworkConfiguration(NicType.ISO) + case NicType.NM: + return NetworkConfiguration(NicType.NM) + case NicType.MANUAL: + nics_arg = config.get('nics', []) + if nics_arg: + nics = [Nic.parse_arg(n) for n in nics_arg] + return NetworkConfiguration(NicType.MANUAL, nics) + + return None + + def install_network_config( self, installation: Any, profile_config: Optional[ProfileConfiguration] = None ): - if self._configuration is None: - return - - if isinstance(self._configuration, list): - for config in self._configuration: - installation.configure_nic(config) - - installation.enable_service('systemd-networkd') - installation.enable_service('systemd-resolved') - else: - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - if self._configuration.is_iso(): + match self.type: + case NicType.ISO: installation.copy_iso_network_config( - enable_services=True # Sources the ISO network configuration to the install medium. + enable_services=True # Sources the ISO network configuration to the install medium. ) - elif self._configuration.is_network_manager(): + case NicType.NM: installation.add_additional_packages(["networkmanager"]) if profile_config and profile_config.profile: if profile_config.profile.is_desktop_type_profile(): installation.add_additional_packages(["network-manager-applet"]) installation.enable_service('NetworkManager.service') + case NicType.MANUAL: + for nic in self.nics: + installation.configure_nic(nic) - def _parse_manual_config(self, configs: List[Dict[str, Any]]) -> Optional[List[NetworkConfiguration]]: - configurations = [] - - for manual_config in configs: - iface = manual_config.get('iface', None) - - if iface is None: - raise ValueError('No iface specified for manual configuration') - - if manual_config.get('dhcp', False) or not any([manual_config.get(v, '') for v in ['ip', 'gateway', 'dns']]): - configurations.append( - NetworkConfiguration(NicType.MANUAL, iface=iface) - ) - else: - ip = manual_config.get('ip', '') - if not ip: - raise ValueError('Manual nic configuration with no auto DHCP requires an IP address') - - dns = manual_config.get('dns', []) - if not isinstance(dns, list): - dns = [dns] - - configurations.append( - NetworkConfiguration( - NicType.MANUAL, - iface=iface, - ip=ip, - gateway=manual_config.get('gateway', ''), - dns=dns, - dhcp=False - ) - ) - - return configurations - - def _parse_nic_type(self, nic_type: str) -> NicType: - try: - return NicType(nic_type) - except ValueError: - options = [e.value for e in NicType] - raise ValueError(f'Unknown nic type: {nic_type}. Possible values are {options}') - - def parse_arguments(self, config: Any): - if isinstance(config, list): # new data format - self._configuration = self._parse_manual_config(config) - elif nic_type := config.get('type', None): # new data format - type_ = self._parse_nic_type(nic_type) - - if type_ != NicType.MANUAL: - self._configuration = NetworkConfiguration(type_) - else: # manual configuration settings - self._configuration = self._parse_manual_config([config]) - else: - debug(f'Unable to parse network configuration: {config}') + installation.enable_service('systemd-networkd') + installation.enable_service('systemd-resolved') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 7f9b9fd6..c8df590d 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Optional import archinstall from archinstall import info, debug @@ -14,7 +14,7 @@ from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors from archinstall.lib.models.bootloader import Bootloader -from archinstall.lib.models.network_configuration import NetworkConfigurationHandler +from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler @@ -82,7 +82,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -158,11 +158,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config: Optional[NetworkConfiguration] = archinstall.arguments.get('network_config', None) if network_config: - handler = NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index 375458a1..a2ab0549 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', + '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -110,7 +110,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config','bootloader', 'hostname', '!root-password', '!users', 'profile_config', 'audio', 'kernels', - 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' + 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] mandatory_list = ['hostname'] @@ -222,11 +222,10 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 4cb07ae1..0a075282 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -29,7 +29,7 @@ To start the installer, run the following in the latest Arch Linux ISO: .. code-block:: sh archinstall --script guided - + | The ``--script guided`` argument is optional as it's the default behavior. | But this will use our most guided installation and if you skip all the option steps it will install a minimal Arch Linux experience. @@ -49,7 +49,7 @@ There are three different configuration files, all of which are optional. .. note:: You can always get the latest options with ``archinstall --dry-run``, but edit the following json according to your needs. Save the configuration as a ``.json`` file. Archinstall can source it via a local or remote path (URL) - + .. code-block:: json { @@ -72,8 +72,8 @@ There are three different configuration files, all of which are optional. ], "keyboard-language": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/config-sample.json b/examples/config-sample.json index a7c5d537..38415b2c 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -99,13 +99,19 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "nic": { - "dhcp": true, - "dns": null, - "gateway": null, - "iface": null, - "ip": null, - "type": "nm" + "network_config": { + "nics": [ + { + "dhcp": false, + "dns": [ + "3.3.3.3" + ], + "gateway": "2.2.2.2", + "iface": "enp0s31f6", + "ip": "1.1.1.1" + } + ], + "type": "manual" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index 8d8d611d..b2250e2c 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -12,8 +12,8 @@ ], "keyboard-layout": "us", "mirror-region": "Worldwide", - "nic": { - "type": "NM" + "network_config": { + "type": "nm" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index ce1a80ec..8e82ca7e 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -61,7 +61,7 @@ def ask_user_questions(): global_menu.enable('parallel downloads') # Ask or Call the helper function that asks the user to optionally configure a network. - global_menu.enable('nic') + global_menu.enable('network_config') global_menu.enable('timezone') @@ -137,11 +137,10 @@ def perform_installation(mountpoint: Path): # If user selected to copy the current ISO network configuration # Perform a copy of the config - network_config = archinstall.arguments.get('nic', None) + network_config = archinstall.arguments.get('network_config', None) if network_config: - handler = models.NetworkConfigurationHandler(network_config) - handler.config_installer( + network_config.install_network_config( installation, archinstall.arguments.get('profile_config', None) ) diff --git a/schema.json b/schema.json index 0a41ebf0..b74588a1 100644 --- a/schema.json +++ b/schema.json @@ -69,21 +69,26 @@ "description": "By default, it will autodetect the best region. Enter a region or a dictionary of regions and mirrors to use specific ones", "type": "object" }, - "nic": { + "network_config": { "description": "Choose between NetworkManager, manual configuration, use systemd-networkd from the ISO or no configuration", "type": "object", "properties": { "type": "string", - "iface": "string", - "dhcp": "boolean", - "ip": "string", - "gateway": "string", - "dns": { - "description": "List of DNS servers", + "nics": [ "type": "array", "items": { - "type": "string" - } + "iface": "string", + "dhcp": "boolean", + "ip": "string", + "gateway": "string", + "dns": { + "description": "List of DNS servers", + "type": "array", + "items": { + "type": "string" + } + } + ] } } }, -- cgit v1.2.3-70-g09d2 From 439bb5428bb6a6f512f695a83ee6b3b8f6537598 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 25 Jul 2023 19:17:09 +1000 Subject: Fix 1934 - audio server regression (#1946) * Audio configuration * Update * Update schema --------- Co-authored-by: Daniel Girtler --- archinstall/__init__.py | 3 ++ archinstall/lib/global_menu.py | 27 ++++++++------ archinstall/lib/interactions/general_conf.py | 30 +++++++++++---- archinstall/lib/models/__init__.py | 1 + archinstall/lib/models/audio_configuration.py | 54 +++++++++++++++++++++++++++ archinstall/scripts/guided.py | 19 +++------- archinstall/scripts/swiss.py | 19 ++++------ docs/installing/guided.rst | 2 +- examples/config-sample.json | 22 ++++------- examples/custom-command-sample.json | 1 - examples/interactive_installation.py | 22 +++-------- schema.json | 19 ++++++---- 12 files changed, 137 insertions(+), 82 deletions(-) create mode 100644 archinstall/lib/models/audio_configuration.py (limited to 'examples/config-sample.json') diff --git a/archinstall/__init__.py b/archinstall/__init__.py index c4b64912..cfaecd16 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -237,6 +237,9 @@ def load_config(): if arguments.get('bootloader', None) is not None: arguments['bootloader'] = models.Bootloader.from_arg(arguments['bootloader']) + if arguments.get('audio_config', None) is not None: + arguments['audio_config'] = models.AudioConfiguration.parse_arg(arguments['audio_config']) + if arguments.get('disk_encryption', None) is not None and disk_config is not None: password = arguments.get('encryption_password', '') arguments['disk_encryption'] = disk.DiskEncryption.parse_arg( diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 5503d9ce..fb62b7b5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING +from typing import Any, List, Optional, Dict, TYPE_CHECKING from . import disk from .general import secret @@ -9,6 +9,7 @@ from .menu import Selector, AbstractMenu from .mirrors import MirrorConfiguration, MirrorMenu from .models import NetworkConfiguration, NicType from .models.bootloader import Bootloader +from .models.audio_configuration import Audio, AudioConfiguration from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration @@ -109,12 +110,11 @@ class GlobalMenu(AbstractMenu): display_func=lambda x: x.profile.name if x else '', preview_func=self._prev_profile ) - self._menu_options['audio'] = \ + self._menu_options['audio_config'] = \ Selector( _('Audio'), lambda preset: self._select_audio(preset), - display_func=lambda x: x if x else '', - default=None + display_func=lambda x: self._display_audio(x) ) self._menu_options['parallel downloads'] = \ Selector( @@ -421,13 +421,18 @@ class GlobalMenu(AbstractMenu): profile_config = ProfileMenu(store, preset=current_profile).run() return profile_config - def _select_audio(self, current: Union[str, None]) -> Optional[str]: - profile_config: Optional[ProfileConfiguration] = self._menu_options['profile_config'].current_selection - if profile_config and profile_config.profile: - is_desktop = profile_config.profile.is_desktop_profile() if profile_config else False - selection = ask_for_audio_selection(is_desktop, current) - return selection - return None + def _select_audio( + self, + current: Optional[AudioConfiguration] = None + ) -> Optional[AudioConfiguration]: + selection = ask_for_audio_selection(current) + return selection + + def _display_audio(self, current: Optional[AudioConfiguration]) -> str: + if not current: + return Audio.no_audio_text() + else: + return current.audio.name def _create_user_account(self, defined_users: List[User]) -> List[User]: users = ask_for_additional_users(defined_users=defined_users) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index ad9ee386..1c570a69 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -5,6 +5,7 @@ from typing import List, Any, Optional, TYPE_CHECKING from ..locale import list_timezones, list_keyboard_languages from ..menu import MenuSelectionType, Menu, TextInput +from ..models.audio_configuration import Audio, AudioConfiguration from ..output import warn from ..packages.packages import validate_package_list from ..storage import storage @@ -55,16 +56,31 @@ def ask_for_a_timezone(preset: Optional[str] = None) -> Optional[str]: return None -def ask_for_audio_selection(desktop: bool = True, preset: Optional[str] = None) -> Optional[str]: - no_audio = str(_('No audio server')) - choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] - default = 'pipewire' if desktop else no_audio +def ask_for_audio_selection( + current: Optional[AudioConfiguration] = None +) -> Optional[AudioConfiguration]: + choices = [ + Audio.Pipewire.name, + Audio.Pulseaudio.name, + Audio.no_audio_text() + ] - choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run() + preset = current.audio.name if current else None + + choice = Menu( + _('Choose an audio server'), + choices, + preset_values=preset + ).run() match choice.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return choice.single_value + case MenuSelectionType.Skip: return current + case MenuSelectionType.Selection: + value = choice.single_value + if value == Audio.no_audio_text(): + return None + else: + return AudioConfiguration(Audio[value]) return None diff --git a/archinstall/lib/models/__init__.py b/archinstall/lib/models/__init__.py index 7415f63f..a1c90e48 100644 --- a/archinstall/lib/models/__init__.py +++ b/archinstall/lib/models/__init__.py @@ -6,3 +6,4 @@ from .network_configuration import ( from .bootloader import Bootloader from .gen import VersionDef, PackageSearchResult, PackageSearch, LocalPackage from .users import PasswordStrength, User +from .audio_configuration import Audio, AudioConfiguration diff --git a/archinstall/lib/models/audio_configuration.py b/archinstall/lib/models/audio_configuration.py new file mode 100644 index 00000000..3a4029db --- /dev/null +++ b/archinstall/lib/models/audio_configuration.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, TYPE_CHECKING, Dict + +from ..hardware import SysInfo +from ..output import info +from ...default_profiles.applications.pipewire import PipewireProfile + +if TYPE_CHECKING: + _: Any + + +@dataclass +class Audio(Enum): + Pipewire = 'pipewire' + Pulseaudio = 'pulseaudio' + + @staticmethod + def no_audio_text() -> str: + return str(_('No audio server')) + + +@dataclass +class AudioConfiguration: + audio: Audio + + def __dump__(self) -> Dict[str, Any]: + return { + 'audio': self.audio.value + } + + @staticmethod + def parse_arg(arg: Dict[str, Any]) -> 'AudioConfiguration': + return AudioConfiguration( + Audio(arg['audio']) + ) + + def install_audio_config( + self, + installation: Any + ): + info(f'Installing audio server: {self.audio.name}') + + match self.audio: + case Audio.Pipewire: + PipewireProfile().install(installation) + case Audio.Pulseaudio: + installation.add_additional_packages("pulseaudio") + + if SysInfo.requires_sof_fw(): + installation.add_additional_packages('sof-firmware') + + if SysInfo.requires_alsa_fw(): + installation.add_additional_packages('alsa-firmware') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index c8df590d..605d2b0e 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -8,11 +8,11 @@ from archinstall import SysInfo from archinstall.lib import locale from archinstall.lib import disk from archinstall.lib.global_menu import GlobalMenu -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.installer import Installer from archinstall.lib.menu import Menu from archinstall.lib.mirrors import use_mirrors, add_custom_mirrors +from archinstall.lib.models import AudioConfiguration from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network_configuration import NetworkConfiguration from archinstall.lib.networking import check_mirror_reachable @@ -70,7 +70,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -172,18 +172,9 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: info("No audio server will be installed") diff --git a/archinstall/scripts/swiss.py b/archinstall/scripts/swiss.py index a2ab0549..cd532f6d 100644 --- a/archinstall/scripts/swiss.py +++ b/archinstall/scripts/swiss.py @@ -1,7 +1,7 @@ import os from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, Optional import archinstall from archinstall import SysInfo, info, debug @@ -9,13 +9,13 @@ from archinstall.lib import mirrors from archinstall.lib import models from archinstall.lib import disk from archinstall.lib import locale +from archinstall.lib.models import AudioConfiguration from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib import menu from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer from archinstall.lib.configuration import ConfigurationOutput -from archinstall.default_profiles.applications.pipewire import PipewireProfile if TYPE_CHECKING: _: Any @@ -95,7 +95,7 @@ class SwissMainMenu(GlobalMenu): options_list = [ 'mirror_config', 'disk_config', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', - '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'network_config', + '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -109,7 +109,7 @@ class SwissMainMenu(GlobalMenu): case ExecutionMode.Only_OS: options_list = [ 'mirror_config','bootloader', 'hostname', - '!root-password', '!users', 'profile_config', 'audio', 'kernels', + '!root-password', '!users', 'profile_config', 'audio_config', 'kernels', 'packages', 'additional-repositories', 'network_config', 'timezone', 'ntp' ] @@ -236,14 +236,11 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") + audio_config: Optional[AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile_handler.install_profile_config(installation, profile_config) diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 0a075282..c5e7f1ed 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -53,7 +53,7 @@ There are three different configuration files, all of which are optional. .. code-block:: json { - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "systemd-bootctl", "custom-commands": [ "cd /home/devel; git clone https://aur.archlinux.org/paru.git", diff --git a/examples/config-sample.json b/examples/config-sample.json index 38415b2c..ed1cc38e 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -2,7 +2,7 @@ "config_version": "2.5.2", "additional-repositories": [], "archinstall-language": "English", - "audio": "pipewire", + "audio_config": {"audio": "pipewire"}, "bootloader": "Systemd-boot", "debug": false, "disk_config": { @@ -99,19 +99,13 @@ "http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch": true, } }, - "network_config": { - "nics": [ - { - "dhcp": false, - "dns": [ - "3.3.3.3" - ], - "gateway": "2.2.2.2", - "iface": "enp0s31f6", - "ip": "1.1.1.1" - } - ], - "type": "manual" + "nic": { + "dhcp": true, + "dns": null, + "gateway": null, + "iface": null, + "ip": null, + "type": "nm" }, "no_pkg_lookups": false, "ntp": true, diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index b2250e2c..34d63d74 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -1,6 +1,5 @@ { "dry_run": true, - "audio": "none", "bootloader": "systemd-bootctl", "debug": false, "harddrives": [ diff --git a/examples/interactive_installation.py b/examples/interactive_installation.py index 8e82ca7e..e075df9b 100644 --- a/examples/interactive_installation.py +++ b/examples/interactive_installation.py @@ -1,12 +1,11 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import archinstall from archinstall import Installer from archinstall import profile from archinstall import SysInfo from archinstall import mirrors -from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall import disk from archinstall import menu from archinstall import models @@ -49,7 +48,7 @@ def ask_user_questions(): global_menu.enable('profile_config') # Ask about audio server selection if one is not already set - global_menu.enable('audio') + global_menu.enable('audio_config') # Ask for preferred kernel: global_menu.enable('kernels', mandatory=True) @@ -151,20 +150,11 @@ def perform_installation(mountpoint: Path): if users := archinstall.arguments.get('!users', None): installation.create_users(users) - if audio := archinstall.arguments.get('audio', None): - info(f'Installing audio server: {audio}') - if audio == 'pipewire': - PipewireProfile().install(installation) - elif audio == 'pulseaudio': - installation.add_additional_packages("pulseaudio") - - if SysInfo.requires_sof_fw(): - installation.add_additional_packages('sof-firmware') - - if SysInfo.requires_alsa_fw(): - installation.add_additional_packages('alsa-firmware') + audio_config: Optional[models.AudioConfiguration] = archinstall.arguments.get('audio_config', None) + if audio_config: + audio_config.install_audio_config(installation) else: - info("No audio server will be installed.") + info("No audio server will be installed") if profile_config := archinstall.arguments.get('profile_config', None): profile.profile_handler.install_profile_config(installation, profile_config) diff --git a/schema.json b/schema.json index b74588a1..5616ed41 100644 --- a/schema.json +++ b/schema.json @@ -12,14 +12,19 @@ "testing" ] }, - "audio": { + "audio_config": { "description": "Audio server to be installed", - "type": "string", - "enum": [ - "pipewire", - "pulseaudio", - "none" - ] + "type": "object", + "properties": { + "audio": { + "description": "Audio server to be installed", + "type": "string", + "enum": [ + "pipewire", + "pulseaudio" + ] + } + }, }, "bootloader": { "description": "Bootloader to be installed", -- cgit v1.2.3-70-g09d2 From b141609990fa4f7305443ee6ea6fe8796604c539 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 24 Sep 2023 19:47:38 +1000 Subject: Fix 1669 | Refactor display of sizes in tables (#2100) * Use sector as default display * Display tables in sector size * Refactor size * Update * Update * fix flake8 --------- Co-authored-by: Daniel Girtler --- archinstall/lib/disk/__init__.py | 1 + archinstall/lib/disk/device_model.py | 169 +++++++++++++++++++----------- archinstall/lib/disk/partitioning_menu.py | 39 ++++--- archinstall/lib/installer.py | 2 +- archinstall/lib/interactions/disk_conf.py | 55 ++++++---- examples/config-sample.json | 17 +-- examples/full_automated_installation.py | 15 +-- 7 files changed, 178 insertions(+), 120 deletions(-) (limited to 'examples/config-sample.json') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index cdc96373..24dafef5 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -14,6 +14,7 @@ from .device_model import ( PartitionTable, Unit, Size, + SectorSize, SubvolumeModification, DeviceGeometry, PartitionType, diff --git a/archinstall/lib/disk/device_model.py b/archinstall/lib/disk/device_model.py index 8bc41e0c..08861a63 100644 --- a/archinstall/lib/disk/device_model.py +++ b/archinstall/lib/disk/device_model.py @@ -93,7 +93,7 @@ class DiskLayoutConfiguration: status=ModificationStatus(partition['status']), fs_type=FilesystemType(partition['fs_type']), start=Size.parse_args(partition['start']), - length=Size.parse_args(partition['length']), + length=Size.parse_args(partition['size']), mount_options=partition['mount_options'], mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None, dev_path=Path(partition['dev_path']) if partition['dev_path'] else None, @@ -138,80 +138,89 @@ class Unit(Enum): sectors = 'sectors' # size in sector - Percent = '%' # size in percentile - @staticmethod def get_all_units() -> List[str]: return [u.name for u in Unit] + @staticmethod + def get_si_units() -> List[Unit]: + return [u for u in Unit if 'i' not in u.name and u.name != 'sectors'] + @dataclass -class Size: +class SectorSize: value: int unit: Unit - sector_size: Optional[Size] = None # only required when unit is sector - total_size: Optional[Size] = None # required when operating on percentages def __post_init__(self): - if self.unit == Unit.sectors and self.sector_size is None: - raise ValueError('Sector size is required when unit is sectors') - elif self.unit == Unit.Percent: - if self.value < 0 or self.value > 100: - raise ValueError('Percentage must be between 0 and 100') - elif self.total_size is None: - raise ValueError('Total size is required when unit is percentage') + match self.unit: + case Unit.sectors: + raise ValueError('Unit type sector not allowed for SectorSize') - @property - def _total_size(self) -> Size: + @staticmethod + def default() -> SectorSize: + return SectorSize(512, Unit.B) + + def json(self) -> Dict[str, Any]: + return { + 'value': self.value, + 'unit': self.unit.name, + } + + @classmethod + def parse_args(cls, arg: Dict[str, Any]) -> SectorSize: + return SectorSize( + arg['value'], + Unit[arg['unit']] + ) + + def normalize(self) -> int: """ - Save method to get the total size, mainly to satisfy mypy - This shouldn't happen as the Size object fails instantiation on missing total size + will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent and self.total_size is None: - raise ValueError('Percent unit size must specify a total size') - return self.total_size # type: ignore + return int(self.value * self.unit.value) # type: ignore + + +@dataclass +class Size: + value: int + unit: Unit + sector_size: SectorSize + + def __post_init__(self): + if not isinstance(self.sector_size, SectorSize): + raise ValueError('sector size must be of type SectorSize') def json(self) -> Dict[str, Any]: return { 'value': self.value, 'unit': self.unit.name, - 'sector_size': self.sector_size.json() if self.sector_size else None, - 'total_size': self._total_size.json() if self._total_size else None + 'sector_size': self.sector_size.json() if self.sector_size else None } @classmethod def parse_args(cls, size_arg: Dict[str, Any]) -> Size: sector_size = size_arg['sector_size'] - total_size = size_arg['total_size'] return Size( size_arg['value'], Unit[size_arg['unit']], - Size.parse_args(sector_size) if sector_size else None, - Size.parse_args(total_size) if total_size else None + SectorSize.parse_args(sector_size), ) def convert( self, target_unit: Unit, - sector_size: Optional[Size] = None, - total_size: Optional[Size] = None + sector_size: Optional[SectorSize] = None ) -> Size: if target_unit == Unit.sectors and sector_size is None: raise ValueError('If target has unit sector, a sector size must be provided') - # not sure why we would ever wanna convert to percentages - if target_unit == Unit.Percent and total_size is None: - raise ValueError('Missing parameter total size to be able to convert to percentage') - if self.unit == target_unit: return self - elif self.unit == Unit.Percent: - amount = int(self._total_size._normalize() * (self.value / 100)) - return Size(amount, Unit.B) elif self.unit == Unit.sectors: norm = self._normalize() - return Size(norm, Unit.B).convert(target_unit, sector_size) + return Size(norm, Unit.B, self.sector_size).convert(target_unit, sector_size) else: if target_unit == Unit.sectors and sector_size is not None: norm = self._normalize() @@ -219,7 +228,7 @@ class Size: return Size(sectors, Unit.sectors, sector_size) else: value = int(self._normalize() / target_unit.value) # type: ignore - return Size(value, target_unit) + return Size(value, target_unit, self.sector_size) def as_text(self) -> str: return self.format_size( @@ -230,31 +239,45 @@ class Size: def format_size( self, target_unit: Unit, - sector_size: Optional[Size] = None, + sector_size: Optional[SectorSize] = None, include_unit: bool = True ) -> str: - if self.unit == Unit.Percent: - return f'{self.value}%' - else: - target_size = self.convert(target_unit, sector_size) - if include_unit: - return f'{target_size.value} {target_unit.name}' - return f'{target_size.value}' + target_size = self.convert(target_unit, sector_size) + + if include_unit: + return f'{target_size.value} {target_unit.name}' + return f'{target_size.value}' + + def format_highest(self, include_unit: bool = True) -> str: + si_units = Unit.get_si_units() + all_si_values = [self.convert(si) for si in si_units] + filtered = filter(lambda x: x.value >= 1, all_si_values) + + # we have to get the max by the unit value as we're interested + # in getting the value in the highest possible unit without floats + si_value = max(filtered, key=lambda x: x.unit.value) + + if include_unit: + return f'{si_value.value} {si_value.unit.name}' + return f'{si_value.value}' def _normalize(self) -> int: """ will normalize the value of the unit to Byte """ - if self.unit == Unit.Percent: - return self.convert(Unit.B).value - elif self.unit == Unit.sectors and self.sector_size is not None: - return self.value * self.sector_size._normalize() + if self.unit == Unit.sectors and self.sector_size is not None: + return self.value * self.sector_size.normalize() return int(self.value * self.unit.value) # type: ignore def __sub__(self, other: Size) -> Size: src_norm = self._normalize() dest_norm = other._normalize() - return Size(abs(src_norm - dest_norm), Unit.B) + return Size(abs(src_norm - dest_norm), Unit.B, self.sector_size) + + def __add__(self, other: Size) -> Size: + src_norm = self._normalize() + dest_norm = other._normalize() + return Size(abs(src_norm + dest_norm), Unit.B, self.sector_size) def __lt__(self, other): return self._normalize() < other._normalize() @@ -296,14 +319,22 @@ class _PartitionInfo: mountpoints: List[Path] btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list) + @property + def sector_size(self) -> SectorSize: + sector_size = self.partition.geometry.device.sectorSize + return SectorSize(sector_size, Unit.B) + def table_data(self) -> Dict[str, Any]: + end = self.start + self.length + part_info = { 'Name': self.name, 'Type': self.type.value, 'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')), 'Path': str(self.path), - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'Flags': ', '.join([f.name for f in self.flags]) } @@ -327,10 +358,14 @@ class _PartitionInfo: start = Size( partition.geometry.start, Unit.sectors, - Size(partition.disk.device.sectorSize, Unit.B) + SectorSize(partition.disk.device.sectorSize, Unit.B) ) - length = Size(int(partition.getLength(unit='B')), Unit.B) + length = Size( + int(partition.getLength(unit='B')), + Unit.B, + SectorSize(partition.disk.device.sectorSize, Unit.B) + ) return _PartitionInfo( partition=partition, @@ -355,7 +390,7 @@ class _DeviceInfo: type: str total_size: Size free_space_regions: List[DeviceGeometry] - sector_size: Size + sector_size: SectorSize read_only: bool dirty: bool @@ -365,7 +400,7 @@ class _DeviceInfo: 'Model': self.model, 'Path': str(self.path), 'Type': self.type, - 'Size': self.total_size.format_size(Unit.MiB), + 'Size': self.total_size.format_highest(), 'Free space': int(total_free_space), 'Sector size': self.sector_size.value, 'Read only': self.read_only @@ -379,15 +414,17 @@ class _DeviceInfo: else: device_type = parted.devices[device.type] - sector_size = Size(device.sectorSize, Unit.B) + sector_size = SectorSize(device.sectorSize, Unit.B) free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()] + sector_size = SectorSize(device.sectorSize, Unit.B) + return _DeviceInfo( model=device.model.strip(), path=Path(device.path), type=device_type, sector_size=sector_size, - total_size=Size(int(device.getLength(unit='B')), Unit.B), + total_size=Size(int(device.getLength(unit='B')), Unit.B, sector_size), free_space_regions=free_space, read_only=device.readOnly, dirty=device.dirty @@ -470,7 +507,7 @@ class SubvolumeModification: class DeviceGeometry: - def __init__(self, geometry: Geometry, sector_size: Size): + def __init__(self, geometry: Geometry, sector_size: SectorSize): self._geometry = geometry self._sector_size = sector_size @@ -498,7 +535,7 @@ class DeviceGeometry: 'Sector size': self._sector_size.value, 'Start (sector/B)': start_str, 'End (sector/B)': end_str, - 'Length (sectors/B)': length_str + 'Size (sectors/B)': length_str } @@ -751,7 +788,7 @@ class PartitionModification: 'status': self.status.value, 'type': self.type.value, 'start': self.start.json(), - 'length': self.length.json(), + 'size': self.length.json(), 'fs_type': self.fs_type.value if self.fs_type else '', 'mountpoint': str(self.mountpoint) if self.mountpoint else None, 'mount_options': self.mount_options, @@ -764,12 +801,15 @@ class PartitionModification: """ Called for displaying data in table format """ + end = self.start + self.length + part_mod = { 'Status': self.status.value, 'Device': str(self.dev_path) if self.dev_path else '', 'Type': self.type.value, - 'Start': self.start.format_size(Unit.MiB), - 'Length': self.length.format_size(Unit.MiB), + 'Start': self.start.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'End': end.format_size(Unit.sectors, self.start.sector_size, include_unit=False), + 'Size': self.length.format_highest(), 'FS type': self.fs_type.value if self.fs_type else 'Unknown', 'Mountpoint': self.mountpoint if self.mountpoint else '', 'Mount options': ', '.join(self.mount_options), @@ -938,7 +978,7 @@ class LsblkInfo: name: str = '' path: Path = Path() pkname: str = '' - size: Size = field(default_factory=lambda: Size(0, Unit.B)) + size: Size = field(default_factory=lambda: Size(0, Unit.B, SectorSize.default())) log_sec: int = 0 pttype: str = '' ptuuid: str = '' @@ -1017,7 +1057,8 @@ class LsblkInfo: if isinstance(getattr(lsblk_info, data_field), Path): val = Path(blockdevice[lsblk_field]) elif isinstance(getattr(lsblk_info, data_field), Size): - val = Size(blockdevice[lsblk_field], Unit.B) + sector_size = SectorSize(blockdevice['log-sec'], Unit.B) + val = Size(blockdevice[lsblk_field], Unit.B, sector_size) else: val = blockdevice[lsblk_field] diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 549c7f34..c5263b82 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \ - ModificationStatus, DeviceGeometry + ModificationStatus, DeviceGeometry, SectorSize from ..menu import Menu, ListManager, MenuSelection, TextInput from ..output import FormattedOutput, warn from .subvolume_menu import SubvolumeMenu @@ -194,42 +194,47 @@ class PartitioningList(ListManager): def _validate_value( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, - value: str + text: str, + start: Optional[Size] ) -> Optional[Size]: - match = re.match(r'([0-9]+)([a-zA-Z|%]*)', value, re.I) + match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I) if match: - value, unit = match.groups() + str_value, unit = match.groups() - if unit == '%': - unit = Unit.Percent.name + if unit == '%' and start: + available = total_size - start + value = int(available.value * (int(str_value) / 100)) + unit = available.unit.name + else: + value = int(str_value) if unit and unit not in Unit.get_all_units(): return None unit = Unit[unit] if unit else Unit.sectors - return Size(int(value), unit, sector_size, total_size) + return Size(value, unit, sector_size) return None def _enter_size( self, - sector_size: Size, + sector_size: SectorSize, total_size: Size, prompt: str, - default: Size + default: Size, + start: Optional[Size], ) -> Size: while True: value = TextInput(prompt).run().strip() - size: Optional[Size] = None if not value: size = default else: - size = self._validate_value(sector_size, total_size, value) + size = self._validate_value(sector_size, total_size, value, start) if size: return size @@ -247,7 +252,7 @@ class PartitioningList(ListManager): total_bytes = device_info.total_size.format_size(Unit.B) prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n' - prompt += str(_('All entered values can be suffixed with a unit: B, KB, KiB, MB, MiB...')) + '\n' + prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n' prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n' print(prompt) @@ -260,13 +265,14 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, start_prompt, - default_start + default_start, + None ) if start_size.value == largest_free_area.start: end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size) else: - end_size = Size(100, Unit.Percent, total_size=device_info.total_size) + end_size = device_info.total_size # prompt until valid end sector was entered end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text()) @@ -274,7 +280,8 @@ class PartitioningList(ListManager): device_info.sector_size, device_info.total_size, end_prompt, - end_size + end_size, + start_size ) return start_size, end_size diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 05eb5867..a238bb8f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -163,7 +163,7 @@ class Installer: lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount) if len(lsblk_info) > 0: - if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB): + if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()): raise DiskError( f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. ' f'Please resize it to at least 200MiB and re-run the installation.' diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index 78e4cff4..8542ab75 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -170,13 +170,13 @@ def select_disk_config( return None -def _boot_partition() -> disk.PartitionModification: +def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification: if SysInfo.has_uefi(): - start = disk.Size(1, disk.Unit.MiB) - size = disk.Size(512, disk.Unit.MiB) + start = disk.Size(1, disk.Unit.MiB, sector_size) + size = disk.Size(512, disk.Unit.MiB, sector_size) else: - start = disk.Size(3, disk.Unit.MiB) - size = disk.Size(203, disk.Unit.MiB) + start = disk.Size(3, disk.Unit.MiB, sector_size) + size = disk.Size(203, disk.Unit.MiB, sector_size) # boot partition return disk.PartitionModification( @@ -215,8 +215,9 @@ def suggest_single_disk_layout( if not filesystem_type: filesystem_type = select_main_filesystem_format(advanced_options) - min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) - root_partition_size = disk.Size(20, disk.Unit.GiB) + sector_size = device.device_info.sector_size + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size) + root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size) using_subvolumes = False using_home_partition = False compression = False @@ -244,7 +245,7 @@ def suggest_single_disk_layout( # Also re-align the start to 1MiB since we don't need the first sectors # like we do in MBR layouts where the boot loader is installed traditionally. - boot_partition = _boot_partition() + boot_partition = _boot_partition(sector_size) device_modification.add_partition(boot_partition) if not using_subvolumes: @@ -259,11 +260,11 @@ def suggest_single_disk_layout( using_home_partition = False # root partition - start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB) + start = disk.Size(513, disk.Unit.MiB, sector_size) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB, sector_size) # Set a size for / (/root) if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: - length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + length = device.device_info.total_size - start else: length = min(device.device_info.total_size, root_partition_size) @@ -294,11 +295,14 @@ def suggest_single_disk_layout( # If we don't want to use subvolumes, # But we want to be able to re-use data between re-installs.. # A second partition for /home would be nice if we have the space for it + start = root_partition.length + length = device.device_info.total_size - root_partition.length + home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), fs_type=filesystem_type, mount_options=['compress=zstd'] if compression else [] @@ -319,9 +323,9 @@ def suggest_multi_disk_layout( # Not really a rock solid foundation of information to stand on, but it's a start: # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - min_home_partition_size = disk.Size(40, disk.Unit.GiB) + min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default()) # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default()) compression = False if not filesystem_type: @@ -362,28 +366,41 @@ def suggest_multi_disk_layout( root_device_modification = disk.DeviceModification(root_device, wipe=True) home_device_modification = disk.DeviceModification(home_device, wipe=True) + root_device_sector_size = root_device_modification.device.device_info.sector_size + home_device_sector_size = home_device_modification.device.device_info.sector_size + # add boot partition to the root device - boot_partition = _boot_partition() + boot_partition = _boot_partition(root_device_sector_size) root_device_modification.add_partition(boot_partition) + if SysInfo.has_uefi(): + root_start = disk.Size(513, disk.Unit.MiB, root_device_sector_size) + else: + root_start = disk.Size(206, disk.Unit.MiB, root_device_sector_size) + + root_length = root_device.device_info.total_size - root_start + # add root partition to the root device root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + start=root_start, + length=root_length, mountpoint=Path('/'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type ) root_device_modification.add_partition(root_partition) + start = disk.Size(1, disk.Unit.MiB, home_device_sector_size) + length = home_device.device_info.total_size - start + # add home partition to home device home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + start=start, + length=length, mountpoint=Path('/home'), mount_options=['compress=zstd'] if compression else [], fs_type=filesystem_type, diff --git a/examples/config-sample.json b/examples/config-sample.json index ed1cc38e..d43f7ea6 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -17,9 +17,8 @@ "Boot" ], "fs_type": "fat32", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 512 }, @@ -28,7 +27,6 @@ "obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 1 }, @@ -39,9 +37,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, @@ -50,7 +47,6 @@ "obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208", "start": { "sector_size": null, - "total_size": null, "unit": "MiB", "value": 513 }, @@ -61,14 +57,8 @@ "btrfs": [], "flags": [], "fs_type": "ext4", - "length": { + "size": { "sector_size": null, - "total_size": { - "sector_size": null, - "total_size": null, - "unit": "B", - "value": 250148290560 - }, "unit": "Percent", "value": 100 }, @@ -77,7 +67,6 @@ "obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f", "start": { "sector_size": null, - "total_size": null, "unit": "GiB", "value": 20 }, diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index 79e85348..d25575d4 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -23,8 +23,8 @@ device_modification = disk.DeviceModification(device, wipe=True) boot_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(1, disk.Unit.MiB), - length=disk.Size(512, disk.Unit.MiB), + start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size), mountpoint=Path('/boot'), fs_type=disk.FilesystemType.Fat32, flags=[disk.PartitionFlag.Boot] @@ -35,20 +35,23 @@ device_modification.add_partition(boot_partition) root_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=disk.Size(513, disk.Unit.MiB), - length=disk.Size(20, disk.Unit.GiB), + start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size), + length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size), mountpoint=None, fs_type=fs_type, mount_options=[], ) device_modification.add_partition(root_partition) +start_home = root_partition.length +length_home = device.device_info.total_size - start_home + # create a new home partition home_partition = disk.PartitionModification( status=disk.ModificationStatus.Create, type=disk.PartitionType.Primary, - start=root_partition.length, - length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + start=start_home, + length=length_home, mountpoint=Path('/home'), fs_type=fs_type, mount_options=[] -- cgit v1.2.3-70-g09d2 From 0ea6dbbd7677b94e863b2ab333431716886b5f84 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 19 Apr 2024 14:47:18 +0200 Subject: Rename "Kde" profile to the correct "KDE Plasma" / "Plasma" (#2421) * schema.json: Remove dead misspelled i3-gasp profile * schema.json: Rename KDE Plasma profile to the correct "Plasma" shorthand * Rename to KDE Plasma in user facing parts and keep the old "Kde" profile for now * Add back an accidental deleted character * Backwards compat v2 --- archinstall/default_profiles/desktop.py | 2 +- archinstall/default_profiles/desktops/kde.py | 28 ------------------------- archinstall/default_profiles/desktops/plasma.py | 27 ++++++++++++++++++++++++ archinstall/lib/models/network_configuration.py | 2 +- archinstall/lib/profile/profiles_handler.py | 5 +++++ examples/config-sample.json | 2 +- mypy-strict.ini | 4 ++-- schema.json | 5 ++--- 8 files changed, 39 insertions(+), 36 deletions(-) delete mode 100644 archinstall/default_profiles/desktops/kde.py create mode 100644 archinstall/default_profiles/desktops/plasma.py (limited to 'examples/config-sample.json') diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index 9d92f822..417d86d6 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -15,7 +15,7 @@ class DesktopProfile(Profile): super().__init__( 'Desktop', ProfileType.Desktop, - description=str(_('Provides a selection of desktop environments and tiling window managers, e.g. gnome, kde, sway')), + description=str(_('Provides a selection of desktop environments and tiling window managers, e.g. GNOME, KDE Plasma, Sway')), current_selection=current_selection, support_greeter=True ) diff --git a/archinstall/default_profiles/desktops/kde.py b/archinstall/default_profiles/desktops/kde.py deleted file mode 100644 index 62274d51..00000000 --- a/archinstall/default_profiles/desktops/kde.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Optional, Any, TYPE_CHECKING - -from archinstall.default_profiles.profile import ProfileType, GreeterType -from archinstall.default_profiles.xorg import XorgProfile - -if TYPE_CHECKING: - _: Any - - -class KdeProfile(XorgProfile): - def __init__(self): - super().__init__('Kde', ProfileType.DesktopEnv, description='') - - @property - def packages(self) -> List[str]: - return [ - "plasma-meta", - "konsole", - "kwrite", - "dolphin", - "ark", - "plasma-workspace", - "egl-wayland" - ] - - @property - def default_greeter_type(self) -> Optional[GreeterType]: - return GreeterType.Sddm diff --git a/archinstall/default_profiles/desktops/plasma.py b/archinstall/default_profiles/desktops/plasma.py new file mode 100644 index 00000000..bcc1ea1b --- /dev/null +++ b/archinstall/default_profiles/desktops/plasma.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Any, TYPE_CHECKING + +from archinstall.default_profiles.profile import ProfileType, GreeterType +from archinstall.default_profiles.xorg import XorgProfile + +if TYPE_CHECKING: + _: Any + +class PlasmaProfile(XorgProfile): + def __init__(self): + super().__init__('KDE Plasma', ProfileType.DesktopEnv, description='') + + @property + def packages(self) -> List[str]: + return [ + "plasma-meta", + "konsole", + "kwrite", + "dolphin", + "ark", + "plasma-workspace", + "egl-wayland" + ] + + @property + def default_greeter_type(self) -> Optional[GreeterType]: + return GreeterType.Sddm diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index b726bb73..dfd8b8cb 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -20,7 +20,7 @@ class NicType(Enum): case NicType.ISO: return str(_('Copy ISO network configuration to installation')) case NicType.NM: - return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')) + return str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE Plasma)')) case NicType.MANUAL: return str(_('Manual configuration')) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 12dcee3f..b9acb4fe 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -107,6 +107,11 @@ class ProfileHandler: if details: for detail in filter(None, details): + # [2024-04-19] TODO: Backwards compatibility after naming change: https://github.com/archlinux/archinstall/pull/2421 + # 'Kde' is deprecated, remove this block in a future version + if detail == 'Kde': + detail = 'KDE Plasma' + if sub_profile := self.get_profile_by_name(detail): valid_sub_profiles.append(sub_profile) else: diff --git a/examples/config-sample.json b/examples/config-sample.json index d43f7ea6..47a4e2e0 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -106,7 +106,7 @@ "greeter": "sddm", "profile": { "details": [ - "Kde" + "KDE Plasma" ], "main": "Desktop" } diff --git a/mypy-strict.ini b/mypy-strict.ini index be7ddf57..c670bbaa 100644 --- a/mypy-strict.ini +++ b/mypy-strict.ini @@ -66,10 +66,10 @@ exclude = (?x)( | ^archinstall/profiles/enlightenment\.py$ | ^archinstall/profiles/gnome\.py$ | ^archinstall/profiles/i3\.py$ - | ^archinstall/profiles/kde\.py$ | ^archinstall/profiles/lxqt\.py$ | ^archinstall/profiles/mate\.py$ | ^archinstall/profiles/minimal\.py$ + | ^archinstall/profiles/plasma\.py$ | ^archinstall/profiles/qtile\.py$ | ^archinstall/profiles/server\.py$ | ^archinstall/profiles/sway\.py$ @@ -86,10 +86,10 @@ exclude = (?x)( | ^profiles/enlightenment\.py$ | ^profiles/gnome\.py$ | ^profiles/i3\.py$ - | ^profiles/kde\.py$ | ^profiles/lxqt\.py$ | ^profiles/mate\.py$ | ^profiles/minimal\.py$ + | ^profiles/plasma\.py$ | ^profiles/qtile\.py$ | ^profiles/server\.py$ | ^profiles/sway\.py$ diff --git a/schema.json b/schema.json index 74ecc4d3..97cb42e1 100644 --- a/schema.json +++ b/schema.json @@ -129,7 +129,7 @@ ] }, "details": { - "description": "Specific profile to be installed based on the 'main' selection; these profiles are present in profiles_v2/, use the name of a profile to install it (case insensitive)", + "description": "Specific profile to be installed based on the 'main' selection; these profiles are present in default_profiles/, use the file name of a profile without the extension to install it (case insensitive)", "type": "string", "enum": [ "awesome", @@ -142,8 +142,7 @@ "enlightenment", "gnome", "i3-wm", - "i3-gasp", - "kde", + "plasma", "lxqt", "mate", "sway", -- cgit v1.2.3-70-g09d2