diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/configverify.py | 16 | ||||
-rw-r--r-- | python/vyos/ifconfig/bond.py | 34 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 97 | ||||
-rw-r--r-- | python/vyos/ifconfig/tunnel.py | 1 | ||||
-rw-r--r-- | python/vyos/ifconfig/vti.py | 32 | ||||
-rw-r--r-- | python/vyos/template.py | 14 | ||||
-rw-r--r-- | python/vyos/util.py | 76 |
7 files changed, 228 insertions, 42 deletions
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 99c472582..88cbf2d5b 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -45,6 +45,16 @@ def verify_mtu(config): raise ConfigError(f'Interface MTU too high, ' \ f'maximum supported MTU is {max_mtu}!') +def verify_mtu_parent(config, parent): + if 'mtu' not in config or 'mtu' not in parent: + return + + mtu = int(config['mtu']) + parent_mtu = int(parent['mtu']) + if mtu > parent_mtu: + raise ConfigError(f'Interface MTU ({mtu}) too high, ' \ + f'parent interface MTU is {parent_mtu}!') + def verify_mtu_ipv6(config): """ Common helper function used by interface implementations to perform @@ -266,6 +276,7 @@ def verify_vlan_config(config): verify_dhcpv6(vlan) verify_address(vlan) verify_vrf(vlan) + verify_mtu_parent(vlan, config) # 802.1ad (Q-in-Q) VLANs for s_vlan in config.get('vif_s', {}): @@ -273,12 +284,15 @@ def verify_vlan_config(config): verify_dhcpv6(s_vlan) verify_address(s_vlan) verify_vrf(s_vlan) + verify_mtu_parent(s_vlan, config) for c_vlan in s_vlan.get('vif_c', {}): c_vlan = s_vlan['vif_c'][c_vlan] verify_dhcpv6(c_vlan) verify_address(c_vlan) verify_vrf(c_vlan) + verify_mtu_parent(c_vlan, config) + verify_mtu_parent(c_vlan, s_vlan) def verify_accel_ppp_base_service(config): """ diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index bfa3b0025..233d53688 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -51,6 +51,10 @@ class BondIf(Interface): 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/bonding/min_links', }, + 'bond_lacp_rate': { + 'validate': lambda v: assert_list(v, ['slow', 'fast']), + 'location': '/sys/class/net/{ifname}/bonding/lacp_rate', + }, 'bond_miimon': { 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/bonding/miimon' @@ -152,6 +156,26 @@ class BondIf(Interface): """ self.set_interface('bond_min_links', number) + def set_lacp_rate(self, slow_fast): + """ + Option specifying the rate in which we'll ask our link partner + to transmit LACPDU packets in 802.3ad mode. Possible values + are: + + slow or 0 + Request partner to transmit LACPDUs every 30 seconds + + fast or 1 + Request partner to transmit LACPDUs every 1 second + + The default is slow. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_lacp_rate('slow') + """ + self.set_interface('bond_lacp_rate', slow_fast) + def set_arp_interval(self, interval): """ Specifies the ARP link monitoring frequency in milliseconds. @@ -382,9 +406,13 @@ class BondIf(Interface): if not dict_search(f'member.interface_remove.{interface}.disable', config): Interface(interface).set_admin_state('up') - # Bonding policy/mode - value = config.get('mode') - if value: self.set_mode(value) + # Bonding policy/mode - default value, always present + mode = config.get('mode') + self.set_mode(mode) + + # LACPDU transmission rate - default value + if mode == '802.3ad': + self.set_lacp_rate(config.get('lacp_rate')) # Add (enslave) interfaces to bond value = dict_search('member.interface', config) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index ff05cab0e..6a66d958f 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -36,6 +36,7 @@ from vyos.template import render from vyos.util import mac2eui64 from vyos.util import dict_search from vyos.util import read_file +from vyos.util import get_interface_config from vyos.template import is_ipv4 from vyos.validate import is_intf_addr_assigned from vyos.validate import is_ipv6_link_local @@ -743,28 +744,37 @@ class Interface(Control): """ self.set_interface('proxy_arp_pvlan', enable) - def get_addr(self): + def get_addr_v4(self): """ - Retrieve assigned IPv4 and IPv6 addresses from given interface. + Retrieve assigned IPv4 addresses from given interface. This is done using the netifaces and ipaddress python modules. Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_addrs() - ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + >>> Interface('eth0').get_addr_v4() + ['172.16.33.30/24'] """ - ipv4 = [] - ipv6 = [] - - if AF_INET in ifaddresses(self.config['ifname']).keys(): + if AF_INET in ifaddresses(self.config['ifname']): for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: # we need to manually assemble a list of IPv4 address/prefix prefix = '/' + \ str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) ipv4.append(v4_addr['addr'] + prefix) + return ipv4 + + def get_addr_v6(self): + """ + Retrieve assigned IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. - if AF_INET6 in ifaddresses(self.config['ifname']).keys(): + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addr_v6() + ['fe80::20c:29ff:fe11:a174/64'] + """ + ipv6 = [] + if AF_INET6 in ifaddresses(self.config['ifname']): for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: # Note that currently expanded netmasks are not supported. That means # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. @@ -777,8 +787,18 @@ class Interface(Control): # addresses v6_addr['addr'] = v6_addr['addr'].split('%')[0] ipv6.append(v6_addr['addr'] + prefix) + return ipv6 - return ipv4 + ipv6 + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addr() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + return self.get_addr_v4() + self.get_addr_v6() def add_addr(self, addr): """ @@ -1289,6 +1309,16 @@ class Interface(Control): vif_s_ifname = f'{ifname}.{vif_s_id}' vif_s_config['ifname'] = vif_s_ifname + + # It is not possible to change the VLAN encapsulation protocol + # "on-the-fly". For this "quirk" we need to actively delete and + # re-create the VIF-S interface. + if self.exists(vif_s_ifname): + cur_cfg = get_interface_config(vif_s_ifname) + protocol = dict_search('linkinfo.info_data.protocol', cur_cfg).lower() + if protocol != vif_s_config['protocol']: + VLANIf(vif_s_ifname).remove() + s_vlan = VLANIf(vif_s_ifname, **tmp) s_vlan.update(vif_s_config) @@ -1315,12 +1345,55 @@ class Interface(Control): # create/update 802.1q VLAN interfaces for vif_id, vif_config in config.get('vif', {}).items(): + + vif_ifname = f'{ifname}.{vif_id}' + vif_config['ifname'] = vif_ifname + tmp = deepcopy(VLANIf.get_config()) tmp['source_interface'] = ifname tmp['vlan_id'] = vif_id + + # We need to ensure that the string format is consistent, and we need to exclude redundant spaces. + sep = ' ' + if 'egress_qos' in vif_config: + # Unwrap strings into arrays + egress_qos_array = vif_config['egress_qos'].split() + # The split array is spliced according to the fixed format + tmp['egress_qos'] = sep.join(egress_qos_array) + + if 'ingress_qos' in vif_config: + # Unwrap strings into arrays + ingress_qos_array = vif_config['ingress_qos'].split() + # The split array is spliced according to the fixed format + tmp['ingress_qos'] = sep.join(ingress_qos_array) + + # Since setting the QoS control parameters in the later stage will + # not completely delete the old settings, + # we still need to delete the VLAN encapsulation interface in order to + # ensure that the changed settings are effective. + cur_cfg = get_interface_config(vif_ifname) + qos_str = '' + tmp2 = dict_search('linkinfo.info_data.ingress_qos', cur_cfg) + if 'ingress_qos' in tmp and tmp2: + for item in tmp2: + from_key = item['from'] + to_key = item['to'] + qos_str += f'{from_key}:{to_key} ' + if qos_str != tmp['ingress_qos']: + if self.exists(vif_ifname): + VLANIf(vif_ifname).remove() + + qos_str = '' + tmp2 = dict_search('linkinfo.info_data.egress_qos', cur_cfg) + if 'egress_qos' in tmp and tmp2: + for item in tmp2: + from_key = item['from'] + to_key = item['to'] + qos_str += f'{from_key}:{to_key} ' + if qos_str != tmp['egress_qos']: + if self.exists(vif_ifname): + VLANIf(vif_ifname).remove() - vif_ifname = f'{ifname}.{vif_id}' - vif_config['ifname'] = vif_ifname vlan = VLANIf(vif_ifname, **tmp) vlan.update(vif_config) diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 2a266fc9f..64c735824 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -62,6 +62,7 @@ class TunnelIf(Interface): mapping_ipv4 = { 'parameters.ip.key' : 'key', 'parameters.ip.no_pmtu_discovery' : 'nopmtudisc', + 'parameters.ip.ignore_df' : 'ignore-df', 'parameters.ip.tos' : 'tos', 'parameters.ip.ttl' : 'ttl', 'parameters.erspan.direction' : 'erspan_dir', diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index e2090c889..9eafcd11b 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. from vyos.ifconfig.interface import Interface +from vyos.util import dict_search @Interface.register class VTIIf(Interface): @@ -25,3 +26,34 @@ class VTIIf(Interface): 'prefixes': ['vti', ], }, } + + def _create(self): + # This table represents a mapping from VyOS internal config dict to + # arguments used by iproute2. For more information please refer to: + # - https://man7.org/linux/man-pages/man8/ip-link.8.html + # - https://man7.org/linux/man-pages/man8/ip-tunnel.8.html + mapping = { + 'source_address' : 'local', + 'source_interface' : 'dev', + 'remote' : 'remote', + 'key' : 'key', + } + + cmd = 'ip link add {ifname} type vti' + for vyos_key, iproute2_key in mapping.items(): + # dict_search will return an empty dict "{}" for valueless nodes like + # "parameters.nolearning" - thus we need to test the nodes existence + # by using isinstance() + tmp = dict_search(vyos_key, self.config) + if isinstance(tmp, dict): + cmd += f' {iproute2_key}' + elif tmp != None: + cmd += f' {iproute2_key} {tmp}' + + self._cmd(cmd.format(**self.config)) + self.set_interface('admin_state', 'down') + + def set_admin_state(self, state): + # function is not implemented for VTI interfaces as this is entirely + # handled by the ipsec up/down scripts + pass diff --git a/python/vyos/template.py b/python/vyos/template.py index 3fbb33acb..e1986b1e4 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -121,6 +121,14 @@ def render( ################################## # Custom template filters follow # ################################## +@register_filter('ip_from_cidr') +def ip_from_cidr(prefix): + """ Take an IPv4/IPv6 CIDR host and strip cidr mask. + Example: + 192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1 + """ + from ipaddress import ip_interface + return str(ip_interface(prefix).ip) @register_filter('address_from_cidr') def address_from_cidr(prefix): @@ -361,3 +369,9 @@ def natural_sort(iterable): return [convert(c) for c in re.split('([0-9]+)', str(key))] return sorted(iterable, key=alphanum_key) + +@register_filter('get_ipv4') +def get_ipv4(interface): + """ Get interface IPv4 addresses""" + from vyos.ifconfig import Interface + return Interface(interface).get_addr_v4() diff --git a/python/vyos/util.py b/python/vyos/util.py index 2a3f6a228..16fcbf10b 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -22,25 +22,13 @@ import sys # where it is used so it is as local as possible to the execution # - -def _need_sudo(command): - return os.path.basename(command.split()[0]) in ('systemctl', ) - - -def _add_sudo(command): - if _need_sudo(command): - return 'sudo ' + command - return command - - from subprocess import Popen from subprocess import PIPE from subprocess import STDOUT from subprocess import DEVNULL - def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ popen is a wrapper helper aound subprocess.Popen with it default setting it will return a tuple (out, err) @@ -79,9 +67,6 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, if not debug.enabled(flag): flag = 'command' - if autosudo: - command = _add_sudo(command) - cmd_msg = f"cmd '{command}'" debug.message(cmd_msg, flag) @@ -98,11 +83,8 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, stdin = PIPE input = input.encode() if type(input) is str else input - p = Popen( - command, - stdin=stdin, stdout=stdout, stderr=stderr, - env=env, shell=use_shell, - ) + p = Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, + env=env, shell=use_shell) pipe = p.communicate(input, timeout) @@ -135,7 +117,7 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=PIPE, decode='utf-8', autosudo=True): + stdout=DEVNULL, stderr=PIPE, decode='utf-8'): """ A wrapper around popen, which discard the stdout and will return the error code of a command @@ -151,8 +133,8 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True, - raising=None, message='', expect=[0]): + stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='', + expect=[0]): """ A wrapper around popen, which returns the stdout and will raise the error code of a command @@ -183,7 +165,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ A wrapper around popen, which print the stdout and will return the error code of a command @@ -682,6 +664,16 @@ def get_interface_config(interface): tmp = loads(cmd(f'ip -d -j link show {interface}'))[0] return tmp +def get_interface_address(interface): + """ Returns the used encapsulation protocol for given interface. + If interface does not exist, None is returned. + """ + if not os.path.exists(f'/sys/class/net/{interface}'): + return None + from json import loads + tmp = loads(cmd(f'ip -d -j addr show {interface}'))[0] + return tmp + def get_all_vrfs(): """ Return a dictionary of all system wide known VRF instances """ from json import loads @@ -694,3 +686,35 @@ def get_all_vrfs(): name = entry.pop('name') data[name] = entry return data + +def cidr_fit(cidr_a, cidr_b, both_directions = False): + """ + Does CIDR A fit inside of CIDR B? + + Credit: https://gist.github.com/magnetikonline/686fde8ee0bce4d4930ce8738908a009 + """ + def split_cidr(cidr): + part_list = cidr.split("/") + if len(part_list) == 1: + # if just an IP address, assume /32 + part_list.append("32") + + # return address and prefix size + return part_list[0].strip(), int(part_list[1]) + def address_to_bits(address): + # convert each octet of IP address to binary + bit_list = [bin(int(part)) for part in address.split(".")] + + # join binary parts together + # note: part[2:] to slice off the leading "0b" from bin() results + return "".join([part[2:].zfill(8) for part in bit_list]) + def binary_network_prefix(cidr): + # return CIDR as bits, to the length of the prefix size only (drop the rest) + address, prefix_size = split_cidr(cidr) + return address_to_bits(address)[:prefix_size] + + prefix_a = binary_network_prefix(cidr_a) + prefix_b = binary_network_prefix(cidr_b) + if both_directions: + return prefix_a.startswith(prefix_b) or prefix_b.startswith(prefix_a) + return prefix_a.startswith(prefix_b) |