#!/usr/bin/env python3 # # Copyright VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # 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 . import os import re import time import logging import logging.handlers from pathlib import Path from sys import argv from vyos.defaults import directories from vyos.utils.process import cmdl from vyos.utils.boot import boot_configuration_complete from vyos.utils.locking import Lock # Define variables vyos_udev_dir = directories['vyos_udev_dir'] # NOTE: this script only picks a provisional name for a physical interface # as it is discovered by udev - it is a best-effort placeholder and does # NOT look at 'hw-id' in config.boot. The authoritative, hw-id based name # (if one is configured) is applied afterwards, once, by # vyos-net-name-resolve.py (src/system/vyos-net-name-resolve.py), which # runs after all hardware has settled. Deciding hw-id names here, from a # single per-uevent sysfs snapshot, is what caused interface names to be # unreliable on systems with multiple NIC vendors/drivers: the DRIVERS # attribute and the interface's permanent MAC address are not guaranteed # to be populated yet at the moment a single "add" uevent is evaluated, # and there is no second chance to re-evaluate it. def is_available(intfs: dict, intf_name: str) -> bool: """Check if interface name is already assigned""" if intf_name in list(intfs.values()): return False return True def find_available(intfs: dict, prefix: str) -> str: """Find lowest indexed interface name that is not assigned""" index_list = [] for x in intfs.values(): if not x.startswith(prefix): continue suffix = x[len(prefix):] if suffix.isdigit(): index_list.append(int(suffix)) if not index_list: return f'{prefix}0' index_list.sort() # find 'holes' in list, if any missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list)) if missing: return f'{prefix}{missing[0]}' return f'{prefix}{index_list[-1] + 1}' def mod_ifname(ifname: str) -> str: """Check interface with names eX and return ifname on the next format eth{ifindex} - 2""" if re.match('^e[0-9]+$', ifname): intf = ifname.split('e') if intf[1]: if int(intf[1]) >= 2: return 'eth' + str(int(intf[1]) - 2) else: return 'eth' + str(intf[1]) return ifname def get_biosdevname(ifname: str) -> str: """Use legacy vyatta-biosdevname to query for name This is carried over for compatibility only, and will likely be dropped going forward. XXX: This throws an error, and likely has for a long time, unnoticed since vyatta_net_name redirected stderr to /dev/null. """ intf = mod_ifname(ifname) if 'eth' not in intf: return intf if os.path.isdir('/proc/xen'): return intf time.sleep(1) try: biosname = cmdl(['/sbin/biosdevname', '--policy', 'all_ethN', '-i', ifname]) except Exception as e: # expected/harmless on most virtualized and some bare-metal # systems - see the XXX above; the caller already falls back to # mod_ifname()'s guess, so this isn't worth more than a debug note logger.debug(f'biosdevname unavailable for {ifname}: {e}') biosname = '' return intf if biosname == '' else biosname def leave_rescan_hint(intf_name: str, hwid: str): """Write interface information reported by udev This script is called while the root mount is still read-only. Leave information in /run/udev: file name, the interface; contents, the hardware id. """ try: os.mkdir(vyos_udev_dir) except FileExistsError: pass except Exception as e: logger.critical(f'Error creating rescan hint directory: {e}') exit(1) try: with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f: f.write(hwid) except OSError as e: logger.critical(f'OSError {e}') def add_assigned_interfaces(intfs: dict): """Add interfaces found by previous invocation of udev rule""" if not os.path.isdir(vyos_udev_dir): return for intf in os.listdir(vyos_udev_dir): path = os.path.join(vyos_udev_dir, intf) try: with open(path) as f: hwid = f.read().rstrip() except OSError as e: logger.error(f'OSError {e}') continue intfs[hwid] = intf def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str: """Called on boot by vyos-router: 'coldplug' in vyatta_net_name Picks a provisional name only - see the module docstring. hw-id from config.boot is deliberately NOT consulted here. """ interfaces: dict = {} add_assigned_interfaces(interfaces) logger.debug(f'{intf_name} ({hwid}): already assigned this boot: {interfaces}') if predefined: newname = predefined else: newname = get_biosdevname(intf_name) if not is_available(interfaces, newname): prefix = re.sub(r'\d+$', '', newname) newname = find_available(interfaces, prefix) logger.info(f"{intf_name} ({hwid}) -> '{newname}'") leave_rescan_hint(newname, hwid) return newname def hotplug_event(): # Not yet implemented, since interface-rescan will only be run on boot. pass if __name__ == '__main__': # Set up logging to syslog syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') formatter = logging.Formatter(f'{Path(__file__).name}: %(message)s') syslog_handler.setFormatter(formatter) logger = logging.getLogger() logger.addHandler(syslog_handler) logger.setLevel(logging.INFO) logger.debug(f'started with arguments: {argv}') if len(argv) > 3: predef_name = argv[3] else: predef_name = '' lock = Lock('vyos_net_name') # Wait 60 seconds for other running scripts to finish lock.acquire(60) if not boot_configuration_complete(): print(on_boot_event(argv[1], argv[2], predefined=predef_name)) lock.release()