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/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 ++++++++++-------------- 7 files changed, 265 insertions(+), 319 deletions(-) delete mode 100644 archinstall/lib/interactions/network_conf.py create mode 100644 archinstall/lib/interactions/network_menu.py (limited to 'archinstall/lib') 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') -- cgit v1.2.3-70-g09d2