#!/usr/bin/env python3 # # Copyright (C) 2019-2020 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 from copy import deepcopy from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data from vyos.config import Config from vyos.util import call, cmd from vyos.validate import is_member, has_address_configured from vyos import ConfigError from vyos import airbag airbag.enable() default_config_data = { **interface_default_data, 'arp_mon_intvl': 0, 'arp_mon_tgt': [], 'deleted': False, 'hash_policy': 'layer2', 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_proxy_arp_pvlan': 0, 'mode': '802.3ad', 'member': [], 'shutdown_required': False, 'primary': '', 'vif_s': {}, 'vif_s_remove': [], 'vif': {}, 'vif_remove': [], } def get_bond_mode(mode): if mode == 'round-robin': return 'balance-rr' elif mode == 'active-backup': return 'active-backup' elif mode == 'xor-hash': return 'balance-xor' elif mode == 'broadcast': return 'broadcast' elif mode == '802.3ad': return '802.3ad' elif mode == 'transmit-load-balance': return 'balance-tlb' elif mode == 'adaptive-load-balance': return 'balance-alb' else: raise ConfigError('invalid bond mode "{}"'.format(mode)) def get_config(): # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') ifname = os.environ['VYOS_TAGNODE_VALUE'] conf = Config() # initialize kernel module if not loaded if not os.path.isfile('/sys/class/net/bonding_masters'): import syslog syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") if call('modprobe bonding max_bonds=0 miimon=250') != 0: syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") raise ConfigError("failed loading bonding kernel module") # check if bond has been removed cfg_base = 'interfaces bonding ' + ifname if not conf.exists(cfg_base): bond = deepcopy(default_config_data) bond['intf'] = ifname bond['deleted'] = True return bond # set new configuration level conf.set_level(cfg_base) bond, disabled = intf_to_dict(conf, default_config_data) # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) # IP address to use for ARP monitoring if conf.exists('arp-monitor target'): bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') # Bonding transmit hash policy if conf.exists('hash-policy'): bond['hash_policy'] = conf.return_value('hash-policy') # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Enable private VLAN proxy ARP on this interface if conf.exists('ip proxy-arp-pvlan'): bond['ip_proxy_arp_pvlan'] = 1 # Bonding mode if conf.exists('mode'): act_mode = conf.return_value('mode') eff_mode = conf.return_effective_value('mode') if not (act_mode == eff_mode): bond['shutdown_required'] = True bond['mode'] = get_bond_mode(act_mode) # determine bond member interfaces (currently configured) bond['member'] = conf.return_values('member interface') # We can not call conf.return_effective_values() as it would not work # on reboots. Reboots/First boot will return that running config and # saved config is the same, thus on a reboot the bond members will # not be added all (https://phabricator.vyos.net/T2030) live_members = BondIf(bond['intf']).get_slaves() if not (bond['member'] == live_members): bond['shutdown_required'] = True # Primary device interface if conf.exists('primary'): bond['primary'] = conf.return_value('primary') add_to_dict(conf, disabled, bond, 'vif', 'vif') add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s') return bond def verify(bond): if bond['deleted']: if bond['is_bridge_member']: raise ConfigError(( f'Cannot delete interface "{bond["intf"]}" as it is a ' f'member of bridge "{bond["is_bridge_member"]}"!')) return None if len(bond['arp_mon_tgt']) > 16: raise ConfigError('The maximum number of arp-monitor targets is 16') if bond['primary']: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError(( 'Mode dependency failed, primary not supported in mode ' f'"{bond["mode"]}"!')) if ( bond['is_bridge_member'] and ( bond['address'] or bond['ipv6_eui64_prefix'] or bond['ipv6_autoconf'] ) ): raise ConfigError(( f'Cannot assign address to interface "{bond["intf"]}" ' f'as it is a member of bridge "{bond["is_bridge_member"]}"!')) if bond['vrf']: if bond['vrf'] not in interfaces(): raise ConfigError(f'VRF "{bond["vrf"]}" does not exist') if bond['is_bridge_member']: raise ConfigError(( f'Interface "{bond["intf"]}" cannot be member of VRF ' f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} ' f'at the same time!')) # use common function to verify VLAN configuration verify_vlan_config(bond) conf = Config() for intf in bond['member']: # check if member interface is "real" if intf not in interfaces(): raise ConfigError(f'Interface {intf} does not exist!') # a bonding member interface is only allowed to be assigned to one bond! all_bonds = conf.list_nodes('interfaces bonding') # We do not need to check our own bond all_bonds.remove(bond['intf']) for tmp in all_bonds: if conf.exists('interfaces bonding {tmp} member interface {intf}'): raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it is already a member of bond "{tmp}"!')) # can not add interfaces with an assigned address to a bond if has_address_configured(conf, intf): raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it has an address assigned!')) # bond members are not allowed to be bridge members tmp = is_member(conf, intf, 'bridge') if tmp: raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it is already a member of bridge "{tmp}"!')) # bond members are not allowed to be vrrp members for tmp in conf.list_nodes('high-availability vrrp group'): if conf.exists('high-availability vrrp group {tmp} interface {intf}'): raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it is already a member of VRRP group "{tmp}"!')) # bond members are not allowed to be underlaying psuedo-ethernet devices for tmp in conf.list_nodes('interfaces pseudo-ethernet'): if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'): raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it is already the link of pseudo-ethernet "{tmp}"!')) # bond members are not allowed to be underlaying vxlan devices for tmp in conf.list_nodes('interfaces vxlan'): if conf.exists('interfaces vxlan {tmp} link {intf}'): raise ConfigError(( f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' f'it is already the link of VXLAN "{tmp}"!')) if bond['primary']: if bond['primary'] not in bond['member']: raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member') if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('primary interface only works for mode active-backup, ' \ 'transmit-load-balance or adaptive-load-balance') if bond['arp_mon_intvl'] > 0: if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ 'transmit-load-balance or adaptive-load-balance') return None def generate(bond): return None def apply(bond): b = BondIf(bond['intf']) if bond['deleted']: # delete interface b.remove() else: # ARP link monitoring frequency, reset miimon when arp-montior is inactive # this is done inside BondIf automatically b.set_arp_interval(bond['arp_mon_intvl']) # ARP monitor targets need to be synchronized between sysfs and CLI. # Unfortunately an address can't be send twice to sysfs as this will # result in the following exception: OSError: [Errno 22] Invalid argument. # # We remove ALL adresses prior adding new ones, this will remove addresses # added manually by the user too - but as we are limited to 16 adresses # from the kernel side this looks valid to me. We won't run into an error # when a user added manual adresses which would result in having more # then 16 adresses in total. arp_tgt_addr = list(map(str, b.get_arp_ip_target().split())) for addr in arp_tgt_addr: b.set_arp_ip_target('-' + addr) # Add configured ARP target addresses for addr in bond['arp_mon_tgt']: b.set_arp_ip_target('+' + addr) # update interface description used e.g. within SNMP b.set_alias(bond['description']) if bond['dhcp_client_id']: b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] if bond['dhcp_hostname']: b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] if bond['dhcp_vendor_class_id']: b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] if bond['dhcpv6_prm_only']: b.dhcp.v6.options['dhcpv6_prm_only'] = True if bond['dhcpv6_temporary']: b.dhcp.v6.options['dhcpv6_temporary'] = True if bond['dhcpv6_pd_length']: b.dhcp.v6.options['dhcpv6_pd_length'] = bond['dhcpv6_pd_length'] if bond['dhcpv6_pd_interfaces']: b.dhcp.v6.options['dhcpv6_pd_interfaces'] = bond['dhcpv6_pd_interfaces'] # ignore link state changes b.set_link_detect(bond['disable_link_detect']) # Bonding transmit hash policy b.set_hash_policy(bond['hash_policy']) # configure ARP cache timeout in milliseconds b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) # configure ARP filter configuration b.set_arp_filter(bond['ip_disable_arp_filter']) # configure ARP accept b.set_arp_accept(bond['ip_enable_arp_accept']) # configure ARP announce b.set_arp_announce(bond['ip_enable_arp_announce']) # configure ARP ignore b.set_arp_ignore(bond['ip_enable_arp_ignore']) # Enable proxy-arp on this interface b.set_proxy_arp(bond['ip_proxy_arp']) # Enable private VLAN proxy ARP on this interface b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) # IPv6 accept RA b.set_ipv6_accept_ra(bond['ipv6_accept_ra']) # IPv6 address autoconfiguration b.set_ipv6_autoconf(bond['ipv6_autoconf']) # IPv6 forwarding b.set_ipv6_forwarding(bond['ipv6_forwarding']) # IPv6 Duplicate Address Detection (DAD) tries b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect']) # Delete old IPv6 EUI64 addresses before changing MAC for addr in bond['ipv6_eui64_prefix_remove']: b.del_ipv6_eui64_address(addr) # Change interface MAC address if bond['mac']: b.set_mac(bond['mac']) # Add IPv6 EUI-based addresses for addr in bond['ipv6_eui64_prefix']: b.add_ipv6_eui64_address(addr) # Maximum Transmission Unit (MTU) b.set_mtu(bond['mtu']) # Primary device interface if bond['primary']: b.set_primary(bond['primary']) # Some parameters can not be changed when the bond is up. if bond['shutdown_required']: # Disable bond prior changing of certain properties b.set_admin_state('down') # The bonding mode can not be changed when there are interfaces enslaved # to this bond, thus we will free all interfaces from the bond first! for intf in b.get_slaves(): b.del_port(intf) # Bonding policy/mode b.set_mode(bond['mode']) # Add (enslave) interfaces to bond for intf in bond['member']: # if we've come here we already verified the interface doesn't # have addresses configured so just flush any remaining ones cmd(f'ip addr flush dev "{intf}"') b.add_port(intf) # As the bond interface is always disabled first when changing # parameters we will only re-enable the interface if it is not # administratively disabled if not bond['disable']: b.set_admin_state('up') else: b.set_admin_state('down') # Configure interface address(es) # - not longer required addresses get removed first # - newly addresses will be added second for addr in bond['address_remove']: b.del_addr(addr) for addr in bond['address']: b.add_addr(addr) # assign/remove VRF (ONLY when not a member of a bridge, # otherwise 'nomaster' removes it from it) if not bond['is_bridge_member']: b.set_vrf(bond['vrf']) # re-add ourselves to any bridge we might have fallen out of if bond['is_bridge_member']: b.add_to_bridge(bond['is_bridge_member']) # apply all vlans to interface apply_all_vlans(b, bond) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)