diff options
author | John Estabrook <jestabro@vyos.io> | 2021-09-30 12:56:22 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-30 12:56:22 -0500 |
commit | ee5af5ddc65333c76baa498d6131f69073e39a7e (patch) | |
tree | e755f3e982c8fd33fd50b2778da8e082169504cf | |
parent | 17dc7cd0aaca5c4ae14d3dc843de7a5b612ab5ed (diff) | |
parent | bfc5d6a1a48578a47369bde40388ec02bcedfa33 (diff) | |
download | vyos-1x-ee5af5ddc65333c76baa498d6131f69073e39a7e.tar.gz vyos-1x-ee5af5ddc65333c76baa498d6131f69073e39a7e.zip |
Merge pull request #1019 from jestabro/interface-names
T3869: Rewrite vyatta_net_name/vyatta_interface_rescan in Python
-rwxr-xr-x | debian/rules | 4 | ||||
-rw-r--r-- | python/vyos/defaults.py | 4 | ||||
-rw-r--r-- | src/etc/udev/rules.d/65-vyatta-net.rules | 26 | ||||
-rw-r--r-- | src/etc/udev/rules.d/65-vyos-net.rules | 26 | ||||
-rwxr-xr-x | src/helpers/vyos-interface-rescan.py | 206 | ||||
-rwxr-xr-x | src/helpers/vyos_net_name | 229 |
6 files changed, 467 insertions, 28 deletions
diff --git a/debian/rules b/debian/rules index c7a7138e1..5a58aeeb6 100755 --- a/debian/rules +++ b/debian/rules @@ -120,6 +120,10 @@ override_dh_auto_install: mkdir -p $(DIR)/$(VYOS_BIN_DIR) cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR) + # Install udev script + mkdir -p $(DIR)/usr/lib/udev + cp src/helpers/vyos_net_name $(DIR)/usr/lib/udev + ifeq ($(DEB_TARGET_ARCH),amd64) # We only install XDP on amd64 systems mkdir -p $(DIR)/$(VYOS_DATA_DIR)/xdp diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index dacdbdef2..00b14a985 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -25,8 +25,8 @@ directories = { "templates": "/usr/share/vyos/templates/", "certbot": "/config/auth/letsencrypt", "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", - "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/" - + "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/", + "vyos_udev_dir": "/run/udev/vyos" } cfg_group = 'vyattacfg' diff --git a/src/etc/udev/rules.d/65-vyatta-net.rules b/src/etc/udev/rules.d/65-vyatta-net.rules deleted file mode 100644 index 2b48c1213..000000000 --- a/src/etc/udev/rules.d/65-vyatta-net.rules +++ /dev/null @@ -1,26 +0,0 @@ -# These rules use vyatta_net_name to persistently name network interfaces -# per "hwid" association in the Vyatta configuration file. - -ACTION!="add", GOTO="vyatta_net_end" -SUBSYSTEM!="net", GOTO="vyatta_net_end" - -# ignore the interface if a name has already been set -NAME=="?*", GOTO="vyatta_net_end" - -# Do name change for ethernet and wireless devices only -KERNEL!="eth*|wlan*", GOTO="vyatta_net_end" - -# ignore "secondary" monitor interfaces of mac80211 drivers -KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyatta_net_end" - -# If using VyOS predefined names -ENV{VYOS_IFNAME}!="eth*", GOTO="end_vyos_predef_names" - -DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address} $env{VYOS_IFNAME}", NAME="%c", GOTO="vyatta_net_end" - -LABEL="end_vyos_predef_names" - -# ignore interfaces without a driver link like bridges and VLANs -DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address}", NAME="%c" - -LABEL="vyatta_net_end" diff --git a/src/etc/udev/rules.d/65-vyos-net.rules b/src/etc/udev/rules.d/65-vyos-net.rules new file mode 100644 index 000000000..c8d5750dd --- /dev/null +++ b/src/etc/udev/rules.d/65-vyos-net.rules @@ -0,0 +1,26 @@ +# These rules use vyos_net_name to persistently name network interfaces +# per "hwid" association in the VyOS configuration file. + +ACTION!="add", GOTO="vyos_net_end" +SUBSYSTEM!="net", GOTO="vyos_net_end" + +# ignore the interface if a name has already been set +NAME=="?*", GOTO="vyos_net_end" + +# Do name change for ethernet and wireless devices only +KERNEL!="eth*|wlan*", GOTO="vyos_net_end" + +# ignore "secondary" monitor interfaces of mac80211 drivers +KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyos_net_end" + +# If using VyOS predefined names +ENV{VYOS_IFNAME}!="eth*", GOTO="end_vyos_predef_names" + +DRIVERS=="?*", PROGRAM="vyos_net_name %k $attr{address} $env{VYOS_IFNAME}", NAME="%c", GOTO="vyos_net_end" + +LABEL="end_vyos_predef_names" + +# ignore interfaces without a driver link like bridges and VLANs +DRIVERS=="?*", PROGRAM="vyos_net_name %k $attr{address}", NAME="%c" + +LABEL="vyos_net_end" diff --git a/src/helpers/vyos-interface-rescan.py b/src/helpers/vyos-interface-rescan.py new file mode 100755 index 000000000..1ac1810e0 --- /dev/null +++ b/src/helpers/vyos-interface-rescan.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. +# +# + +import os +import stat +import argparse +import logging +import netaddr + +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.util import get_cfg_group_id + +debug = False + +vyos_udev_dir = directories['vyos_udev_dir'] +vyos_log_dir = directories['log'] +log_file = os.path.splitext(os.path.basename(__file__))[0] +vyos_log_file = os.path.join(vyos_log_dir, log_file) + +logger = logging.getLogger(__name__) +handler = logging.FileHandler(vyos_log_file, mode='a') +formatter = logging.Formatter('%(levelname)s: %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +passlist = { + '02:07:01' : 'Interlan', + '02:60:60' : '3Com', + '02:60:8c' : '3Com', + '02:a0:c9' : 'Intel', + '02:aa:3c' : 'Olivetti', + '02:cf:1f' : 'CMC', + '02:e0:3b' : 'Prominet', + '02:e6:d3' : 'BTI', + '52:54:00' : 'Realtek', + '52:54:4c' : 'Novell 2000', + '52:54:ab' : 'Realtec', + 'e2:0c:0f' : 'Kingston Technologies' +} + +def is_multicast(addr: netaddr.eui.EUI) -> bool: + return bool(addr.words[0] & 0b1) + +def is_locally_administered(addr: netaddr.eui.EUI) -> bool: + return bool(addr.words[0] & 0b10) + +def is_on_passlist(hwid: str) -> bool: + top = hwid.rsplit(':', 3)[0] + if top in list(passlist): + return True + return False + +def is_persistent(hwid: str) -> bool: + addr = netaddr.EUI(hwid) + if is_multicast(addr): + return False + if is_locally_administered(addr) and not is_on_passlist(hwid): + return False + return True + +def get_wireless_physical_device(intf: str) -> str: + if 'wlan' not in intf: + return '' + try: + tmp = os.readlink(f'/sys/class/net/{intf}/phy80211') + except OSError: + logger.critical(f"Failed to read '/sys/class/net/{intf}/phy80211'") + return '' + phy = os.path.basename(tmp) + logger.info(f"wireless phy is {phy}") + return phy + +def get_interface_type(intf: str) -> str: + if 'eth' in intf: + intf_type = 'ethernet' + elif 'wlan' in intf: + intf_type = 'wireless' + else: + logger.critical('Unrecognized interface type!') + intf_type = '' + return intf_type + +def get_new_interfaces() -> dict: + """ Read any new interface data left in /run/udev/vyos by vyos_net_name + """ + interfaces = {} + + 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 + interfaces[intf] = hwid + + # reverse sort to simplify insertion in config + interfaces = {key: value for key, value in sorted(interfaces.items(), + reverse=True)} + return interfaces + +def filter_interfaces(intfs: dict) -> dict: + """ Ignore no longer existing interfaces or non-persistent mac addresses + """ + filtered = {} + + for intf, hwid in intfs.items(): + if not os.path.isdir(os.path.join('/sys/class/net', intf)): + continue + if not is_persistent(hwid): + continue + filtered[intf] = hwid + + return filtered + +def interface_rescan(config_path: str): + """ Read new data and update config file + """ + interfaces = get_new_interfaces() + + logger.debug(f"interfaces from udev: {interfaces}") + + interfaces = filter_interfaces(interfaces) + + logger.debug(f"filtered interfaces: {interfaces}") + + try: + with open(config_path) as f: + config_file = f.read() + except OSError as e: + logger.critical(f"OSError {e}") + exit(1) + + config = ConfigTree(config_file) + + for intf, hwid in interfaces.items(): + logger.info(f"Writing '{intf}' '{hwid}' to config file") + intf_type = get_interface_type(intf) + if not intf_type: + continue + if not config.exists(['interfaces', intf_type]): + config.set(['interfaces', intf_type]) + config.set_tag(['interfaces', intf_type]) + config.set(['interfaces', intf_type, intf, 'hw-id'], value=hwid) + + if intf_type == 'wireless': + phy = get_wireless_physical_device(intf) + if not phy: + continue + config.set(['interfaces', intf_type, intf, 'physical-device'], + value=phy) + + try: + with open(config_path, 'w') as f: + f.write(config.to_string()) + except OSError as e: + logger.critical(f"OSError {e}") + +def main(): + global debug + + argparser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter) + argparser.add_argument('configfile', type=str) + argparser.add_argument('--debug', action='store_true') + args = argparser.parse_args() + + if args.debug: + debug = True + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + configfile = args.configfile + + # preserve vyattacfg group write access to running config + os.setgid(get_cfg_group_id()) + os.umask(0o002) + + # log file perms are not automatic; this could be cleaner by moving to a + # logging config file + os.chown(vyos_log_file, 0, get_cfg_group_id()) + os.chmod(vyos_log_file, + stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH) + + interface_rescan(configfile) + +if __name__ == '__main__': + main() diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name new file mode 100755 index 000000000..0652e98b1 --- /dev/null +++ b/src/helpers/vyos_net_name @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. +# +# + +import os +import re +import time +import logging +import threading +from sys import argv + +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.util import cmd + +vyos_udev_dir = directories['vyos_udev_dir'] +vyos_log_dir = '/run/udev/log' +vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name') + +config_path = '/opt/vyatta/etc/config/config.boot' +config_status = '/tmp/vyos-config-status' + +lock = threading.Lock() + +try: + os.mkdir(vyos_log_dir) +except FileExistsError: + pass + +logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG) + +def boot_configuration_complete() -> bool: + """ Check if vyos-router has completed, hence hotplug event + """ + if os.path.isfile(config_status): + return True + return False + +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 iterface name that is not assigned + """ + index_list = [int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x] + 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}{len(index_list)}' + +def get_biosdevname(ifname: str) -> str: + """ Use legacy vyatta-biosdevname to query for name + + This is carried over for compatability 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. + """ + if 'eth' not in ifname: + return ifname + if os.path.isdir('/proc/xen'): + return ifname + + time.sleep(1) + + try: + biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}') + except Exception as e: + logging.error(f'biosdevname error: {e}') + biosname = '' + + return ifname 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: + logging.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: + logging.critical(f"OSError {e}") + +def get_configfile_interfaces() -> dict: + """Read existing interfaces from config file + """ + interfaces: dict = {} + + if not os.path.isfile(config_path): + # If the case, then we are running off of livecd; return empty + return interfaces + + try: + with open(config_path) as f: + config_file = f.read() + except OSError as e: + logging.critical(f"OSError {e}") + exit(1) + + config = ConfigTree(config_file) + + base = ['interfaces', 'ethernet'] + if config.exists(base): + eth_intfs = config.list_nodes(base) + for intf in eth_intfs: + path = base + [intf, 'hw-id'] + if not config.exists(path): + logging.warning(f"no 'hw-id' entry for {intf}") + continue + hwid = config.return_value(path) + if hwid in list(interfaces): + logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}") + continue + interfaces[hwid] = intf + + base = ['interfaces', 'wireless'] + if config.exists(base): + wlan_intfs = config.list_nodes(base) + for intf in wlan_intfs: + path = base + [intf, 'hw-id'] + if not config.exists(path): + logging.warning(f"no 'hw-id' entry for {intf}") + continue + hwid = config.return_value(path) + if hwid in list(interfaces): + logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}") + continue + interfaces[hwid] = intf + + logging.debug(f"config file entries: {interfaces}") + + return interfaces + +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: + logging.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 + """ + logging.info(f"lookup {intf_name}, {hwid}") + interfaces = get_configfile_interfaces() + logging.debug(f"config file interfaces are {interfaces}") + + if hwid in list(interfaces) and intf_name == interfaces[hwid]: + logging.info(f"use mapping from config file: '{hwid}' -> '{intf_name}'") + return intf_name + + add_assigned_interfaces(interfaces) + logging.debug(f"adding assigned interfaces: {interfaces}") + + if predefined: + newname = predefined + logging.info(f"predefined interface name for '{intf_name}' is '{newname}'") + else: + newname = get_biosdevname(intf_name) + logging.info(f"biosdevname returned '{newname}' for '{intf_name}'") + + if not is_available(interfaces, newname): + prefix = re.sub(r'\d+$', '', newname) + newname = find_available(interfaces, prefix) + + logging.info(f"new name for '{intf_name}' is '{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 len(argv) > 3: + predef_name = argv[3] +else: + predef_name = '' + +lock.acquire() +if not boot_configuration_complete(): + res = on_boot_event(argv[1], argv[2], predefined=predef_name) + logging.debug(f"on boot, returned name is {res}") +else: + logging.debug("boot configuration complete") +lock.release() + |