From 537b9cab037aecfd18edef156dd3ea55072918e9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Mar 2022 01:57:57 +1100 Subject: Rework network config (#1001) * Update network configuration * Rework network configuration * Update documentation * Fix flake8 * Update Co-authored-by: Daniel Girtler Co-authored-by: Anton Hvornum --- .flake8 | 3 +- archinstall/__init__.py | 11 +-- archinstall/lib/installer.py | 43 ++++++----- archinstall/lib/models/network_configuration.py | 97 +++++++++++++++++++++++++ archinstall/lib/user_interaction.py | 35 +++++---- docs/installing/guided.rst | 13 ++-- examples/config-sample.json | 5 +- examples/custom-command-sample.json | 2 +- examples/guided.py | 18 ++--- examples/swiss.py | 17 +---- schema.json | 24 +++--- 11 files changed, 177 insertions(+), 91 deletions(-) create mode 100644 archinstall/lib/models/network_configuration.py diff --git a/.flake8 b/.flake8 index d9fb938e..bf169692 100644 --- a/.flake8 +++ b/.flake8 @@ -6,5 +6,4 @@ max-complexity = 40 max-line-length = 236 show-source = True statistics = True -per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503 guided.py:C901 -builtins = _ \ No newline at end of file +per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503 guided.py:C901 network_configuration.py:F821 diff --git a/archinstall/__init__.py b/archinstall/__init__.py index cb95028b..8c64a1f3 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -123,7 +123,7 @@ def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, erro print(f" We ignore the entry {element} as it isn't related to any argument") return config -def get_arguments(): +def get_arguments() -> Dict[str, Any]: """ The handling of parameters from the command line Is done on following steps: 0) we create a dict to store the arguments and their values @@ -203,13 +203,8 @@ def load_config(): storage['gfx_driver_packages'] = AVAILABLE_GFX_DRIVERS.get(arguments.get('gfx_driver', None), None) if arguments.get('servers', None) is not None: storage['_selected_servers'] = arguments.get('servers', None) - if nic_config := arguments.get('nic', {}): - if isinstance(nic_config,str) or nic_config.get('nic', '') == 'Copy ISO network configuration to installation': - arguments['nic'] = {'type': 'iso_config'} - elif 'NetworkManager' in nic_config: - arguments['nic'] = {'type': 'network_manager', 'NetworkManager': True} - else: - arguments['nic'] = {k if k != 'nic' else 'type': v for k, v in nic_config.items()} + if arguments.get('nic', None) is not None: + arguments['nic'] = NetworkConfiguration.parse_arguments(arguments.get('nic')) def post_process_arguments(arguments): storage['arguments'] = arguments diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index cf643b27..894bcc2a 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -35,6 +35,7 @@ __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "l __accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"] from .pacman import run_pacman +from .models.network_configuration import NetworkConfiguration class InstallationFile: @@ -479,37 +480,35 @@ class Installer: def drop_to_shell(self) -> None: subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) - def configure_nic(self, - nic :str, - dhcp :bool = True, - ip :Optional[str] = None, - gateway :Optional[str] = None, - dns :Optional[str] = None, - *args :str, - **kwargs :str - ) -> None: + def configure_nic(self, network_config: NetworkConfiguration) -> None: from .systemd import Networkd - if dhcp: - conf = Networkd(Match={"Name": nic}, Network={"DHCP": "yes"}) + if network_config.dhcp: + conf = Networkd(Match={"Name": network_config.iface}, Network={"DHCP": "yes"}) else: - assert ip + network = {"Address": network_config.ip} + if network_config.gateway: + network["Gateway"] = network_config.gateway + if network_config.dns: + dns = network_config.dns + network["DNS"] = dns if isinstance(dns, list) else [dns] - network = {"Address": ip} - if gateway: - network["Gateway"] = gateway - if dns: - assert type(dns) == list - network["DNS"] = dns - - conf = Networkd(Match={"Name": nic}, Network=network) + conf = Networkd(Match={"Name": network_config.iface}, Network=network) for plugin in plugins.values(): if hasattr(plugin, 'on_configure_nic'): - if (new_conf := plugin.on_configure_nic(nic, dhcp, ip, gateway, dns)): + new_conf = plugin.on_configure_nic( + network_config.iface, + network_config.dhcp, + network_config.ip, + network_config.gateway, + network_config.dns + ) + + if new_conf: conf = new_conf - with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf: + with open(f"{self.target}/etc/systemd/network/10-{network_config.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/models/network_configuration.py b/archinstall/lib/models/network_configuration.py new file mode 100644 index 00000000..f1ee4c3f --- /dev/null +++ b/archinstall/lib/models/network_configuration.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional, Dict + +from ... import log + + +class NicType(str, Enum): + ISO = "iso" + NM = "nm" + MANUAL = "manual" + + +@dataclass +class NetworkConfiguration: + type: NicType + iface: str = None + ip: str = None + dhcp: bool = True + gateway: str = None + dns: List[str] = None + + 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' + + # for json serialization when calling json.dumps(...) on this class + def json(self): + return self.__dict__ + + @classmethod + def parse_arguments(cls, config: Dict[str, str]) -> Optional["NetworkConfiguration"]: + nic_type = config.get('type', None) + + if not nic_type: + 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 + + def is_network_manager(self) -> bool: + return self.type == NicType.NM + + 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) + installation.enable_service('systemd-networkd') + installation.enable_service('systemd-resolved') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 7524dd8b..1f62b7fd 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -14,6 +14,7 @@ from typing import List, Any, Optional, Dict, Union, TYPE_CHECKING # https://stackoverflow.com/a/39757388/929999 from .menu.text_input import TextInput +from .models.network_configuration import NetworkConfiguration, NicType if TYPE_CHECKING: from .disk.partition import Partition @@ -450,11 +451,12 @@ def ask_additional_packages_to_install(pre_set_packages :List[str] = []) -> List return packages -def ask_to_configure_network(preset :Dict[str, Any] = {}) -> Dict[str, Any]: - # Optionally configure one network interface. - # while 1: - # {MAC: Ifname} +def ask_to_configure_network(preset :Dict[str, Any] = {}) -> Optional[NetworkConfiguration]: + """ + Configure the network on the newly installed system + """ 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() @@ -473,17 +475,17 @@ def ask_to_configure_network(preset :Dict[str, Any] = {}) -> Dict[str, Any]: except ValueError: pass - nic = Menu(_('Select one network interface to configure'), interfaces.values(),cursor_index=cursor_idx).run() + nic = Menu(_('Select one network interface to configure'), interfaces.values(), cursor_index=cursor_idx, sort=False).run() if not nic: - return {} - - # nic = network_manager_nt + return None - if nic == interfaces['iso_config']: - return {'type': 'iso_config'} + if nic == interfaces['none']: + return None + elif nic == interfaces['iso_config']: + return NetworkConfiguration(NicType.ISO) elif nic == interfaces['network_manager']: - return {'type': 'network_manager', 'NetworkManager': True} + return NetworkConfiguration(NicType.NM) else: # Current workaround: # For selecting modes without entering text within brackets, @@ -543,10 +545,17 @@ def ask_to_configure_network(preset :Dict[str, Any] = {}) -> Dict[str, Any]: if len(dns_input): dns = dns_input.split(' ') - return {'type': nic, 'dhcp': False, 'ip': ip, 'gateway': gateway, 'dns': dns} + return NetworkConfiguration( + NicType.MANUAL, + iface=nic, + ip=ip, + gateway=gateway, + dns=dns, + dhcp=False + ) else: # this will contain network iface names - return {'type': nic} + return NetworkConfiguration(NicType.MANUAL, iface=nic) def partition_overlap(partitions :list, start :str, end :str) -> bool: diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 5c360256..ac6254f2 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -73,8 +73,7 @@ There are three different configuration files, all of which are optional. "keyboard-language": "us", "mirror-region": "Worldwide", "nic": { - "NetworkManager": true, - "nic": "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)" + "type": "NM" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], @@ -124,9 +123,13 @@ Options for ``--config`` | | | "Worldwide" or "Sweden" | | Either takes a dictionary structure of region and a given set of mirrors. | | | | | | Or just a region and archinstall will source any mirrors for that region automatically | | +----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+ -| nic | | { NetworkManager: } | | Takes three different kind of options. Copy, NetworkManager or a nic name. | No | -| | | { "eth0": {"address": "", "subnet": "255.0.0.0"}}| | Copy will copy the network configuration used in the live ISO. NetworkManager will | | -| | | "Copy ISO network configuration to installation" | | install and configure `NetworkManager `_ | | +| nic | | { type: } | | Type must be one of ISO, NM, MANUAL. ISO will copy the configuration on the image, | No | +| | | | | NM configures NetworkManager and MANUAL allows to specify custom configuration | | +| | | { "iface": "eth0"} | | Only MANUAL: name of the interface | | +| | | { "dhcp": } | | Only MANUAL: If set to true DHCP auto will be setup and all further configs ignored | | +| | | { "ip": } | | Only MANUAL: Ip address to set, is MANDATORY | | +| | | { "gateway": } | | Only MANUAL: Optional gateway | | +| | | { "dns": []} | | Only MANUAL: Optional DNS servers | | +----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+ | ntp | | Set to true to set-up ntp post install | No | +----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+ diff --git a/examples/config-sample.json b/examples/config-sample.json index 18644860..dc8693a7 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -15,8 +15,7 @@ } }, "nic": { - "NetworkManager": true, - "nic": "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)" + "type": "NM" }, "ntp": true, "packages": [], @@ -26,4 +25,4 @@ "sys-encoding": "utf-8", "sys-language": "en_US", "timezone": "UTC" -} \ No newline at end of file +} diff --git a/examples/custom-command-sample.json b/examples/custom-command-sample.json index 9c39e15f..8d8d611d 100644 --- a/examples/custom-command-sample.json +++ b/examples/custom-command-sample.json @@ -13,7 +13,7 @@ "keyboard-layout": "us", "mirror-region": "Worldwide", "nic": { - "NetworkManager": true + "type": "NM" }, "ntp": true, "packages": ["docker", "git", "wget", "zsh"], diff --git a/examples/guided.py b/examples/guided.py index 23fd45a6..d2447341 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -145,11 +145,11 @@ def perform_installation(mountpoint): # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium - + # Retrieve list of additional repositories and set boolean values appropriately enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', None) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', None) - + if installation.minimal_installation(testing=enable_testing, multilib=enable_multilib): installation.set_locale(archinstall.arguments['sys-language'], archinstall.arguments['sys-encoding'].upper()) installation.set_hostname(archinstall.arguments['hostname']) @@ -163,16 +163,10 @@ def perform_installation(mountpoint): # If user selected to copy the current ISO network configuration # Perform a copy of the config - if archinstall.arguments.get('nic', {}).get('type', '') == 'iso_config': - installation.copy_iso_network_config(enable_services=True) # Sources the ISO network configuration to the install medium. - elif archinstall.arguments.get('nic', {}).get('NetworkManager', False): - installation.add_additional_packages("networkmanager") - installation.enable_service('NetworkManager.service') - # Otherwise, if a interface was selected, configure that interface - elif archinstall.arguments.get('nic', {}): - installation.configure_nic(**archinstall.arguments.get('nic', {})) - installation.enable_service('systemd-networkd') - installation.enable_service('systemd-resolved') + network_config = archinstall.arguments.get('nic', None) + + if network_config: + network_config.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 f60ff60f..4eb51a05 100644 --- a/examples/swiss.py +++ b/examples/swiss.py @@ -393,19 +393,10 @@ def os_setup(installation): if archinstall.arguments['swap']: installation.setup_swap('zram') - # If user selected to copy the current ISO network configuration - # Perform a copy of the config - if archinstall.arguments.get('nic', {}).get('type', '') == 'iso_config': - installation.copy_iso_network_config( - enable_services=True) # Sources the ISO network configuration to the install medium. - elif archinstall.arguments.get('nic', {}).get('NetworkManager', False): - installation.add_additional_packages("networkmanager") - installation.enable_service('NetworkManager.service') - # Otherwise, if a interface was selected, configure that interface - elif archinstall.arguments.get('nic', {}): - installation.configure_nic(**archinstall.arguments.get('nic', {})) - installation.enable_service('systemd-networkd') - installation.enable_service('systemd-resolved') + network_config = archinstall.arguments.get('nic', None) + + if network_config: + network_config.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/schema.json b/schema.json index aacf3f07..e4b19b64 100644 --- a/schema.json +++ b/schema.json @@ -84,18 +84,18 @@ "description": "Choose between NetworkManager, manual configuration, use systemd-networkd from the ISO or no configuration", "type": "object", "properties": { - "NetworkManager": { - "description": "", - "type": "boolean" - }, - "interface-name": { - "address": "ip address", - "subnet": "255.255.255.0", - "gateway": "ip address", - "dns": "ip address" - }, - "nic": "Copy ISO network configuration to installation", - "nic": {} + "type": "string", + "iface": "string", + "dhcp": "boolean", + "ip": "string", + "gateway": "string", + "dns": { + "description": "List of DNS servers", + "type": "array", + "items": { + "type": "string" + } + } } }, "ntp": { -- cgit v1.2.3-70-g09d2