From 477b5b120e120766d789a691fce60cec843aff43 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 22 Apr 2022 21:24:12 +1000 Subject: Support for multiple network interfaces (#1052) * Support for multiple network interfaces * Fix mypy * Fix flake8 Co-authored-by: Daniel Girtler --- .github/workflows/mypy.yaml | 3 +- archinstall/__init__.py | 5 +- archinstall/lib/menu/global_menu.py | 15 +- archinstall/lib/menu/list_manager.py | 126 ++++++------ archinstall/lib/menu/selection_menu.py | 4 +- archinstall/lib/models/network_configuration.py | 220 ++++++++++++--------- .../lib/user_interaction/manage_users_conf.py | 33 ++-- archinstall/lib/user_interaction/network_conf.py | 161 +++++++++------ .../lib/user_interaction/subvolume_config.py | 33 ++-- examples/guided.py | 4 +- examples/swiss.py | 9 +- 11 files changed, 356 insertions(+), 257 deletions(-) diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index c13c0dd8..6fd0876f 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -13,5 +13,6 @@ jobs: - run: python --version - run: mypy --version # one day this will be enabled + # run: mypy --strict --module archinstall || exit 0 - name: run mypy - run: mypy --strict --module archinstall || exit 0 + run: mypy --follow-imports=skip archinstall/lib/menu/selection_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 32665efd..638ac08a 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -12,6 +12,7 @@ from .lib.installer import __packages__, Installer, accessibility_tools_in_use from .lib.locale_helpers import * from .lib.luks import * from .lib.mirrors import * +from .lib.models.network_configuration import NetworkConfigurationHandler from .lib.networking import * from .lib.output import * from .lib.models.dataclasses import ( @@ -207,7 +208,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: - arguments['nic'] = NetworkConfiguration.parse_arguments(arguments.get('nic')) + handler = NetworkConfigurationHandler() + handler.parse_arguments(arguments.get('nic')) + arguments['nic'] = handler.configuration def post_process_arguments(arguments): storage['arguments'] = arguments diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index 50d3180b..fba0ce29 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from ..menu import Menu from ..menu.selection_menu import Selector, GeneralMenu from ..general import SysCommand, secret from ..hardware import has_uefi +from ..models import NetworkConfiguration from ..storage import storage from ..output import log from ..profiles import is_desktop_profile @@ -139,7 +140,7 @@ class GlobalMenu(GeneralMenu): Selector( _('Configure network'), ask_to_configure_network, - display_func=lambda x: x if x else _('Not configured, unavailable unless setup manually'), + display_func=lambda x: self._prev_network_configuration(x), default={}) self._menu_options['timezone'] = \ Selector( @@ -192,6 +193,16 @@ class GlobalMenu(GeneralMenu): return _('Install ({} config(s) missing)').format(missing) return 'Install' + def _prev_network_configuration(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): + ifaces = [x.iface for x in cur_value] + return f'Configured ifaces: {ifaces}' + else: + return str(cur_value) + def _prev_install_missing_config(self) -> Optional[str]: if missing := self._missing_configs(): text = str(_('Missing configurations:\n')) diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 5b4f720c..5da34e06 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -89,10 +89,21 @@ from .text_input import TextInput from .menu import Menu from os import system from copy import copy -from typing import Union +from typing import Union, Any, List, TYPE_CHECKING + +if TYPE_CHECKING: + _: Any + class ListManager: - def __init__(self,prompt :str, base_list :Union[list,dict] ,base_actions :list = None,null_action :str = None, default_action :Union[str,list] = None, header :Union[str,list] = None): + def __init__( + self, + prompt :str, + base_list :Union[list,dict] , + base_actions :list = None, + null_action :str = None, + default_action :Union[str,list] = None, + header :Union[str,list] = None): """ param :prompt Text which will appear at the header type param: string | DeferredTranslation @@ -115,16 +126,16 @@ class ListManager: """ explainer = str(_('\n Choose an object from the list, and select one of the available actions for it to execute')) - self.prompt = prompt + explainer if prompt else explainer + self._prompt = prompt + explainer if prompt else explainer - self.null_action = str(null_action) if null_action else None + self._null_action = str(null_action) if null_action else None if not default_action: - self.default_action = [self.null_action,] + self._default_action = [self._null_action] elif isinstance(default_action,(list,tuple)): - self.default_action = default_action + self._default_action = default_action else: - self.default_action = [str(default_action),] + self._default_action = [str(default_action),] self.header = header if header else None self.cancel_action = str(_('Cancel')) @@ -133,24 +144,23 @@ class ListManager: self.bottom_list = [self.confirm_action,self.cancel_action] self.bottom_item = [self.cancel_action] self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] - self.base_data = base_list - self.data = copy(base_list) # as refs, changes are immediate + self._data = copy(base_list) # as refs, changes are immediate # default values for the null case self.target = None - self.action = self.null_action - if len(self.data) == 0 and self.null_action: - self.exec_action() + self.action = self._null_action + if len(self._data) == 0 and self._null_action: + self.exec_action(self._data) def run(self): while True: - self.data_formatted = self.reformat() - options = self.data_formatted + [self.separator] - if self.default_action: - options += self.default_action + self._data_formatted = self.reformat(self._data) + options = self._data_formatted + [self.separator] + if self._default_action: + options += self._default_action options += self.bottom_list system('clear') - target = Menu(self.prompt, + target = Menu(self._prompt, options, sort=False, clear_screen=False, @@ -162,53 +172,53 @@ class ListManager: break if target and target == self.separator: continue - if target and target in self.default_action: + if target and target in self._default_action: self.action = target target = None self.target = None - self.exec_action() + self.exec_action(self._data) continue - if isinstance(self.data,dict): - key = list(self.data.keys())[self.data_formatted.index(target)] - self.target = {key: self.data[key]} + if isinstance(self._data,dict): + key = list(self._data.keys())[self._data_formatted.index(target)] + self.target = {key: self._data[key]} else: - self.target = self.data[self.data_formatted.index(target)] + self.target = self._data[self._data_formatted.index(target)] # Possible enhacement. If run_actions returns false a message line indicating the failure self.run_actions(target) if not target or target == self.cancel_action: # TODO dubious return self.base_data # return the original list else: - return self.data + return self._data def run_actions(self,prompt_data=None): options = self.action_list() + self.bottom_item prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) - self.action = Menu(prompt, - options, - sort=False, - skip=False, - clear_screen=False, - clear_menu_on_exit=False, - preset_values=self.bottom_item, - show_search_hint=False).run() - if self.action == self.cancel_action: + self.action = Menu( + prompt, + options, + sort=False, + clear_screen=False, + clear_menu_on_exit=False, + preset_values=self.bottom_item, + show_search_hint=False).run() + if not self.action or self.action == self.cancel_action: return False else: - return self.exec_action() + return self.exec_action(self._data) """ The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case """ - def reformat(self): + def reformat(self, data: Any) -> List[Any]: """ method to get the data in a format suitable to be shown - It is executed once for run loop and processes the whole self.data structure + It is executed once for run loop and processes the whole self._data structure """ - if isinstance(self.data,dict): - return list(map(lambda x:f"{x} : {self.data[x]}",self.data)) + if isinstance(data,dict): + return list(map(lambda x:f"{x} : {data[x]}",data)) else: - return list(map(lambda x:str(x),self.data)) + return list(map(lambda x:str(x),data)) def action_list(self): """ @@ -217,32 +227,32 @@ class ListManager: """ return self.base_actions - def exec_action(self): + def exec_action(self, data: Any): """ what's executed one an item (self.target) and one action (self.action) is selected. Should be overwritten by the user - The result is expected to update self.data in this routine, else it is ignored + The result is expected to update self._data in this routine, else it is ignored The basic code is useful for simple lists and dictionaries (key:value pairs, both strings) """ # TODO guarantee unicity - if isinstance(self.data,list): + if isinstance(self._data,list): if self.action == str(_('Add')): self.target = TextInput(_('Add :'),None).run() - self.data.append(self.target) + self._data.append(self.target) if self.action == str(_('Copy')): while True: target = TextInput(_('Copy to :'),self.target).run() if target != self.target: - self.data.append(self.target) + self._data.append(self.target) break elif self.action == str(_('Edit')): tgt = self.target - idx = self.data.index(self.target) + idx = self._data.index(self.target) result = TextInput(_('Edite :'),tgt).run() - self.data[idx] = result + self._data[idx] = result elif self.action == str(_('Delete')): - del self.data[self.data.index(self.target)] - elif isinstance(self.data,dict): + del self._data[self._data.index(self.target)] + elif isinstance(self._data,dict): # allows overwrites if self.target: origkey,origval = list(self.target.items())[0] @@ -252,27 +262,15 @@ class ListManager: if self.action == str(_('Add')): key = TextInput(_('Key :'),None).run() value = TextInput(_('Value :'),None).run() - self.data[key] = value + self._data[key] = value if self.action == str(_('Copy')): while True: key = TextInput(_('Copy to new key:'),origkey).run() if key != origkey: - self.data[key] = origval + self._data[key] = origval break elif self.action == str(_('Edit')): value = TextInput(_(f'Edit {origkey} :'),origval).run() - self.data[origkey] = value + self._data[origkey] = value elif self.action == str(_('Delete')): - del self.data[origkey] - - return True - - -if __name__ == "__main__": - # opciones = ['uno','dos','tres','cuatro'] - # opciones = archinstall.list_mirrors() - opciones = {'uno':1,'dos':2,'tres':3,'cuatro':4} - acciones = ['editar','borrar','aƱadir'] - cabecera = ["En Jaen Donde Resido","Vive don Lope de Sosa"] - opciones = ListManager('Vamos alla',opciones,None,_('Add'),default_action=acciones,header=cabecera).run() - print(opciones) + del self._data[origkey] diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py index cbcb2583..ca48fda2 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -1,7 +1,7 @@ from __future__ import annotations -import sys -import logging +import logging +import sys from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu diff --git a/archinstall/lib/models/network_configuration.py b/archinstall/lib/models/network_configuration.py index eb4c25ee..16136177 100644 --- a/archinstall/lib/models/network_configuration.py +++ b/archinstall/lib/models/network_configuration.py @@ -2,7 +2,12 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Union, Any, TYPE_CHECKING + +from ..output import log + +if TYPE_CHECKING: + _: Any class NicType(str, Enum): @@ -14,11 +19,11 @@ class NicType(str, Enum): @dataclass class NetworkConfiguration: type: NicType - iface: str = None - ip: str = None + iface: Optional[str] = None + ip: Optional[str] = None dhcp: bool = True - gateway: str = None - dns: List[str] = None + gateway: Optional[str] = None + dns: Union[None, List[str]] = None def __str__(self): if self.is_iso(): @@ -37,63 +42,6 @@ class NetworkConfiguration: def json(self): return self.__dict__ - @classmethod - def parse_arguments(cls, config: Union[str,Dict[str, str]]) -> Optional["NetworkConfiguration"]: - from ... import log - - nic_type = config.get('type', None) - - if not nic_type: - # old style definitions - if isinstance(config,str): # is a ISO network - return NetworkConfiguration(NicType.ISO) - elif config.get('NetworkManager'): # is a network manager configuration - return NetworkConfiguration(NicType.NM) - elif 'ip' in config: - return NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - ip=config.get('ip'), - gateway=config.get('gateway', ''), - dns=config.get('dns', []), - dhcp=False - ) - elif 'nic' in config: - return NetworkConfiguration( - NicType.MANUAL, - iface=config.get('nic', ''), - dhcp=True - ) - else: # not recognized - return None - - try: - type = NicType(nic_type) - except ValueError: - options = [e.value for e in NicType] - log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') - exit(1) - - if type == NicType.MANUAL: - if config.get('dhcp', False) or not any([config.get(v) for v in ['ip', 'gateway', 'dns']]): - return NetworkConfiguration(type, iface=config.get('iface', '')) - - ip = config.get('ip', '') - if not ip: - log('Manual nic configuration with no auto DHCP requires an IP address', fg='red') - exit(1) - - return NetworkConfiguration( - type, - iface=config.get('iface', ''), - ip=ip, - gateway=config.get('gateway', ''), - dns=config.get('dns', []), - dhcp=False - ) - else: - return NetworkConfiguration(type) - def is_iso(self) -> bool: return self.type == NicType.ISO @@ -103,37 +51,123 @@ class NetworkConfiguration: def is_manual(self) -> bool: return self.type == NicType.MANUAL - def config_installer(self, installation: 'Installer'): - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - if self.is_iso(): - installation.copy_iso_network_config(enable_services=True) # Sources the ISO network configuration to the install medium. - elif self.is_network_manager(): - installation.add_additional_packages("networkmanager") - installation.enable_service('NetworkManager.service') - # Otherwise, if a interface was selected, configure that interface - elif self.is_manual(): - installation.configure_nic(self) + +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(self, installation: Any): + 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') - - def get(self, key :str, default_value :Any = None) -> Any: - result = self.__getitem__(key) - if result is None: - return default_value - else: - return result - - def __getitem__(self, key :str) -> Any: - if key == 'type': - return self.type - elif key == 'iface': - return self.iface - elif key == 'gateway': - return self.gateway - elif key == 'dns': - return self.dns - elif key == 'dhcp': - return self.dhcp else: - raise KeyError(f"key {key} not available at NetworkConfiguration") + # If user selected to copy the current ISO network configuration + # Perform a copy of the config + if self._configuration.is_iso(): + installation.copy_iso_network_config( + enable_services=True) # Sources the ISO network configuration to the install medium. + elif self._configuration.is_network_manager(): + installation.add_additional_packages("networkmanager") + installation.enable_service('NetworkManager.service') + + def _backwards_compability_config(self, config: Union[str,Dict[str, str]]) -> Union[List[NetworkConfiguration], NetworkConfiguration, None]: + def get(config: Dict[str, str], key: str) -> List[str]: + if (value := config.get(key, None)) is not None: + return [value] + return [] + + if isinstance(config, str): # is a ISO network + return NetworkConfiguration(NicType.ISO) + elif config.get('NetworkManager'): # is a network manager configuration + return NetworkConfiguration(NicType.NM) + elif 'ip' in config: + return [NetworkConfiguration( + NicType.MANUAL, + iface=config.get('nic', ''), + ip=config.get('ip'), + gateway=config.get('gateway', ''), + dns=get(config, 'dns'), + dhcp=False + )] + elif 'nic' in config: + return [NetworkConfiguration( + NicType.MANUAL, + iface=config.get('nic', ''), + dhcp=True + )] + else: # not recognized + return None + + def _parse_manual_config(self, config: Dict[str, Any]) -> Union[None, List[NetworkConfiguration]]: + manual_configs: List = config.get('config', []) + + if not manual_configs: + return None + + if not isinstance(manual_configs, list): + log(_('Manual configuration setting must be a list')) + exit(1) + + configurations = [] + + for manual_config in manual_configs: + iface = manual_config.get('iface', None) + + if iface is None: + log(_('No iface specified for manual configuration')) + exit(1) + + 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 = config.get('ip', '') + if not ip: + log(_('Manual nic configuration with no auto DHCP requires an IP address'), fg='red') + exit(1) + + configurations.append( + NetworkConfiguration( + NicType.MANUAL, + iface=iface, + ip=ip, + gateway=config.get('gateway', ''), + dns=config.get('dns', []), + dhcp=False + ) + ) + + return configurations + + def parse_arguments(self, config: Any): + nic_type = config.get('type', None) + + if not nic_type: + # old style definitions + network_config = self._backwards_compability_config(config) + if network_config: + return network_config + return None + + try: + type_ = NicType(nic_type) + except ValueError: + options = [e.value for e in NicType] + log(_('Unknown nic type: {}. Possible values are {}').format(nic_type, options), fg='red') + exit(1) + + if type_ != NicType.MANUAL: + self._configuration = NetworkConfiguration(type_) + else: # manual configuration settings + self._configuration = self._parse_manual_config(config) diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index 664327d6..a6ff3111 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import re -from typing import Any, Dict, TYPE_CHECKING +from typing import Any, Dict, TYPE_CHECKING, List from ..menu import Menu from ..menu.list_manager import ListManager @@ -34,25 +34,22 @@ class UserList(ListManager): str(_('Promote/Demote user')), str(_('Delete User')) ] - self.default_action = self.actions[0] - super().__init__(prompt, lusers, self.actions, self.default_action) + super().__init__(prompt, lusers, self.actions, self.actions[0]) - def reformat(self): - - def format_element(elem): + def reformat(self, data: Any) -> List[Any]: + def format_element(elem :str): # secret gives away the length of the password - if self.data[elem].get('!password'): + if data[elem].get('!password'): pwd = '*' * 16 - # pwd = archinstall.secret(self.data[elem]['!password']) else: pwd = '' - if self.data[elem].get('sudoer'): - super = 'Superuser' + if data[elem].get('sudoer'): + super_user = 'Superuser' else: - super = ' ' - return f"{elem:16}: password {pwd:16} {super}" + super_user = ' ' + return f"{elem:16}: password {pwd:16} {super_user}" - return list(map(lambda x: format_element(x), self.data)) + return list(map(lambda x: format_element(x), data)) def action_list(self): if self.target: @@ -71,7 +68,7 @@ class UserList(ListManager): else: return self.actions - def exec_action(self): + def exec_action(self, data: Any): if self.target: active_user = list(self.target.keys())[0] else: @@ -80,14 +77,14 @@ class UserList(ListManager): if self.action == self.actions[0]: # add new_user = self.add_user() # no unicity check, if exists will be replaced - self.data.update(new_user) + data.update(new_user) elif self.action == self.actions[1]: # change password - self.data[active_user]['!password'] = get_password( + data[active_user]['!password'] = get_password( prompt=str(_('Password for user "{}": ').format(active_user))) elif self.action == self.actions[2]: # promote/demote - self.data[active_user]['sudoer'] = not self.data[active_user]['sudoer'] + data[active_user]['sudoer'] = not data[active_user]['sudoer'] elif self.action == self.actions[3]: # delete - del self.data[active_user] + del data[active_user] def _check_for_correct_username(self, username: str) -> bool: if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index f90a2af8..80c9106b 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -2,8 +2,7 @@ from __future__ import annotations import ipaddress import logging -from copy import copy -from typing import Any, Optional, Dict, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING, List, Union from ..menu.text_input import TextInput from ..models.network_configuration import NetworkConfiguration, NicType @@ -11,69 +10,77 @@ from ..models.network_configuration import NetworkConfiguration, NicType from ..networking import list_interfaces from ..menu import Menu from ..output import log +from ..menu.list_manager import ListManager if TYPE_CHECKING: _: Any -def ask_to_configure_network(preset: Dict[str, Any] = {}) -> Optional[NetworkConfiguration]: +class ManualNetworkConfig(ListManager): """ - Configure the network on the newly installed system + subclass of ListManager for the managing of network configuration accounts """ - interfaces = { - '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)')), - **list_interfaces() - } - # for this routine it's easier to set the cursor position rather than a preset value - cursor_idx = None - if preset: - if preset['type'] == 'iso_config': - cursor_idx = 0 - elif preset['type'] == 'network_manager': - cursor_idx = 1 + + def __init__(self, prompt: str, ifaces: Union[None, NetworkConfiguration, List[NetworkConfiguration]]): + """ + param: prompt + type: str + param: ifaces already defined previously + type: Dict + """ + + if ifaces is not None and isinstance(ifaces, list): + display_values = {iface.iface: iface for iface in ifaces} else: - try: - # let's hope order in dictionaries stay - cursor_idx = list(interfaces.values()).index(preset.get('type')) - except ValueError: - pass + display_values = {} - nic = Menu(_('Select one network interface to configure'), interfaces.values(), cursor_index=cursor_idx, - sort=False).run() + self._action_add = str(_('Add interface')) + self._action_edit = str(_('Edit interface')) + self._action_delete = str(_('Delete interface')) - if not nic: - return None + self._iface_actions = [self._action_edit, self._action_delete] - if nic == interfaces['none']: - return None - elif nic == interfaces['iso_config']: - return NetworkConfiguration(NicType.ISO) - elif nic == interfaces['network_manager']: - return NetworkConfiguration(NicType.NM) - else: - # Current workaround: - # For selecting modes without entering text within brackets, - # printing out this part separate from options, passed in - # `generic_select` - # we only keep data if it is the same nic as before - if preset.get('type') != nic: - preset_d = {'type': nic, 'dhcp': True, 'ip': None, 'gateway': None, 'dns': []} - else: - preset_d = copy(preset) + super().__init__(prompt, display_values, self._iface_actions, self._action_add) + def run_manual(self) -> List[NetworkConfiguration]: + ifaces = super().run() + if ifaces is not None: + return list(ifaces.values()) + return [] + + def exec_action(self, data: Any): + if self.action == self._action_add: + iface_name = self._select_iface(data.keys()) + if iface_name: + iface = NetworkConfiguration(NicType.MANUAL, iface=iface_name) + data[iface_name] = self._edit_iface(iface) + elif self.target: + iface_name = list(self.target.keys())[0] + iface = data[iface_name] + + if self.action == self._action_edit: + data[iface_name] = self._edit_iface(iface) + elif self.action == self._action_delete: + del data[iface_name] + + def _select_iface(self, existing_ifaces: List[str]) -> Optional[str]: + all_ifaces = list_interfaces().values() + available = set(all_ifaces) - set(existing_ifaces) + iface = Menu(str(_('Select interface to add')), list(available), skip=True).run() + return iface + + def _edit_iface(self, edit_iface :NetworkConfiguration): + iface_name = edit_iface.iface modes = ['DHCP (auto detect)', 'IP (static)'] default_mode = 'DHCP (auto detect)' - cursor_idx = 0 if preset_d.get('dhcp', True) else 1 - prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(nic, default_mode) - mode = Menu(prompt, modes, default_option=default_mode, cursor_index=cursor_idx).run() - # TODO preset values for ip and gateway + 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).run() + if mode == 'IP (static)': while 1: - prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(nic) - ip = TextInput(prompt, preset_d.get('ip')).run().strip() + 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) @@ -84,7 +91,7 @@ def ask_to_configure_network(preset: Dict[str, Any] = {}) -> Optional[NetworkCon # Implemented new check for correct gateway IP address while 1: gateway = TextInput(_('Enter your gateway (router) IP address or leave blank for none: '), - preset_d.get('gateway')).run().strip() + edit_iface.gateway).run().strip() try: if len(gateway) == 0: gateway = None @@ -94,18 +101,58 @@ def ask_to_configure_network(preset: Dict[str, Any] = {}) -> Optional[NetworkCon except ValueError: log("You need to enter a valid gateway (router) IP address.", level=logging.WARNING, fg='red') - dns = None - if preset_d.get('dns'): - preset_d['dns'] = ' '.join(preset_d['dns']) + if edit_iface.dns: + display_dns = ' '.join(edit_iface.dns) else: - preset_d['dns'] = None - dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), - preset_d['dns']).run().strip() + display_dns = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() if len(dns_input): dns = dns_input.split(' ') - return NetworkConfiguration(NicType.MANUAL, iface=nic, ip=ip, gateway=gateway, dns=dns, dhcp=False) + 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=nic) + return NetworkConfiguration(NicType.MANUAL, iface=iface_name) + + +def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[NetworkConfiguration]]) -> Optional[Union[List[NetworkConfiguration], 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 + + nic = Menu(_( + 'Select one network interface to configure'), + list(network_options.values()), + cursor_index=cursor_idx, + sort=False + ).run() + + if not nic: + return preset + + if nic == network_options['none']: + return None + elif nic == network_options['iso_config']: + return NetworkConfiguration(NicType.ISO) + elif nic == network_options['network_manager']: + return NetworkConfiguration(NicType.NM) + elif nic == network_options['manual']: + manual = ManualNetworkConfig('Configure interfaces', preset) + return manual.run_manual() + + return preset diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py index 6de8d0ef..0515876b 100644 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ b/archinstall/lib/user_interaction/subvolume_config.py @@ -1,3 +1,5 @@ +from typing import List, Any, Dict + from ..menu.list_manager import ListManager from ..menu.selection_menu import Selector, GeneralMenu from ..menu.text_input import TextInput @@ -12,8 +14,8 @@ class SubvolumeList(ListManager): self.ObjectDefaultAction = str(_('Add')) super().__init__(prompt,list,None,self.ObjectNullAction,self.ObjectDefaultAction) - def reformat(self): - def presentation(key,value): + def reformat(self, data: Any) -> List[Any]: + def presentation(key :str, value :Dict): text = _(" Subvolume :{:16}").format(key) if isinstance(value,str): text += _(" mounted at {:16}").format(value) @@ -26,32 +28,31 @@ class SubvolumeList(ListManager): text += _(" with option {}").format(', '.join(value['options'])) return text - return sorted(list(map(lambda x:presentation(x,self.data[x]),self.data))) + return sorted(list(map(lambda x:presentation(x,data[x]),data))) def action_list(self): return super().action_list() - def exec_action(self): + def exec_action(self, data: Any): if self.target: origkey,origval = list(self.target.items())[0] else: origkey = None if self.action == str(_('Delete')): - del self.data[origkey] - return True - - if self.action == str(_('Add')): - self.target = {} - print(_('\n Fill the desired values for a new subvolume \n')) - with SubvolumeMenu(self.target,self.action) as add_menu: - for data in ['name','mountpoint','options']: - add_menu.exec_option(data) + del data[origkey] else: - SubvolumeMenu(self.target,self.action).run() - self.data.update(self.target) + if self.action == str(_('Add')): + self.target = {} + print(_('\n Fill the desired values for a new subvolume \n')) + with SubvolumeMenu(self.target,self.action) as add_menu: + for data in ['name','mountpoint','options']: + add_menu.exec_option(data) + else: + SubvolumeMenu(self.target,self.action).run() + + data.update(self.target) - return True class SubvolumeMenu(GeneralMenu): def __init__(self,parameters,action=None): diff --git a/examples/guided.py b/examples/guided.py index a677baa9..56e05ef9 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -4,6 +4,7 @@ import time import archinstall from archinstall import ConfigurationOutput +from archinstall.lib.models.network_configuration import NetworkConfigurationHandler if archinstall.arguments.get('help'): print("See `man archinstall` for help.") @@ -167,7 +168,8 @@ def perform_installation(mountpoint): network_config = archinstall.arguments.get('nic', None) if network_config: - network_config.config_installer(installation) + handler = NetworkConfigurationHandler(network_config) + handler.config_installer(installation) if archinstall.arguments.get('audio', None) is not None: installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=logging.INFO) diff --git a/examples/swiss.py b/examples/swiss.py index b94ddcce..baf7b618 100644 --- a/examples/swiss.py +++ b/examples/swiss.py @@ -17,9 +17,13 @@ import logging import os import time import pathlib +from typing import TYPE_CHECKING, Any import archinstall -from archinstall import ConfigurationOutput +from archinstall import ConfigurationOutput, NetworkConfigurationHandler + +if TYPE_CHECKING: + _: Any if archinstall.arguments.get('help'): print("See `man archinstall` for help.") @@ -397,7 +401,8 @@ def os_setup(installation): network_config = archinstall.arguments.get('nic', None) if network_config: - network_config.config_installer(installation) + handler = NetworkConfigurationHandler(network_config) + handler.config_installer(installation) if archinstall.arguments.get('audio', None) is not None: installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}",level=logging.INFO) -- cgit v1.2.3-70-g09d2