# # Copyright (C) 2019 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the # GNU General Public License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # import copy import re from pyanaconda.core.regexes import NM_MAC_INITRAMFS_CONNECTION from pyanaconda.core.constants import NETWORK_CAPABILITY_TEAM from pyanaconda.modules.common.task import Task from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.modules.network.network_interface import NetworkInitializationTaskInterface from pyanaconda.modules.network.nm_client import get_device_name_from_network_data, \ update_connection_from_ksdata, add_connection_from_ksdata, bound_hwaddr_of_device, \ update_connection_values, commit_changes_with_autoconnection_blocked, \ get_config_file_connection_of_device, clone_connection_sync, nm_client_in_thread, \ is_bootif_connection from pyanaconda.modules.network.device_configuration import supported_wired_device_types, \ virtual_device_types from pyanaconda.modules.network.utils import guard_by_system_configuration, is_nbft_device log = get_module_logger(__name__) import gi gi.require_version("NM", "1.0") from gi.repository import NM class ApplyKickstartTask(Task): """Task for application of kickstart network configuration.""" def __init__(self, network_data, supported_devices, capabilities, bootif, ifname_option_values): """Create a new task. :param network_data: kickstart network data to be applied :type: list(NetworkData) :param supported_devices: list of names of supported network devices :type supported_devices: list(str) :param capabilities: list of capabilities supported by the network backend :type capabilities: list(int) :param bootif: MAC addres of device to be used for --device=bootif specification :type bootif: str :param ifname_option_values: list of ifname boot option values :type ifname_option_values: list(str) """ super().__init__() self._network_data = network_data self._supported_devices = supported_devices self._capabilities = capabilities self._bootif = bootif self._ifname_option_values = ifname_option_values @property def name(self): return "Apply kickstart" def for_publication(self): """Return a DBus representation.""" return NetworkInitializationTaskInterface(self) @guard_by_system_configuration(return_value=[]) def run(self): """Run the kickstart application. :returns: names of devices to which kickstart was applied :rtype: list(str) """ with nm_client_in_thread() as nm_client: return self._run(nm_client) def _run(self, nm_client): applied_devices = [] if not self._network_data: log.debug("%s: No kickstart data.", self.name) return applied_devices if not nm_client: log.debug("%s: No NetworkManager available.", self.name) return applied_devices for network_data in self._network_data: # Wireless is not supported if network_data.essid: log.info("%s: Wireless devices configuration is not supported.", self.name) continue if network_data.teamslaves and NETWORK_CAPABILITY_TEAM not in self._capabilities: log.info("%s: Team devices configuration is not supported.", self.name) continue device_name = get_device_name_from_network_data(nm_client, network_data, self._supported_devices, self._bootif) if not device_name: log.warning("%s: --device %s not found", self.name, network_data.device) continue if is_nbft_device(device_name): log.debug("Ignoring nBFT device %s", device_name) continue applied_devices.append(device_name) connection = self._find_initramfs_connection_of_iface(nm_client, device_name) if connection: # if the device was already configured in initramfs update the settings log.debug("%s: updating connection %s of device %s", self.name, connection.get_uuid(), device_name) update_connection_from_ksdata( nm_client, connection, network_data, device_name, ifname_option_values=self._ifname_option_values ) if network_data.activate: device = nm_client.get_device_by_iface(device_name) nm_client.activate_connection_async(connection, device, None, None) log.debug("%s: activating updated connection %s with device %s", self.name, connection.get_uuid(), device_name) else: log.debug("%s: adding connection for %s", self.name, device_name) add_connection_from_ksdata( nm_client, network_data, device_name, activate=network_data.activate, ifname_option_values=self._ifname_option_values ) return applied_devices def _find_initramfs_connection_of_iface(self, nm_client, iface): device = nm_client.get_device_by_iface(iface) if device: cons = device.get_available_connections() for con in cons: if con.get_interface_name() == iface and con.get_id() == iface: return con return None class DumpMissingConfigFilesTask(Task): """Task for dumping of missing config files.""" def __init__(self, default_network_data, ifname_option_values): """Create a new task. :param default_network_data: kickstart network data of default device configuration :type default_network_data: NetworkData :param ifname_option_values: list of ifname boot option values :type ifname_option_values: list(str) """ super().__init__() self._default_network_data = default_network_data self._ifname_option_values = ifname_option_values @property def name(self): return "Dump missing config files" def for_publication(self): """Return a DBus representation.""" return NetworkInitializationTaskInterface(self) def _select_persistent_connection_for_device(self, device, cons, allow_ports=False): """Select the connection suitable to store configuration for the device.""" iface = device.get_iface() ac = device.get_active_connection() if ac: con = ac.get_connection() if con.get_interface_name() == iface and con in cons: if allow_ports or not con.get_setting_connection().get_master(): return con else: log.debug("%s: active connection for %s can't be used as persistent", self.name, iface) for con in cons: if con.get_interface_name() == iface: if allow_ports or not con.get_setting_connection().get_master(): return con return None def _update_connection(self, nm_client, con, iface): log.debug("%s: updating id and binding (interface-name) of connection %s for %s", self.name, con.get_uuid(), iface) s_con = con.get_setting_connection() s_con.set_property(NM.SETTING_CONNECTION_ID, iface) s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, iface) s_wired = con.get_setting_wired() if s_wired: # By default connections are bound to interface name s_wired.set_property(NM.SETTING_WIRED_MAC_ADDRESS, None) bound_mac = bound_hwaddr_of_device(nm_client, iface, self._ifname_option_values) if bound_mac: s_wired.set_property(NM.SETTING_WIRED_MAC_ADDRESS, bound_mac) log.debug("%s: iface %s bound to mac address %s by ifname boot option", self.name, iface, bound_mac) log.debug("%s: updating addr-gen-mode of connection %s for %s", self.name, con.get_uuid(), iface) s_ipv6 = con.get_setting_ip6_config() # For example port connections do not have ipv6 setting present if s_ipv6: s_ipv6.set_property(NM.SETTING_IP6_CONFIG_ADDR_GEN_MODE, NM.SettingIP6ConfigAddrGenMode.EUI64) @guard_by_system_configuration(return_value=[]) def run(self): """Run dumping of missing config files. :returns: names of devices for which config file was created :rtype: list(str) """ with nm_client_in_thread() as nm_client: return self._run(nm_client) def _run(self, nm_client): new_configs = [] if not nm_client: log.debug("%s: No NetworkManager available.", self.name) return new_configs dumped_device_types = supported_wired_device_types + virtual_device_types for device in nm_client.get_devices(): if device.get_device_type() not in dumped_device_types: continue iface = device.get_iface() if is_nbft_device(iface or ""): log.debug("Ignoring nBFT device %s", iface) continue if get_config_file_connection_of_device(nm_client, iface): continue available_cons = device.get_available_connections() log.debug("%s: %s connections found for device %s", self.name, [con.get_uuid() for con in available_cons], iface) initramfs_cons = [con for con in available_cons if self._is_initramfs_connection(con, iface)] log.debug("%s: %s initramfs connections found for device %s", self.name, [con.get_uuid() for con in initramfs_cons], iface) dumped_con = None device_is_port = any(con.get_setting_connection().get_master() for con in available_cons) if device_is_port: # We have to dump persistent ifcfg files for ports created in initramfs # Filter out potenital connection created for BOOTIF option rhbz#2175664 port_cons = [c for c in available_cons if not is_bootif_connection(c)] if initramfs_cons: if len(port_cons) == 1: log.debug("%s: port device %s has an initramfs port connection", self.name, iface) dumped_con = self._select_persistent_connection_for_device( device, port_cons, allow_ports=True) else: log.debug("%s: port device %s has an initramfs connection", self.name, iface) else: log.debug("%s: not creating default connection for port device %s", self.name, iface) continue if not dumped_con: dumped_con = self._select_persistent_connection_for_device(device, available_cons) if not dumped_con and len(initramfs_cons) == 1: # Try to clone the persistent connection for the device # from the connection which should be a generic (not bound # to iface) connection created by NM in initramfs dumped_con = clone_connection_sync(nm_client, initramfs_cons[0], con_id=iface) if dumped_con: log.debug("%s: dumping connection %s to config file for %s", self.name, dumped_con.get_uuid(), iface) self._dump_connection(nm_client, dumped_con, iface, bool(initramfs_cons)) else: log.debug("%s: none of the connections can be dumped as persistent", self.name) if len(available_cons) > 1 and not device_is_port: log.warning("%s: unexpected number of connections, not dumping any", self.name) continue log.debug("%s: creating default connection for %s", self.name, iface) self._create_default_connection(nm_client, iface, bool(initramfs_cons)) new_configs.append(iface) return new_configs def _dump_connection(self, nm_client, dumped_con, iface, initramfs_con): self._update_connection(nm_client, dumped_con, iface) # Update some values of connection generated in initramfs so it # can be used as persistent configuration. if initramfs_con: update_connection_values( dumped_con, [ # Make sure ONBOOT is yes (NM.SETTING_CONNECTION_SETTING_NAME, NM.SETTING_CONNECTION_AUTOCONNECT, True), # Update cloned generic connection from initramfs (NM.SETTING_CONNECTION_SETTING_NAME, NM.SETTING_CONNECTION_MULTI_CONNECT, 0), # Update cloned generic connection from initramfs (NM.SETTING_CONNECTION_SETTING_NAME, NM.SETTING_CONNECTION_WAIT_DEVICE_TIMEOUT, -1) ] ) commit_changes_with_autoconnection_blocked(dumped_con, nm_client) def _create_default_connection(self, nm_client, iface, initramfs_con): network_data = copy.deepcopy(self._default_network_data) network_data.onboot = initramfs_con add_connection_from_ksdata( nm_client, network_data, iface, activate=False, ifname_option_values=self._ifname_option_values ) def _is_initramfs_connection(self, con, iface): con_id = con.get_id() return con_id in ["Wired Connection", iface] \ or re.match(NM_MAC_INITRAMFS_CONNECTION, con_id)