diff options
author | Christian Poessinger <christian@poessinger.com> | 2019-10-13 12:49:47 +0200 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2019-10-13 12:49:47 +0200 |
commit | 79bc826426385e5b40fbe58137d0a2d2831cf274 (patch) | |
tree | e21c70c44c5889c3b37eabf9e7a7d6095f842a69 /src/conf_mode/interfaces-bonding.py | |
parent | 5c834feebe5e7749ec731dcf9556ae6997b0b526 (diff) | |
parent | fe7279454c86a2947b23fb0d769483b7fe2a3cc3 (diff) | |
download | vyos-1x-79bc826426385e5b40fbe58137d0a2d2831cf274.tar.gz vyos-1x-79bc826426385e5b40fbe58137d0a2d2831cf274.zip |
Merge branch 'current' of github.com:vyos/vyos-1x into equuleus
* 'current' of github.com:vyos/vyos-1x:
Sync XML interface description source file pattern and conf script name
Python/ifconfig: T1557: add support for DHCPv6 client options
bonding: T1614: support DHCP options on VLAN interfaces
Python/ifconfig: T1557: bugfix when configuring accept_ra on VLAN interfaces
conf_mode: bonding/ethernet fix comments
Jenkins: Docker: always pull container from Dockerhub
Diffstat (limited to 'src/conf_mode/interfaces-bonding.py')
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 526 |
1 files changed, 526 insertions, 0 deletions
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py new file mode 100755 index 000000000..8a0f9f84d --- /dev/null +++ b/src/conf_mode/interfaces-bonding.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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, VLANIf +from vyos.configdict import list_diff, vlan_to_dict +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'arp_mon_intvl': 0, + 'arp_mon_tgt': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'hash_policy': 'layer2', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mode': '802.3ad', + 'member': [], + 'mtu': 1500, + '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 apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(VLANIf("lo")): + raise TypeError() + + # get DHCP config dictionary and update values + opt = vlan.get_dhcp_options() + + if config['dhcp_client_id']: + opt['client_id'] = config['dhcp_client_id'] + + if config['dhcp_hostname']: + opt['hostname'] = config['dhcp_hostname'] + + if config['dhcp_vendor_class_id']: + opt['vendor_class_id'] = config['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + vlan.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = vlan.get_dhcpv6_options() + + if config['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if config['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + vlan.set_dhcpv6_options(opt) + + # update interface description used e.g. within SNMP + vlan.set_alias(config['description']) + # ignore link state changes + vlan.set_link_detect(config['disable_link_detect']) + # Maximum Transmission Unit (MTU) + vlan.set_mtu(config['mtu']) + # Change VLAN interface MAC address + if config['mac']: + vlan.set_mac(config['mac']) + + # enable/disable VLAN interface + if config['disable']: + vlan.set_state('down') + else: + vlan.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + +def get_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 os.system('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") + + bond = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if bond has been removed + cfg_base = 'interfaces bonding ' + bond['intf'] + if not conf.exists(cfg_base): + bond['deleted'] = True + return bond + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + bond['address'] = conf.return_values('address') + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + bond['address_remove'] = list_diff(eff_addr, bond['address']) + + # 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') + + # retrieve interface description + if conf.exists('description'): + bond['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCP client vendor identifier + if conf.exists('dhcp-options vendor-class-id'): + bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bond['dhcpv6_prm_only'] = True + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bond['dhcpv6_temporary'] = True + + # ignore link state changes + if conf.exists('disable-link-detect'): + bond['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + bond['disable'] = True + + # 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 proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + bond['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + bond['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + bond['mac'] = conf.return_value('mac') + + # Bonding mode + if conf.exists('mode'): + bond['mode'] = get_bond_mode(conf.return_value('mode')) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + bond['mtu'] = int(conf.return_value('mtu')) + + # determine bond member interfaces (currently configured) + if conf.exists('member interface'): + bond['member'] = conf.return_values('member interface') + + # Primary device interface + if conf.exists('primary'): + bond['primary'] = conf.return_value('primary') + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # get vif-s interfaces (currently effective) - to determine which vif-s + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + bond['vif_s_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + bond['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + bond['vif_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + bond['vif'].append(vlan_to_dict(conf)) + + return bond + + +def verify(bond): + if len (bond['arp_mon_tgt']) > 16: + raise ConfigError('The maximum number of targets that can be specified 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 this mode.'.format()) + + if bond['primary'] not in bond['member']: + raise ConfigError('Interface "{}" is not part of the bond' \ + .format(bond['primary'])) + + + # DHCPv6 parameters-only and temporary address are mutually exclusive + for vif_s in bond['vif_s']: + if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif_c in vif_s['vif_c']: + if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif in bond['vif']: + if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + + for vif_s in bond['vif_s']: + for vif in bond['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + + + conf = Config() + for intf in bond['member']: + # 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('can not enslave interface {} which already ' \ + 'belongs to {}'.format(intf, tmp)) + + # can not add interfaces with an assigned address to a bond + if conf.exists('interfaces ethernet ' + intf + ' address'): + raise ConfigError('can not enslave interface {} which has an address ' \ + 'assigned'.format(intf)) + + # bond members are not allowed to be bridge members, too + for tmp in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'bridge {}'.format(intf, tmp)) + + # bond members are not allowed to be vrrp members, too + for tmp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'VRRP group {}'.format(intf, 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('can not enslave interface {} which belongs to ' \ + 'pseudo-ethernet {}'.format(intf, 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('can not enslave interface {} which belongs to ' \ + 'vxlan {}'.format(intf, tmp)) + + + if bond['primary']: + if bond['primary'] not in bond['member']: + raise ConfigError('primary interface must be a member interface of {}' \ + .format(bond['intf'])) + + 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: + # Some parameters can not be changed when the bond is up. + # Always disable the bond prior changing anything + b.set_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) + + # 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']) + + # get DHCP config dictionary and update values + opt = b.get_dhcp_options() + + if bond['dhcp_client_id']: + opt['client_id'] = bond['dhcp_client_id'] + + if bond['dhcp_hostname']: + opt['hostname'] = bond['dhcp_hostname'] + + if bond['dhcp_vendor_class_id']: + opt['vendor_class_id'] = bond['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + b.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = b.get_dhcpv6_options() + + if bond['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if bond['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + b.set_dhcpv6_options(opt) + + # 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']) + # 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']) + + # Change interface MAC address + if bond['mac']: + b.set_mac(bond['mac']) + + # Bonding policy + b.set_mode(bond['mode']) + # Maximum Transmission Unit (MTU) + b.set_mtu(bond['mtu']) + + # Primary device interface + if bond['primary']: + b.set_primary(bond['primary']) + + # Add (enslave) interfaces to bond + for intf in bond['member']: + 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_state('up') + + # 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) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in bond['vif_s_remove']: + b.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in bond['vif_s']: + s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in bond['vif_remove']: + b.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in bond['vif']: + vlan = b.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |