summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configverify.py16
-rw-r--r--python/vyos/ifconfig/bond.py34
-rw-r--r--python/vyos/ifconfig/interface.py97
-rw-r--r--python/vyos/ifconfig/tunnel.py1
-rw-r--r--python/vyos/ifconfig/vti.py32
-rw-r--r--python/vyos/template.py14
-rw-r--r--python/vyos/util.py76
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)