From 3986e21992e8881938b89c1b21c21556ce18a066 Mon Sep 17 00:00:00 2001 From: Chris Patterson Date: Mon, 24 Jan 2022 08:58:31 -0500 Subject: [PATCH] net: introduce find_candidate_nics() find_fallback_nic_on_linux(), etc. provides valuable filtering of network interfaces in an effort to determine the best candidate for the fallback interface. Expose this logic with a new set of methods for finding the candidate network interfaces. These methods can be used by data sources which cannot rely on the fallback interface being the correct choice. Note that the MAC address filtering is now part of find_candidate_nics_on_linux(). This should be consistent behavior as find_fallback_nic_on_linux() never selected an interface without a MAC. find_fallback_nic_on_linux() continues to prefer eth0, but we make no such distinction in the candidate search. Specifically, for Linux, order the NICs: 1. DEFAULT_PRIMARY_INTERFACE, if connected. 2. Remaining connected interfaces, naturally sorted. 3. DEFAULT_PRIMARY_INTERFACE, if possibly connected. 4. Remaining possibly connected interfaces, naturally sorted. This way the fallback calculation is consistent with previous behavior, but we don't ignore possibly connected interfaces just because others may be connected. For non-Linux, continue to sort interfaces naturally. Signed-off-by: Chris Patterson --- cloudinit/net/__init__.py | 144 +++++++++++++++++++++++++++----------- 1 file changed, 104 insertions(+), 40 deletions(-) diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index 1a738dbcf7f..be9df85fb13 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -11,7 +11,7 @@ import logging import os import re -from typing import Any, Dict +from typing import Any, Dict, List, Optional from cloudinit import subp, util from cloudinit.net.network_state import mask_to_net_prefix @@ -389,8 +389,23 @@ def is_disabled_cfg(cfg): return cfg.get("config") == "disabled" -def find_fallback_nic(blacklist_drivers=None): - """Return the name of the 'fallback' network device.""" +def find_candidate_nics(blacklist_drivers: Optional[List] = None) -> List[str]: + """Get the list of network interfaces viable for networking. + + @return List of interfaces, sorted naturally. + """ + if util.is_FreeBSD() or util.is_DragonFlyBSD(): + return find_candidate_nics_on_freebsd(blacklist_drivers) + elif util.is_NetBSD() or util.is_OpenBSD(): + return find_candidate_nics_on_netbsd_or_openbsd(blacklist_drivers) + else: + return find_candidate_nics_on_linux(blacklist_drivers) + + +def find_fallback_nic( + blacklist_drivers: Optional[List] = None, +) -> Optional[str]: + """Get the name of the 'fallback' network device.""" if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_fallback_nic_on_freebsd(blacklist_drivers) elif util.is_NetBSD() or util.is_OpenBSD(): @@ -399,37 +414,73 @@ def find_fallback_nic(blacklist_drivers=None): return find_fallback_nic_on_linux(blacklist_drivers) -def find_fallback_nic_on_netbsd_or_openbsd(blacklist_drivers=None): - values = list( - sorted(get_interfaces_by_mac().values(), key=natural_sort_key) - ) - if values: - return values[0] +def find_candidate_nics_on_netbsd_or_openbsd( + blacklist_drivers: Optional[List] = None, +) -> List[str]: + """Get the names of the candidate network devices on NetBSD/OpenBSD. + + @param blacklist_drivers: currently ignored + @return list of sorted interfaces + """ + return sorted(get_interfaces_by_mac().values(), key=natural_sort_key) -def find_fallback_nic_on_freebsd(blacklist_drivers=None): - """Return the name of the 'fallback' network device on FreeBSD. +def find_fallback_nic_on_netbsd_or_openbsd( + blacklist_drivers: Optional[List] = None, +) -> Optional[str]: + """Get the 'fallback' network device name on NetBSD/OpenBSD. @param blacklist_drivers: currently ignored @return default interface, or None + """ + names = find_candidate_nics_on_netbsd_or_openbsd(blacklist_drivers) + if names: + return names[0] + + return None - we'll use the first interface from ``ifconfig -l -u ether`` +def find_candidate_nics_on_freebsd( + blacklist_drivers: Optional[List] = None, +) -> List[str]: + """Get the names of the candidate network devices on FreeBSD. + + @param blacklist_drivers: Currently ignored. + @return List of sorted interfaces. """ stdout, _stderr = subp.subp(["ifconfig", "-l", "-u", "ether"]) values = stdout.split() if values: - return values[0] + return values + # On FreeBSD <= 10, 'ifconfig -l' ignores the interfaces with DOWN # status - values = list(get_interfaces_by_mac().values()) - values.sort() - if values: - return values[0] + return sorted(get_interfaces_by_mac().values(), key=natural_sort_key) + +def find_fallback_nic_on_freebsd( + blacklist_drivers: Optional[List] = None, +) -> Optional[str]: + """Get the 'fallback' network device name on FreeBSD. + + @param blacklist_drivers: Currently ignored. + @return List of sorted interfaces. + """ + names = find_candidate_nics_on_freebsd(blacklist_drivers) + if names: + return names[0] + + return None -def find_fallback_nic_on_linux(blacklist_drivers=None): - """Return the name of the 'fallback' network device on Linux.""" + +def find_candidate_nics_on_linux( + blacklist_drivers: Optional[List] = None, +) -> List[str]: + """Get the names of the candidate network devices on Linux. + + @param blacklist_drivers: Filter out NICs with these drivers. + @return List of sorted interfaces. + """ if not blacklist_drivers: blacklist_drivers = [] @@ -464,6 +515,9 @@ def find_fallback_nic_on_linux(blacklist_drivers=None): connected = [] possibly_connected = [] for interface in potential_interfaces: + if not read_sys_net_safe(interface, "address"): + # skip interfaces without MAC addresses + continue if interface.startswith("veth"): continue if is_bridge(interface): @@ -491,24 +545,34 @@ def find_fallback_nic_on_linux(blacklist_drivers=None): possibly_connected.append(interface) continue - # don't bother with interfaces that might not be connected if there are - # some that definitely are - if connected: - potential_interfaces = connected - else: - potential_interfaces = possibly_connected - - # if eth0 exists use it above anything else, otherwise get the interface - # that we can read 'first' (using the sorted definition of first). - names = list(sorted(potential_interfaces, key=natural_sort_key)) - if DEFAULT_PRIMARY_INTERFACE in names: - names.remove(DEFAULT_PRIMARY_INTERFACE) - names.insert(0, DEFAULT_PRIMARY_INTERFACE) - - # pick the first that has a mac-address - for name in names: - if read_sys_net_safe(name, "address"): - return name + # Order the NICs: + # 1. DEFAULT_PRIMARY_INTERFACE, if connected. + # 2. Remaining connected interfaces, naturally sorted. + # 3. DEFAULT_PRIMARY_INTERFACE, if possibly connected. + # 4. Remaining possibly connected interfaces, naturally sorted. + sorted_interfaces = [] + for interfaces in [connected, possibly_connected]: + interfaces = sorted(interfaces, key=natural_sort_key) + if DEFAULT_PRIMARY_INTERFACE in interfaces: + interfaces.remove(DEFAULT_PRIMARY_INTERFACE) + interfaces.insert(0, DEFAULT_PRIMARY_INTERFACE) + sorted_interfaces += interfaces + + return sorted_interfaces + + +def find_fallback_nic_on_linux( + blacklist_drivers: Optional[List] = None, +) -> Optional[str]: + """Get the 'fallback' network device name on Linux. + + @param blacklist_drivers: Ignore devices with these drivers. + @return List of sorted interfaces. + """ + names = find_candidate_nics_on_linux(blacklist_drivers) + if names: + return names[0] + return None @@ -872,7 +936,7 @@ def get_interfaces_by_mac(blacklist_drivers=None) -> dict: ) -def get_interfaces_by_mac_on_freebsd(blacklist_drivers=None) -> dict(): +def get_interfaces_by_mac_on_freebsd(blacklist_drivers=None) -> dict: (out, _) = subp.subp(["ifconfig", "-a", "ether"]) # flatten each interface block in a single line @@ -900,7 +964,7 @@ def find_mac(flat_list): return results -def get_interfaces_by_mac_on_netbsd(blacklist_drivers=None) -> dict(): +def get_interfaces_by_mac_on_netbsd(blacklist_drivers=None) -> dict: ret = {} re_field_match = ( r"(?P\w+).*address:\s" @@ -916,7 +980,7 @@ def get_interfaces_by_mac_on_netbsd(blacklist_drivers=None) -> dict(): return ret -def get_interfaces_by_mac_on_openbsd(blacklist_drivers=None) -> dict(): +def get_interfaces_by_mac_on_openbsd(blacklist_drivers=None) -> dict: ret = {} re_field_match = ( r"(?P\w+).*lladdr\s"