anaconda/anaconda-40.22.3.13/pyanaconda/ui/tui/spokes/network.py

635 lines
26 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
# Network configuration spoke classes
#
# Copyright (C) 2013 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 gi
gi.require_version("NM", "1.0")
from gi.repository import NM
import socket
from pyanaconda import network
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.modules.common.constants.services import NETWORK
from pyanaconda.modules.common.structures.network import NetworkDeviceConfiguration
from pyanaconda.flags import flags
from pyanaconda.ui.categories.system import SystemCategory
from pyanaconda.ui.tui.spokes import NormalTUISpoke
from pyanaconda.ui.tui.tuiobject import Dialog, report_if_failed
from pyanaconda.ui.common import FirstbootSpokeMixIn
from pyanaconda.core.i18n import N_, _
from pyanaconda.core.regexes import IPV4_PATTERN_WITH_ANCHORS, IPV4_NETMASK_WITH_ANCHORS, IPV4_OR_DHCP_PATTERN_WITH_ANCHORS
from pyanaconda.core.constants import ANACONDA_ENVIRON
from pyanaconda.anaconda_loggers import get_module_logger
from simpleline.render.containers import ListColumnContainer
from simpleline.render.prompt import Prompt
from simpleline.render.screen import InputState
from simpleline.render.screen_handler import ScreenHandler
from simpleline.render.widgets import TextWidget, CheckboxWidget, EntryWidget
log = get_module_logger(__name__)
# This will be used in decorators in ConfigureDeviceSpoke.
# The decorators are processed before the class is created so you can have this as a variable there.
IP_ERROR_MSG = N_("Bad format of the IP address")
NETMASK_ERROR_MSG = N_("Bad format of the netmask")
__all__ = ["NetworkSpoke"]
# TODO: use our own datastore?
class WiredTUIConfigurationData():
"""Holds tui input configuration data of wired device."""
def __init__(self):
self.ip = "dhcp"
self.netmask = ""
self.gateway = ""
self.ipv6 = "auto"
self.ipv6gateway = ""
self.nameserver = ""
self.ipv6addrgenmode = NM.SettingIP6ConfigAddrGenMode.EUI64
self.onboot = False
def set_from_connection(self, connection):
"""Set the object from NM RemoteConnection.
:param connection: connection to be used to set the object
:type connection: NM.RemoteConnection
"""
connection_uuid = connection.get_uuid()
ip4_config = connection.get_setting_ip4_config()
ip4_method = ip4_config.get_method()
if ip4_method == NM.SETTING_IP4_CONFIG_METHOD_AUTO:
self.ip = "dhcp"
elif ip4_method == NM.SETTING_IP4_CONFIG_METHOD_MANUAL:
if ip4_config.get_num_addresses() > 0:
addr = ip4_config.get_address(0)
self.ip = addr.get_address()
self.netmask = network.prefix_to_netmask(addr.get_prefix())
else:
log.error("No ip4 address found for manual method in %s", connection_uuid)
elif ip4_method == NM.SETTING_IP4_CONFIG_METHOD_DISABLED:
self.ip = ""
else:
log.error("Unexpected ipv4 method %s found in connection %s", ip4_method, connection_uuid)
self.ip = "dhcp"
self.gateway = ip4_config.get_gateway() or ""
ip6_config = connection.get_setting_ip6_config()
self.ipv6addrgenmode = ip6_config.get_addr_gen_mode()
ip6_method = ip6_config.get_method()
if ip6_method == NM.SETTING_IP6_CONFIG_METHOD_AUTO:
self.ipv6 = "auto"
elif ip6_method == NM.SETTING_IP6_CONFIG_METHOD_IGNORE:
self.ipv6 = "ignore"
elif ip6_method == NM.SETTING_IP6_CONFIG_METHOD_DHCP:
self.ipv6 = "dhcp"
elif ip6_method == NM.SETTING_IP6_CONFIG_METHOD_MANUAL:
if ip6_config.get_num_addresses() > 0:
addr = ip6_config.get_address(0)
self.ipv6 = "{}/{}".format(addr.get_address(), addr.get_prefix())
else:
log.error("No ip6 address found for manual method in %s", connection_uuid)
else:
log.error("Unexpected ipv6 method %s found in connection %s", ip6_method, connection_uuid)
self.ipv6 = "auto"
self.ipv6gateway = ip6_config.get_gateway() or ""
nameservers = []
for i in range(0, ip4_config.get_num_dns()):
nameservers.append(ip4_config.get_dns(i))
for i in range(0, ip6_config.get_num_dns()):
nameservers.append(ip6_config.get_dns(i))
self.nameserver = ",".join(nameservers)
self.onboot = connection.get_setting_connection().get_autoconnect()
def update_connection(self, connection):
"""Update NM RemoteConnection from the object.
:param connection: connection to be updated from the object
:type connection: NM.RemoteConnection
"""
# ipv4 settings
if self.ip == "dhcp":
method4 = NM.SETTING_IP4_CONFIG_METHOD_AUTO
elif self.ip:
method4 = NM.SETTING_IP4_CONFIG_METHOD_MANUAL
else:
method4 = NM.SETTING_IP4_CONFIG_METHOD_DISABLED
connection.remove_setting(NM.SettingIP4Config)
s_ip4 = NM.SettingIP4Config.new()
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, method4)
if method4 == NM.SETTING_IP4_CONFIG_METHOD_MANUAL:
prefix4 = network.netmask_to_prefix(self.netmask)
addr4 = NM.IPAddress.new(socket.AF_INET, self.ip, prefix4)
s_ip4.add_address(addr4)
if self.gateway:
s_ip4.props.gateway = self.gateway
connection.add_setting(s_ip4)
# ipv6 settings
if self.ipv6 == "ignore":
method6 = NM.SETTING_IP6_CONFIG_METHOD_IGNORE
elif not self.ipv6 or self.ipv6 == "auto":
method6 = NM.SETTING_IP6_CONFIG_METHOD_AUTO
elif self.ipv6 == "dhcp":
method6 = NM.SETTING_IP6_CONFIG_METHOD_DHCP
else:
method6 = NM.SETTING_IP6_CONFIG_METHOD_MANUAL
connection.remove_setting(NM.SettingIP6Config)
s_ip6 = NM.SettingIP6Config.new()
s_ip6.set_property(NM.SETTING_IP6_CONFIG_ADDR_GEN_MODE, self.ipv6addrgenmode)
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, method6)
if method6 == NM.SETTING_IP6_CONFIG_METHOD_MANUAL:
addr6, _slash, prefix6 = self.ipv6.partition("/")
if prefix6:
prefix6 = int(prefix6)
else:
prefix6 = 64
addr6 = NM.IPAddress.new(socket.AF_INET6, addr6, prefix6)
s_ip6.add_address(addr6)
if self.ipv6gateway:
s_ip6.props.gateway = self.ipv6gateway
connection.add_setting(s_ip6)
# nameservers
if self.nameserver:
for ns in [str.strip(i) for i in self.nameserver.split(",")]:
if NM.utils_ipaddr_valid(socket.AF_INET6, ns):
s_ip6.add_dns(ns)
elif NM.utils_ipaddr_valid(socket.AF_INET, ns):
s_ip4.add_dns(ns)
else:
log.error("IP address %s is not valid", ns)
s_con = connection.get_setting_connection()
s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, self.onboot)
def __str__(self):
return "WiredTUIConfigurationData ip:{} netmask:{} gateway:{} ipv6:{} ipv6gateway:{} " \
"nameserver:{} onboot:{} addr-gen-mode:{}".format(self.ip, self.netmask, self.gateway,
self.ipv6, self.ipv6gateway,
self.nameserver, self.onboot,
self.ipv6addrgenmode)
class NetworkSpoke(FirstbootSpokeMixIn, NormalTUISpoke):
""" Spoke used to configure network settings.
.. inheritance-diagram:: NetworkSpoke
:parts: 3
"""
category = SystemCategory
configurable_device_types = [
NM.DeviceType.ETHERNET,
NM.DeviceType.INFINIBAND,
]
@staticmethod
def get_screen_id():
"""Return a unique id of this UI screen."""
return "network-configuration"
def __init__(self, data, storage, payload):
NormalTUISpoke.__init__(self, data, storage, payload)
self.title = N_("Network configuration")
self._network_module = NETWORK.get_proxy()
self.nm_client = network.get_nm_client()
if not self.nm_client and conf.system.provides_system_bus:
self.nm_client = NM.Client.new(None)
self._container = None
self.hostname = self._network_module.Hostname
self.editable_configurations = []
self.errors = []
self._apply = False
@classmethod
def should_run(cls, environment, data):
"""Should the spoke run?"""
if not FirstbootSpokeMixIn.should_run(environment, data):
return False
return conf.system.can_configure_network
def initialize(self):
self.initialize_start()
NormalTUISpoke.initialize(self)
self._update_editable_configurations()
self._network_module.DeviceConfigurationChanged.connect(self._device_configurations_changed)
self.initialize_done()
def _device_configurations_changed(self, device_configurations):
log.debug("device configurations changed: %s", device_configurations)
self._update_editable_configurations()
def _update_editable_configurations(self):
device_configurations = NetworkDeviceConfiguration.from_structure_list(
self._network_module.GetDeviceConfigurations()
)
self.editable_configurations = [dc for dc in device_configurations
if dc.device_type in self.configurable_device_types]
@property
def completed(self):
""" Check whether this spoke is complete or not."""
# If we can't configure network, don't require it
return (not conf.system.can_configure_network
or self._network_module.IsConnecting
or self._network_module.Connected)
@property
def mandatory(self):
# the network spoke should be mandatory only if it is running
# during the installation and if the installation source requires network
return ANACONDA_ENVIRON in flags.environs and self.payload.needs_network
@property
def status(self):
""" Short msg telling what devices are active. """
return network.status_message(self.nm_client)
def _summary_text(self):
"""Devices cofiguration shown to user."""
msg = ""
activated_devs = self._network_module.GetActivatedInterfaces()
for device_configuration in self.editable_configurations:
name = device_configuration.device_name
if name in activated_devs:
msg += self._activated_device_msg(name)
else:
msg += _("Wired (%(interface_name)s) disconnected\n") \
% {"interface_name": name}
return msg
def _activated_device_msg(self, devname):
msg = _("Wired (%(interface_name)s) connected\n") \
% {"interface_name": devname}
device = self.nm_client.get_device_by_iface(devname)
if device:
addr_str = dnss_str = gateway_str = netmask_str = ""
ipv4config = device.get_ip4_config()
if ipv4config:
addresses = ipv4config.get_addresses()
if addresses:
a0 = addresses[0]
addr_str = a0.get_address()
prefix = a0.get_prefix()
netmask_str = network.prefix_to_netmask(prefix)
gateway_str = ipv4config.get_gateway() or ''
dnss_str = ",".join(ipv4config.get_nameservers())
msg += _(" IPv4 Address: %(addr)s Netmask: %(netmask)s Gateway: %(gateway)s\n") % \
{"addr": addr_str, "netmask": netmask_str, "gateway": gateway_str}
msg += _(" DNS: %s\n") % dnss_str
ipv6config = device.get_ip6_config()
if ipv6config:
for address in ipv6config.get_addresses():
addr_str = address.get_address()
prefix = address.get_prefix()
# Do not display link-local addresses
if not addr_str.startswith("fe80:"):
msg += _(" IPv6 Address: %(addr)s/%(prefix)d\n") % \
{"addr": addr_str, "prefix": prefix}
return msg
def refresh(self, args=None):
""" Refresh screen. """
NormalTUISpoke.refresh(self, args)
self._container = ListColumnContainer(1, columns_width=78, spacing=1)
if not self.nm_client:
self.window.add_with_separator(TextWidget(_("Network configuration is not available.")))
return
summary = self._summary_text()
self.window.add_with_separator(TextWidget(summary))
hostname = _("Host Name: %s\n") % self._network_module.Hostname
self.window.add_with_separator(TextWidget(hostname))
current_hostname = _("Current host name: %s\n") % self._network_module.GetCurrentHostname()
self.window.add_with_separator(TextWidget(current_hostname))
# if we have any errors, display them
while len(self.errors) > 0:
self.window.add_with_separator(TextWidget(self.errors.pop()))
dialog = Dialog(_("Host Name"))
self._container.add(TextWidget(_("Set host name")), callback=self._set_hostname_callback, data=dialog)
for device_configuration in self.editable_configurations:
iface = device_configuration.device_name
text = (_("Configure device %s") % iface)
self._container.add(TextWidget(text), callback=self._ensure_connection_and_configure,
data=iface)
self.window.add_with_separator(self._container)
def _set_hostname_callback(self, dialog):
self.hostname = dialog.run()
self.redraw()
self.apply()
def _ensure_connection_and_configure(self, iface):
for device_configuration in self.editable_configurations:
if device_configuration.device_name == iface:
connection_uuid = device_configuration.connection_uuid
if connection_uuid:
self._configure_connection(iface, connection_uuid)
else:
device_type = self.nm_client.get_device_by_iface(iface).get_device_type()
connection = get_default_connection(iface, device_type)
connection_uuid = connection.get_uuid()
log.debug("adding default connection %s for %s", connection_uuid, iface)
data = (iface, connection_uuid)
self.nm_client.add_connection2(
connection.to_dbus(NM.ConnectionSerializationFlags.ALL),
(NM.SettingsAddConnection2Flags.TO_DISK |
NM.SettingsAddConnection2Flags.BLOCK_AUTOCONNECT),
None,
False,
None,
self._default_connection_added_cb,
data
)
return
log.error("device configuration for %s not found", iface)
def _default_connection_added_cb(self, client, result, data):
iface, connection_uuid = data
try:
_connection, result = client.add_connection2_finish(result)
except Exception as e: # pylint: disable=broad-except
msg = "adding default connection {} from {} failed: {}".format(
connection_uuid, iface, str(e))
log.error(msg)
self.errors.append(msg)
self.redraw()
else:
log.debug("added default connection %s for %s: %s", connection_uuid, iface, result)
self._configure_connection(iface, connection_uuid)
def _configure_connection(self, iface, connection_uuid):
connection = self.nm_client.get_connection_by_uuid(connection_uuid)
new_spoke = ConfigureDeviceSpoke(self.data, self.storage, self.payload,
self._network_module, iface, connection)
ScreenHandler.push_screen_modal(new_spoke)
if new_spoke.errors:
self.errors.extend(new_spoke.errors)
self.redraw()
return
if new_spoke.apply_configuration:
self._apply = True
self._network_module.LogConfigurationState(
"Settings of {} updated in TUI.".format(iface)
)
self.redraw()
self.apply()
def input(self, args, key):
""" Handle the input. """
if self._container.process_user_input(key):
return InputState.PROCESSED
else:
return super().input(args, key)
def apply(self):
"""Apply all of our settings."""
# Inform network module that device configurations might have been changed
# and we want to generate kickstart from device configurations
# (persistent NM / config files configuration), instead of using original kickstart.
self._network_module.NetworkDeviceConfigurationChanged()
(valid, error) = network.is_valid_hostname(self.hostname, local=True)
if not self.hostname or valid:
self._network_module.Hostname = self.hostname
else:
self.errors.append(_("Host name is not valid: %s") % error)
self.hostname = self._network_module.Hostname
if self._apply:
self._apply = False
if ANACONDA_ENVIRON in flags.environs:
from pyanaconda.payload.manager import payloadMgr
payloadMgr.start(self.payload)
class ConfigureDeviceSpoke(NormalTUISpoke):
""" Spoke to set various configuration options for net devices. """
category = "network"
def __init__(self, data, storage, payload, network_module, iface, connection):
super().__init__(data, storage, payload)
self.title = N_("Device configuration")
self._network_module = network_module
self._container = None
self._connection = connection
self._iface = iface
self._connection_uuid = connection.get_uuid()
self.errors = []
self.apply_configuration = False
self._data = WiredTUIConfigurationData()
self._data.set_from_connection(self._connection)
log.debug("Configure iface %s: connection %s -> %s", self._iface, self._connection_uuid,
self._data)
def refresh(self, args=None):
""" Refresh window. """
super().refresh(args)
self._container = ListColumnContainer(1)
dialog = Dialog(title=(_('IPv4 address or %s for DHCP') % '"dhcp"'),
conditions=[self._check_ipv4_or_dhcp])
self._container.add(EntryWidget(dialog.title, self._data.ip), self._set_ipv4_or_dhcp, dialog)
dialog = Dialog(title=_("IPv4 netmask"), conditions=[self._check_netmask])
self._container.add(EntryWidget(dialog.title, self._data.netmask), self._set_netmask, dialog)
dialog = Dialog(title=_("IPv4 gateway"), conditions=[self._check_ipv4])
self._container.add(EntryWidget(dialog.title, self._data.gateway), self._set_ipv4_gateway, dialog)
msg = (_('IPv6 address[/prefix] or %(auto)s for automatic, %(dhcp)s for DHCP, '
'%(ignore)s to turn off')
% {"auto": '"auto"', "dhcp": '"dhcp"', "ignore": '"ignore"'})
dialog = Dialog(title=msg, conditions=[self._check_ipv6_config])
self._container.add(EntryWidget(dialog.title, self._data.ipv6), self._set_ipv6, dialog)
dialog = Dialog(title=_("IPv6 default gateway"), conditions=[self._check_ipv6])
self._container.add(EntryWidget(dialog.title, self._data.ipv6gateway), self._set_ipv6_gateway, dialog)
dialog = Dialog(title=_("Nameservers (comma separated)"), conditions=[self._check_nameservers])
self._container.add(EntryWidget(dialog.title, self._data.nameserver), self._set_nameservers, dialog)
msg = _("Connect automatically after reboot")
w = CheckboxWidget(title=msg, completed=self._data.onboot)
self._container.add(w, self._set_onboot_handler)
msg = _("Apply configuration in installer")
w = CheckboxWidget(title=msg, completed=self.apply_configuration)
self._container.add(w, self._set_apply_handler)
self.window.add_with_separator(self._container)
message = _("Configuring device %s.") % self._iface
self.window.add_with_separator(TextWidget(message))
@report_if_failed(message=IP_ERROR_MSG)
def _check_ipv4_or_dhcp(self, user_input, report_func):
return IPV4_OR_DHCP_PATTERN_WITH_ANCHORS.match(user_input) is not None
@report_if_failed(message=IP_ERROR_MSG)
def _check_ipv4(self, user_input, report_func):
return IPV4_PATTERN_WITH_ANCHORS.match(user_input) is not None
@report_if_failed(message=NETMASK_ERROR_MSG)
def _check_netmask(self, user_input, report_func):
return IPV4_NETMASK_WITH_ANCHORS.match(user_input) is not None
@report_if_failed(message=IP_ERROR_MSG)
def _check_ipv6(self, user_input, report_func):
return network.check_ip_address(user_input, version=6)
@report_if_failed(message=IP_ERROR_MSG)
def _check_ipv6_config(self, user_input, report_func):
if user_input in ["auto", "dhcp", "ignore"]:
return True
addr, _slash, prefix = user_input.partition("/")
if prefix:
try:
if not 1 <= int(prefix) <= 128:
return False
except ValueError:
return False
return network.check_ip_address(addr, version=6)
@report_if_failed(message=IP_ERROR_MSG)
def _check_nameservers(self, user_input, report_func):
if user_input.strip():
addresses = [str.strip(i) for i in user_input.split(",")]
for ip in addresses:
if not network.check_ip_address(ip):
return False
return True
def _set_ipv4_or_dhcp(self, dialog):
self._data.ip = dialog.run()
def _set_netmask(self, dialog):
self._data.netmask = dialog.run()
def _set_ipv4_gateway(self, dialog):
self._data.gateway = dialog.run()
def _set_ipv6(self, dialog):
self._data.ipv6 = dialog.run()
def _set_ipv6_gateway(self, dialog):
self._data.ipv6gateway = dialog.run()
def _set_nameservers(self, dialog):
self._data.nameserver = dialog.run()
def _set_apply_handler(self, args):
self.apply_configuration = not self.apply_configuration
def _set_onboot_handler(self, args):
self._data.onboot = not self._data.onboot
def input(self, args, key):
if self._container.process_user_input(key):
return InputState.PROCESSED_AND_REDRAW
else:
if key.lower() == Prompt.CONTINUE:
if self._data.ip != "dhcp" and not self._data.netmask:
self.errors.append(_("Configuration not saved: netmask missing in static configuration"))
else:
self.apply()
return InputState.PROCESSED_AND_CLOSE
else:
return super().input(args, key)
@property
def indirect(self):
return True
def apply(self):
"""Apply changes to NM connection."""
log.debug("updating connection %s:\n%s", self._connection_uuid,
self._connection.to_dbus(NM.ConnectionSerializationFlags.ALL))
updated_connection = NM.SimpleConnection.new_clone(self._connection)
self._data.update_connection(updated_connection)
# Commit the changes
self._connection.update2(
updated_connection.to_dbus(NM.ConnectionSerializationFlags.ALL),
NM.SettingsUpdate2Flags.TO_DISK | NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT,
None,
None,
self._connection_updated_cb,
self._connection_uuid
)
def _connection_updated_cb(self, connection, result, connection_uuid):
connection.update2_finish(result)
log.debug("updated connection %s:\n%s", connection_uuid,
connection.to_dbus(NM.ConnectionSerializationFlags.ALL))
if self.apply_configuration:
nm_client = network.get_nm_client()
device = nm_client.get_device_by_iface(self._iface)
log.debug("activating connection %s with device %s",
connection_uuid, self._iface)
nm_client.activate_connection_async(connection, device, None, None)
def get_default_connection(iface, device_type):
"""Get default connection to be edited by the UI."""
connection = NM.SimpleConnection.new()
s_con = NM.SettingConnection.new()
s_con.props.uuid = NM.utils_uuid_generate()
s_con.props.autoconnect = True
s_con.props.id = iface
s_con.props.interface_name = iface
if device_type == NM.DeviceType.ETHERNET:
s_con.props.type = "802-3-ethernet"
s_wired = NM.SettingWired.new()
connection.add_setting(s_wired)
elif device_type == NM.DeviceType.INFINIBAND:
s_con.props.type = "infiniband"
s_ib = NM.SettingInfiniband.new()
s_ib.props.transport_mode = "datagram"
connection.add_setting(s_ib)
connection.add_setting(s_con)
return connection