anaconda/anaconda-40.22.3.13/pyanaconda/network.py
2024-11-14 21:39:56 -08:00

608 lines
22 KiB
Python

#
# network.py - network configuration install data
#
# Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Red Hat, Inc.
# 2008, 2009, 2017
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>.
import shutil
import socket
import itertools
import os
import time
import threading
import re
import ipaddress
from dasbus.typing import get_native
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core import util, constants
from pyanaconda.core.i18n import _
from pyanaconda.core.kernel import kernel_arguments
from pyanaconda.core.path import make_directories
from pyanaconda.core.regexes import HOSTNAME_PATTERN_WITHOUT_ANCHORS, \
IPV6_ADDRESS_IN_DRACUT_IP_OPTION, MAC_OCTET
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.constants import TIME_SOURCE_SERVER
from pyanaconda.modules.common.constants.services import NETWORK, TIMEZONE, STORAGE
from pyanaconda.modules.common.constants.objects import FCOE
from pyanaconda.modules.common.task import sync_run_task
from pyanaconda.modules.common.structures.network import NetworkDeviceInfo
from pyanaconda.modules.common.structures.timezone import TimeSourceData
from pyanaconda.modules.common.util import is_module_available
import gi
gi.require_version("NM", "1.0")
from gi.repository import NM
log = get_module_logger(__name__)
network_connected = None
network_connected_condition = threading.Condition()
_nm_client = None
__all__ = ["get_supported_devices", "status_message", "wait_for_connectivity",
"wait_for_connecting_NM_thread", "wait_for_network_devices", "wait_for_connected_NM",
"initialize_network", "copy_resolv_conf_to_root", "prefix_to_netmask",
"netmask_to_prefix", "get_first_ip_address", "is_valid_hostname", "check_ip_address",
"get_nm_client", "write_configuration"]
def get_nm_client():
"""Get NetworkManager Client."""
if conf.system.provides_system_bus:
global _nm_client
if not _nm_client:
_nm_client = NM.Client.new(None)
return _nm_client
else:
log.debug("NetworkManager client not available (system does not provide it).")
return None
def check_ip_address(address, version=None):
"""Check if the given IP address is valid in given version if set.
:param str address: IP address for testing
:param int version: ``4`` for IPv4, ``6`` for IPv6 or
``None`` to allow either format
:returns: ``True`` if IP address is valid or ``False`` if not
:rtype: bool
"""
try:
if version == 4:
ipaddress.IPv4Address(address)
elif version == 6:
ipaddress.IPv6Address(address)
elif not version: # any of those
ipaddress.ip_address(address)
else:
log.error("IP version %s is not supported", version)
return False
return True
except ValueError:
return False
def is_valid_hostname(hostname, local=False):
"""Check if the given string is (syntactically) a valid hostname.
:param str hostname: a string to check
:param bool local: is the hostname static (for this system) or not (on the network)
:returns: a pair containing boolean value (valid or invalid) and
an error message (if applicable)
:rtype: (bool, str)
"""
if not hostname:
return (False, _("Host name cannot be None or an empty string."))
if len(hostname) > 64:
return (False, _("Host name must be 64 or fewer characters in length."))
if local and hostname[-1] == ".":
return (False, _("Local host name must not end with period '.'."))
if not re.match('^' + HOSTNAME_PATTERN_WITHOUT_ANCHORS + '$', hostname):
return (False, _("Host names can only contain the characters 'a-z', "
"'A-Z', '0-9', '-', or '.', parts between periods "
"must contain something and cannot start or end with "
"'-'."))
return (True, "")
def get_ip_addresses():
"""Return a list of IP addresses for all active devices."""
ipv4_addresses = []
ipv6_addresses = []
for device in get_activated_devices(get_nm_client()):
ipv4_addresses += get_device_ip_addresses(device, version=4)
ipv6_addresses += get_device_ip_addresses(device, version=6)
# prefer IPv4 addresses to IPv6 addresses
return ipv4_addresses + ipv6_addresses
def get_first_ip_address():
"""Return the first non-local IP of active devices.
:return: IP address assigned to an active device
:rtype: str or None
"""
for ip in get_ip_addresses():
if ip not in ("127.0.0.1", "::1"):
return ip
return None
def netmask_to_prefix(netmask):
""" Convert netmask to prefix (CIDR bits) """
prefix = 0
while prefix < 33:
if prefix_to_netmask(prefix) == netmask:
return prefix
prefix += 1
return prefix
def prefix_to_netmask(prefix):
""" Convert prefix (CIDR bits) to netmask """
_bytes = []
for _i in range(4):
if prefix >= 8:
_bytes.append(255)
prefix -= 8
else:
_bytes.append(256 - 2 ** (8 - prefix))
prefix = 0
netmask = ".".join(str(byte) for byte in _bytes)
return netmask
def hostname_from_cmdline(kernel_args):
"""Get hostname defined by boot options.
:param kernel_args: structure holding installer boot options
:type kernel_args: KernelArguments
"""
# legacy hostname= option
hostname = kernel_args.get('hostname', "")
# ip= option (man dracut.cmdline)
ipopts = kernel_args.get('ip')
# Example (2 options):
# ens3:dhcp 10.34.102.244::10.34.102.54:255.255.255.0:myhostname:ens9:none
if ipopts:
for ipopt in ipopts.split(" "):
if ipopt.startswith("["):
# Replace ipv6 addresses with empty string, example of ipv6 config:
# [fd00:10:100::84:5]::[fd00:10:100::86:49]:80:myhostname:ens9:none
ipopt = IPV6_ADDRESS_IN_DRACUT_IP_OPTION.sub('', ipopt)
elements = ipopt.split(':')
# Hostname can be defined only in option having more than 5 elements.
# But filter out auto ip= with mac address set by MAC_OCTET matching, eg:
# ip=<interface>:dhcp::52:54:00:12:34:56
# where the 4th element is not hostname.
if len(elements) > 5 and not re.match(MAC_OCTET, elements[5]):
hostname = ipopt.split(':')[4]
return hostname
def iface_for_host_ip(host_ip):
"""Get interface used to access given host IP."""
route = util.execWithCapture("ip", ["route", "get", "to", host_ip])
if not route:
log.error("Could not get interface for route to %s", host_ip)
return ""
route_info = route.split()
if route_info[0] != host_ip or len(route_info) < 5 or \
"dev" not in route_info or route_info.index("dev") > 3:
log.error('Unexpected "ip route get to %s" reply: %s', host_ip, route_info)
return ""
return route_info[route_info.index("dev") + 1]
def copy_resolv_conf_to_root(root="/"):
"""Copy resolv.conf to a system root."""
src = "/etc/resolv.conf"
dst = os.path.join(root, src.lstrip('/'))
if not os.path.isfile(src):
log.debug("%s does not exist", src)
return
if os.path.isfile(dst):
log.debug("%s already exists", dst)
return
dst_dir = os.path.dirname(dst)
if not os.path.isdir(dst_dir):
make_directories(dst_dir)
shutil.copyfile(src, dst)
def run_network_initialization_task(task_path):
"""Run network initialization task and log the result."""
task_proxy = NETWORK.get_proxy(task_path)
log.debug("Running task %s", task_proxy.Name)
sync_run_task(task_proxy)
result = get_native(task_proxy.GetResult())
msg = "%s result: %s" % (task_proxy.Name, result)
log.debug(msg)
def initialize_network():
"""Initialize networking."""
if not conf.system.can_configure_network:
return
network_proxy = NETWORK.get_proxy()
msg = "Initialization started."
log.debug(msg)
network_proxy.LogConfigurationState(msg)
log.debug("Devices found: %s",
[dev.device_name for dev in get_supported_devices()])
run_network_initialization_task(network_proxy.ApplyKickstartWithTask())
run_network_initialization_task(network_proxy.DumpMissingConfigFilesWithTask())
if not network_proxy.Hostname:
bootopts_hostname = hostname_from_cmdline(kernel_arguments)
if bootopts_hostname:
log.debug("Updating host name from boot options: %s", bootopts_hostname)
network_proxy.Hostname = bootopts_hostname
# Create device configuration tracking in the module.
# It will be used to generate kickstart from persistent network configuration
# managed by NM (having config files) and updated by NM signals on device
# configuration changes.
log.debug("Creating network configurations.")
network_proxy.CreateDeviceConfigurations()
log.debug("Initialization finished.")
def write_configuration(overwrite=False):
"""Install network configuration to target system."""
fcoe_proxy = STORAGE.get_proxy(FCOE)
fcoe_nics = fcoe_proxy.GetNics()
fcoe_ifaces = [dev.device_name for dev in get_supported_devices()
if dev.device_name in fcoe_nics]
network_proxy = NETWORK.get_proxy()
task_path = network_proxy.ConfigureActivationOnBootWithTask(fcoe_ifaces)
task_proxy = NETWORK.get_proxy(task_path)
sync_run_task(task_proxy)
task_path = network_proxy.InstallNetworkWithTask(overwrite)
task_proxy = NETWORK.get_proxy(task_path)
sync_run_task(task_proxy)
task_path = network_proxy.ConfigureHostnameWithTask(overwrite)
task_proxy = NETWORK.get_proxy(task_path)
sync_run_task(task_proxy)
if conf.system.can_change_hostname:
hostname = network_proxy.Hostname
if hostname:
network_proxy.SetCurrentHostname(hostname)
def _set_ntp_servers_from_dhcp():
"""Set NTP servers of timezone module from dhcp if not set by kickstart."""
# FIXME - do it only if they will be applied (the guard at the end of the function)
if not is_module_available(TIMEZONE):
return
timezone_proxy = TIMEZONE.get_proxy()
ntp_servers = get_ntp_servers_from_dhcp(get_nm_client())
log.info("got %d NTP servers from DHCP", len(ntp_servers))
hostnames = []
for server_address in ntp_servers:
try:
hostname = socket.gethostbyaddr(server_address)[0]
except socket.error:
# getting hostname failed, just use the address returned from DHCP
log.debug("getting NTP server host name failed for address: %s",
server_address)
hostname = server_address
hostnames.append(hostname)
# check if some NTP servers were specified from kickstart
if not timezone_proxy.TimeSources and conf.target.is_hardware:
# no NTP servers were specified, add those from DHCP
servers = []
for hostname in hostnames:
server = TimeSourceData()
server.type = TIME_SOURCE_SERVER
server.hostname = hostname
server.options = ["iburst"]
servers.append(server)
timezone_proxy.TimeSources = \
TimeSourceData.to_structure_list(servers)
def wait_for_connected_NM(timeout=constants.NETWORK_CONNECTION_TIMEOUT, only_connecting=False):
"""Wait for NM being connected.
If only_connecting is set, wait only if NM is in connecting state and
return immediately after leaving this state (regardless of the new state).
Used to wait for dhcp configuration in progress.
:param timeout: timeout in seconds
:type timeout: int
:parm only_connecting: wait only for the result of NM being connecting
:type only_connecting: bool
:return: NM is connected
:rtype: bool
"""
network_proxy = NETWORK.get_proxy()
if network_proxy.Connected:
return True
if only_connecting:
if network_proxy.IsConnecting():
log.debug("waiting for connecting NM (dhcp in progress?), timeout=%d", timeout)
else:
return False
else:
log.debug("waiting for connected NM, timeout=%d", timeout)
i = 0
while i < timeout:
i += constants.NETWORK_CONNECTED_CHECK_INTERVAL
time.sleep(constants.NETWORK_CONNECTED_CHECK_INTERVAL)
if network_proxy.Connected:
log.debug("NM connected, waited %d seconds", i)
return True
elif only_connecting:
if not network_proxy.IsConnecting():
break
log.debug("NM not connected, waited %d seconds", i)
return False
def wait_for_network_devices(devices, timeout=constants.NETWORK_CONNECTION_TIMEOUT):
"""Wait for network devices to be activated with a connection."""
devices = set(devices)
i = 0
log.debug("waiting for connection of devices %s for iscsi", devices)
while i < timeout:
network_proxy = NETWORK.get_proxy()
activated_devices = network_proxy.GetActivatedInterfaces()
if not devices - set(activated_devices):
return True
i += 1
time.sleep(1)
return False
def wait_for_connecting_NM_thread():
"""Wait for connecting NM in thread, do some work and signal connectivity.
This function is called from a thread which is run at startup to wait for
NetworkManager being in connecting state (eg getting IP from DHCP). When NM
leaves connecting state do some actions and signal new state if NM becomes
connected.
"""
connected = wait_for_connected_NM(only_connecting=True)
if connected:
_set_ntp_servers_from_dhcp()
with network_connected_condition:
global network_connected
network_connected = connected
network_connected_condition.notify_all()
def wait_for_connectivity(timeout=constants.NETWORK_CONNECTION_TIMEOUT):
"""Wait for network connectivty to become available
:param timeout: how long to wait in seconds
:type timeout: integer of float"""
connected = False
network_connected_condition.acquire()
# if network_connected is None, network connectivity check
# has not yet been run or is in progress, so wait for it to finish
if network_connected is None:
# wait releases the lock and reacquires it once the thread is unblocked
network_connected_condition.wait(timeout=timeout)
connected = network_connected
# after wait() unblocks, we get the lock back,
# so we need to release it
network_connected_condition.release()
return connected
def get_activated_devices(nm_client):
"""Get activated NetworkManager devices."""
activated_devices = []
if not nm_client:
return activated_devices
for ac in nm_client.get_active_connections():
if ac.get_state() != NM.ActiveConnectionState.ACTIVATED:
continue
for device in ac.get_devices():
activated_devices.append(device)
return activated_devices
def status_message(nm_client):
"""A short string describing which devices are connected."""
msg = _("Unknown")
if not nm_client:
msg = _("Status not available")
return msg
state = nm_client.get_state()
if state == NM.State.CONNECTING:
msg = _("Connecting...")
elif state == NM.State.DISCONNECTING:
msg = _("Disconnecting...")
else:
active_devs = [d for d in get_activated_devices(nm_client)
if not is_libvirt_device(d.get_ip_iface() or d.get_iface())]
if active_devs:
ports = {}
ssids = {}
nonports = []
# first find ports and wireless aps
for device in active_devs:
device_ports = []
if hasattr(device, 'get_slaves'):
device_ports = [port_dev.get_iface() for port_dev in device.get_slaves()]
iface = device.get_iface()
ports[iface] = device_ports
if device.get_device_type() == NM.DeviceType.WIFI:
ssid = ""
ap = device.get_active_access_point()
if ap:
ssid = ap.get_ssid().get_data().decode()
ssids[iface] = ssid
all_ports = set(itertools.chain.from_iterable(ports.values()))
nonports = [dev for dev in active_devs if dev.get_iface() not in all_ports]
if len(nonports) == 1:
device = nonports[0]
iface = device.get_ip_iface() or device.get_iface()
device_type = device.get_device_type()
if device_type_is_supported_wired(device_type):
msg = _("Wired (%(interface_name)s) connected") \
% {"interface_name": iface}
elif device_type == NM.DeviceType.WIFI:
msg = _("Wireless connected to %(access_point)s") \
% {"access_point": ssids[iface]}
elif device_type == NM.DeviceType.BOND:
msg = _("Bond %(interface_name)s (%(list_of_ports)s) connected") \
% {"interface_name": iface,
"list_of_ports": ",".join(ports[iface])}
elif device_type == NM.DeviceType.TEAM:
msg = _("Team %(interface_name)s (%(list_of_ports)s) connected") \
% {"interface_name": iface,
"list_of_ports": ",".join(ports[iface])}
elif device_type == NM.DeviceType.BRIDGE:
msg = _("Bridge %(interface_name)s (%(list_of_ports)s) connected") \
% {"interface_name": iface,
"list_of_ports": ",".join(ports[iface])}
elif device_type == NM.DeviceType.VLAN:
parent = device.get_parent()
vlanid = device.get_vlan_id()
msg = _("VLAN %(interface_name)s (%(parent_device)s, ID %(vlanid)s) connected") \
% {"interface_name": iface, "parent_device": parent, "vlanid": vlanid}
elif len(nonports) > 1:
devlist = []
for device in nonports:
iface = device.get_ip_iface() or device.get_iface()
device_type = device.get_device_type()
if device_type_is_supported_wired(device_type):
devlist.append("%s" % iface)
elif device_type == NM.DeviceType.WIFI:
devlist.append("%s" % ssids[iface])
elif device_type == NM.DeviceType.BOND:
devlist.append("%s (%s)" % (iface, ",".join(ports[iface])))
elif device_type == NM.DeviceType.TEAM:
devlist.append("%s (%s)" % (iface, ",".join(ports[iface])))
elif device_type == NM.DeviceType.BRIDGE:
devlist.append("%s (%s)" % (iface, ",".join(ports[iface])))
elif device_type == NM.DeviceType.VLAN:
devlist.append("%s" % iface)
msg = _("Connected: %(list_of_interface_names)s") % {"list_of_interface_names": ", ".join(devlist)}
else:
msg = _("Not connected")
if not get_supported_devices():
msg = _("No network devices available")
return msg
def get_supported_devices():
"""Get existing network devices supported by the installer.
:return: basic information about the devices
:rtype: list(NetworkDeviceInfo)
"""
network_proxy = NETWORK.get_proxy()
return NetworkDeviceInfo.from_structure_list(network_proxy.GetSupportedDevices())
def get_ntp_servers_from_dhcp(nm_client):
"""Return IPs of NTP servers obtained by DHCP.
:param nm_client: instance of NetworkManager client
:type nm_client: NM.Client
:return: IPs of NTP servers obtained by DHCP
:rtype: list of str
"""
ntp_servers = []
if not nm_client:
return ntp_servers
for device in get_activated_devices(nm_client):
dhcp4_config = device.get_dhcp4_config()
if dhcp4_config:
options = dhcp4_config.get_options()
ntp_servers_string = options.get("ntp_servers")
if ntp_servers_string:
ntp_servers.extend(ntp_servers_string.split(" "))
# NetworkManager does not request NTP/SNTP options for DHCP6
return ntp_servers
def get_device_ip_addresses(device, version=4):
"""Get IP addresses of the device.
Ignores ipv6 link-local addresses.
:param device: NetworkManager device object
:type device: NMDevice
:param version: IP version (4 or 6)
:type version: int
"""
addresses = []
if version == 4:
ipv4_config = device.get_ip4_config()
if ipv4_config:
addresses = [addr.get_address() for addr in ipv4_config.get_addresses()]
elif version == 6:
ipv6_config = device.get_ip6_config()
if ipv6_config:
all_addresses = [addr.get_address() for addr in ipv6_config.get_addresses()]
addresses = [addr for addr in all_addresses
if not addr.startswith("fe80:")]
return addresses
def is_libvirt_device(iface):
return iface and iface.startswith("virbr")
def device_type_is_supported_wired(device_type):
return device_type in [NM.DeviceType.ETHERNET, NM.DeviceType.INFINIBAND]