summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/configsession.py4
-rw-r--r--python/vyos/configverify.py72
-rw-r--r--python/vyos/ethtool.py101
-rw-r--r--python/vyos/frr.py35
-rw-r--r--python/vyos/ifconfig/__init__.py2
-rwxr-xr-xpython/vyos/ifconfig/erspan.py190
-rw-r--r--python/vyos/ifconfig/interface.py21
7 files changed, 403 insertions, 22 deletions
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 82b9355a3..670e6c7fc 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -129,9 +129,9 @@ class ConfigSession(object):
def __run_command(self, cmd_list):
p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ (stdout_data, stderr_data) = p.communicate()
+ output = stdout_data.decode()
result = p.wait()
- output = p.stdout.read().decode()
- p.communicate()
if result != 0:
raise ConfigSessionError(output)
return output
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index abd91583d..5a4d14c68 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -89,6 +89,49 @@ def verify_vrf(config):
'Interface "{ifname}" cannot be both a member of VRF "{vrf}" '
'and bridge "{is_bridge_member}"!'.format(**config))
+def verify_tunnel(config):
+ """
+ This helper is used to verify the common part of the tunnel
+ """
+ from vyos.template import is_ipv4
+ from vyos.template import is_ipv6
+
+ if 'encapsulation' not in config:
+ raise ConfigError('Must configure the tunnel encapsulation for '\
+ '{ifname}!'.format(**config))
+
+ if 'local_ip' not in config and 'dhcp_interface' not in config:
+ raise ConfigError('local-ip is mandatory for tunnel')
+
+ if 'remote_ip' not in config and config['encapsulation'] != 'gre':
+ raise ConfigError('remote-ip is mandatory for tunnel')
+
+ if {'local_ip', 'dhcp_interface'} <= set(config):
+ raise ConfigError('Can not use both local-ip and dhcp-interface')
+
+ if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre', 'ip6erspan']:
+ error_ipv6 = 'Encapsulation mode requires IPv6'
+ if 'local_ip' in config and not is_ipv6(config['local_ip']):
+ raise ConfigError(f'{error_ipv6} local-ip')
+
+ if 'remote_ip' in config and not is_ipv6(config['remote_ip']):
+ raise ConfigError(f'{error_ipv6} remote-ip')
+ else:
+ error_ipv4 = 'Encapsulation mode requires IPv4'
+ if 'local_ip' in config and not is_ipv4(config['local_ip']):
+ raise ConfigError(f'{error_ipv4} local-ip')
+
+ if 'remote_ip' in config and not is_ipv4(config['remote_ip']):
+ raise ConfigError(f'{error_ipv4} remote-ip')
+
+ if config['encapsulation'] in ['sit', 'gre-bridge']:
+ if 'source_interface' in config:
+ raise ConfigError('Option source-interface can not be used with ' \
+ 'encapsulation "sit" or "gre-bridge"')
+ elif config['encapsulation'] == 'gre':
+ if 'local_ip' in config and is_ipv6(config['local_ip']):
+ raise ConfigError('Can not use local IPv6 address is for mGRE tunnels')
+
def verify_eapol(config):
"""
Common helper function used by interface implementations to perform
@@ -209,6 +252,13 @@ def verify_vlan_config(config):
Common helper function used by interface implementations to perform
recurring validation of interface VLANs
"""
+
+ # VLAN and Q-in-Q IDs are not allowed to overlap
+ if 'vif' in config and 'vif_s' in config:
+ duplicate = list(set(config['vif']) & set(config['vif_s']))
+ if duplicate:
+ raise ConfigError(f'Duplicate VLAN id "{duplicate[0]}" used for vif and vif-s interfaces!')
+
# 802.1q VLANs
for vlan in config.get('vif', {}):
vlan = config['vif'][vlan]
@@ -217,17 +267,17 @@ def verify_vlan_config(config):
verify_vrf(vlan)
# 802.1ad (Q-in-Q) VLANs
- for vlan in config.get('vif_s', {}):
- vlan = config['vif_s'][vlan]
- verify_dhcpv6(vlan)
- verify_address(vlan)
- verify_vrf(vlan)
-
- for vlan in config.get('vif_s', {}).get('vif_c', {}):
- vlan = config['vif_c'][vlan]
- verify_dhcpv6(vlan)
- verify_address(vlan)
- verify_vrf(vlan)
+ for s_vlan in config.get('vif_s', {}):
+ s_vlan = config['vif_s'][s_vlan]
+ verify_dhcpv6(s_vlan)
+ verify_address(s_vlan)
+ verify_vrf(s_vlan)
+
+ 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)
def verify_accel_ppp_base_service(config):
"""
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
new file mode 100644
index 000000000..cef7d476f
--- /dev/null
+++ b/python/vyos/ethtool.py
@@ -0,0 +1,101 @@
+# Copyright 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.util import cmd
+
+class Ethtool:
+ """
+ Class is used to retrive and cache information about an ethernet adapter
+ """
+
+ # dictionary containing driver featurs, it will be populated on demand and
+ # the content will look like:
+ # {
+ # 'tls-hw-tx-offload': {'fixed': True, 'on': False},
+ # 'tx-checksum-fcoe-crc': {'fixed': True, 'on': False},
+ # 'tx-checksum-ip-generic': {'fixed': False, 'on': True},
+ # 'tx-checksum-ipv4': {'fixed': True, 'on': False},
+ # 'tx-checksum-ipv6': {'fixed': True, 'on': False},
+ # 'tx-checksum-sctp': {'fixed': True, 'on': False},
+ # 'tx-checksumming': {'fixed': False, 'on': True},
+ # 'tx-esp-segmentation': {'fixed': True, 'on': False},
+ # }
+ features = { }
+ ring_buffers = { }
+
+ def __init__(self, ifname):
+ # Now populate features dictionaty
+ tmp = cmd(f'ethtool -k {ifname}')
+ # skip the first line, it only says: "Features for eth0":
+ for line in tmp.splitlines()[1:]:
+ if ":" in line:
+ key, value = [s.strip() for s in line.strip().split(":", 1)]
+ fixed = "fixed" in value
+ if fixed:
+ value = value.split()[0].strip()
+ self.features[key.strip()] = {
+ "on": value == "on",
+ "fixed": fixed
+ }
+
+ tmp = cmd(f'ethtool -g {ifname}')
+ # We are only interested in line 2-5 which contains the device maximum
+ # ringbuffers
+ for line in tmp.splitlines()[2:6]:
+ if ':' in line:
+ key, value = [s.strip() for s in line.strip().split(":", 1)]
+ key = key.lower().replace(' ', '_')
+ self.ring_buffers[key] = int(value)
+
+
+ def is_fixed_lro(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('large-receive-offload', True).get('fixed', True)
+
+ def is_fixed_gro(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('generic-receive-offload', True).get('fixed', True)
+
+ def is_fixed_gso(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('generic-segmentation-offload', True).get('fixed', True)
+
+ def is_fixed_sg(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('scatter-gather', True).get('fixed', True)
+
+ def is_fixed_tso(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('tcp-segmentation-offload', True).get('fixed', True)
+
+ def is_fixed_ufo(self):
+ # in case of a missing configuration, rather return "fixed". In Ethtool
+ # terminology "fixed" means the setting can not be changed by the user.
+ return self.features.get('udp-fragmentation-offload', True).get('fixed', True)
+
+ def get_rx_buffer(self):
+ # in case of a missing configuration rather return a "small"
+ # buffer of only 512 bytes.
+ return self.ring_buffers.get('rx', '512')
+
+ def get_tx_buffer(self):
+ # in case of a missing configuration rather return a "small"
+ # buffer of only 512 bytes.
+ return self.ring_buffers.get('tx', '512')
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
index 76e204ab3..69c7a14ce 100644
--- a/python/vyos/frr.py
+++ b/python/vyos/frr.py
@@ -69,8 +69,17 @@ import tempfile
import re
from vyos import util
import logging
+from logging.handlers import SysLogHandler
+import os
LOG = logging.getLogger(__name__)
+DEBUG = os.path.exists('/tmp/vyos.frr.debug')
+if DEBUG:
+ LOG.setLevel(logging.DEBUG)
+ ch = SysLogHandler(address='/dev/log')
+ ch2 = logging.StreamHandler()
+ LOG.addHandler(ch)
+ LOG.addHandler(ch2)
_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd',
'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']
@@ -175,15 +184,23 @@ def reload_configuration(config, daemon=None):
f.write(config)
f.flush()
+ LOG.debug(f'reload_configuration: Reloading config using temporary file: {f.name}')
cmd = f'{path_frr_reload} --reload'
if daemon:
cmd += f' --daemon {daemon}'
+
+ if DEBUG:
+ cmd += f' --debug --stdout'
+
cmd += f' {f.name}'
+ LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"')
output, code = util.popen(cmd, stderr=util.STDOUT)
f.close()
+ for i, e in enumerate(output.split('\n')):
+ LOG.debug(f'frr-reload output: {i:3} {e}')
if code == 1:
- raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}')
+ raise CommitError(f'Configuration FRR failed while commiting code, please enabling debugging to examine logs')
elif code:
raise OSError(code, output)
@@ -382,6 +399,11 @@ class FRRConfig:
raise ValueError(
'The config element needs to be a string or list type object')
+ if config:
+ LOG.debug(f'__init__: frr library initiated with initial config')
+ for i, e in enumerate(self.config):
+ LOG.debug(f'__init__: initial {i:3} {e}')
+
def load_configuration(self, daemon=None):
'''Load the running configuration from FRR into the config object
daemon: str with name of the FRR Daemon to load configuration from or
@@ -390,9 +412,16 @@ class FRRConfig:
Using this overwrites the current loaded config objects and replaces the original loaded config
'''
self.imported_config = get_configuration(daemon=daemon)
- LOG.debug(f'load_configuration: Configuration loaded from FRR: {self.imported_config}')
+ if daemon:
+ LOG.debug(f'load_configuration: Configuration loaded from FRR daemon {daemon}')
+ else:
+ LOG.debug(f'load_configuration: Configuration loaded from FRR integrated config')
+
self.original_config = self.imported_config.split('\n')
self.config = self.original_config.copy()
+
+ for i, e in enumerate(self.imported_config.split('\n')):
+ LOG.debug(f'load_configuration: loaded {i:3} {e}')
return
def test_configuration(self):
@@ -408,6 +437,8 @@ class FRRConfig:
None to use the consolidated config
'''
LOG.debug('commit_configuration: Commiting configuration')
+ for i, e in enumerate(self.config):
+ LOG.debug(f'commit_configuration: new_config {i:3} {e}')
reload_configuration('\n'.join(self.config), daemon=daemon)
def modify_section(self, start_pattern, replacement=[], stop_pattern=r'\S+', remove_stop_mark=False, count=0):
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index 9cd8d44c1..f7b55c9dd 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -39,6 +39,8 @@ from vyos.ifconfig.tunnel import IPIP6If
from vyos.ifconfig.tunnel import IP6IP6If
from vyos.ifconfig.tunnel import SitIf
from vyos.ifconfig.tunnel import Sit6RDIf
+from vyos.ifconfig.erspan import ERSpanIf
+from vyos.ifconfig.erspan import ER6SpanIf
from vyos.ifconfig.wireless import WiFiIf
from vyos.ifconfig.l2tpv3 import L2TPv3If
from vyos.ifconfig.macsec import MACsecIf
diff --git a/python/vyos/ifconfig/erspan.py b/python/vyos/ifconfig/erspan.py
new file mode 100755
index 000000000..50230e14a
--- /dev/null
+++ b/python/vyos/ifconfig/erspan.py
@@ -0,0 +1,190 @@
+# Copyright 2019-2020 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#erspan
+# http://vger.kernel.org/lpc_net2018_talks/erspan-linux-presentation.pdf
+
+from copy import deepcopy
+
+from netaddr import EUI
+from netaddr import mac_unix_expanded
+from random import getrandbits
+
+from vyos.util import dict_search
+from vyos.ifconfig.interface import Interface
+from vyos.validate import assert_list
+
+@Interface.register
+class _ERSpan(Interface):
+ """
+ _ERSpan: private base class for ERSPAN tunnels
+ """
+ default = {
+ **Interface.default,
+ **{
+ 'type': 'erspan',
+ }
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'erspan',
+ 'prefixes': ['ersp',],
+ },
+ }
+
+ options = ['local_ip','remote_ip','encapsulation','parameters']
+
+ def __init__(self,ifname,**config):
+ self.config = deepcopy(config) if config else {}
+ super().__init__(ifname, **self.config)
+
+ def change_options(self):
+ pass
+
+ def update(self, config):
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ super().update(config)
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
+ def _create(self):
+ pass
+
+class ERSpanIf(_ERSpan):
+ """
+ ERSpanIf: private base class for ERSPAN Over GRE and IPv4 tunnels
+ """
+
+ def _create(self):
+ ifname = self.config['ifname']
+ local_ip = self.config['local_ip']
+ remote_ip = self.config['remote_ip']
+ key = self.config['parameters']['ip']['key']
+ version = self.config['parameters']['version']
+ command = f'ip link add dev {ifname} type erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}'
+
+ if int(version) == 1:
+ idx=dict_search('parameters.erspan.idx',self.config)
+ if idx:
+ command += f' erspan {idx}'
+ elif int(version) == 2:
+ direction=dict_search('parameters.erspan.direction',self.config)
+ if direction:
+ command += f' erspan_dir {direction}'
+ hwid=dict_search('parameters.erspan.hwid',self.config)
+ if hwid:
+ command += f' erspan_hwid {hwid}'
+
+ ttl = dict_search('parameters.ip.ttl',self.config)
+ if ttl:
+ command += f' ttl {ttl}'
+ tos = dict_search('parameters.ip.tos',self.config)
+ if tos:
+ command += f' tos {tos}'
+
+ self._cmd(command)
+
+ def change_options(self):
+ ifname = self.config['ifname']
+ local_ip = self.config['local_ip']
+ remote_ip = self.config['remote_ip']
+ key = self.config['parameters']['ip']['key']
+ version = self.config['parameters']['version']
+ command = f'ip link set dev {ifname} type erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}'
+
+ if int(version) == 1:
+ idx=dict_search('parameters.erspan.idx',self.config)
+ if idx:
+ command += f' erspan {idx}'
+ elif int(version) == 2:
+ direction=dict_search('parameters.erspan.direction',self.config)
+ if direction:
+ command += f' erspan_dir {direction}'
+ hwid=dict_search('parameters.erspan.hwid',self.config)
+ if hwid:
+ command += f' erspan_hwid {hwid}'
+
+ ttl = dict_search('parameters.ip.ttl',self.config)
+ if ttl:
+ command += f' ttl {ttl}'
+ tos = dict_search('parameters.ip.tos',self.config)
+ if tos:
+ command += f' tos {tos}'
+
+ self._cmd(command)
+
+class ER6SpanIf(_ERSpan):
+ """
+ ER6SpanIf: private base class for ERSPAN Over GRE and IPv6 tunnels
+ """
+
+ def _create(self):
+ ifname = self.config['ifname']
+ local_ip = self.config['local_ip']
+ remote_ip = self.config['remote_ip']
+ key = self.config['parameters']['ip']['key']
+ version = self.config['parameters']['version']
+ command = f'ip link add dev {ifname} type ip6erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}'
+
+ if int(version) == 1:
+ idx=dict_search('parameters.erspan.idx',self.config)
+ if idx:
+ command += f' erspan {idx}'
+ elif int(version) == 2:
+ direction=dict_search('parameters.erspan.direction',self.config)
+ if direction:
+ command += f' erspan_dir {direction}'
+ hwid=dict_search('parameters.erspan.hwid',self.config)
+ if hwid:
+ command += f' erspan_hwid {hwid}'
+
+ ttl = dict_search('parameters.ip.ttl',self.config)
+ if ttl:
+ command += f' ttl {ttl}'
+ tos = dict_search('parameters.ip.tos',self.config)
+ if tos:
+ command += f' tos {tos}'
+
+ self._cmd(command)
+
+ def change_options(self):
+ ifname = self.config['ifname']
+ local_ip = self.config['local_ip']
+ remote_ip = self.config['remote_ip']
+ key = self.config['parameters']['ip']['key']
+ version = self.config['parameters']['version']
+ command = f'ip link set dev {ifname} type ip6erspan local {local_ip} remote {remote_ip} seq key {key} erspan_ver {version}'
+
+ if int(version) == 1:
+ idx=dict_search('parameters.erspan.idx',self.config)
+ if idx:
+ command += f' erspan {idx}'
+ elif int(version) == 2:
+ direction=dict_search('parameters.erspan.direction',self.config)
+ if direction:
+ command += f' erspan_dir {direction}'
+ hwid=dict_search('parameters.erspan.hwid',self.config)
+ if hwid:
+ command += f' erspan_hwid {hwid}'
+
+ self._cmd(command)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 3b92ce463..4bdabd432 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -923,12 +923,12 @@ class Interface(Control):
else:
add_vlan.append(vlan)
allowed_vlan_ids.append(vlan)
-
+
# Remove redundant VLANs from the system
for vlan in list_diff(cur_vlan_ids, add_vlan):
cmd = f'bridge vlan del dev {ifname} vid {vlan} master'
self._cmd(cmd)
-
+
for vlan in allowed_vlan_ids:
cmd = f'bridge vlan add dev {ifname} vid {vlan} master'
self._cmd(cmd)
@@ -1015,9 +1015,11 @@ class Interface(Control):
source_if = next(iter(self._config['is_mirror_intf']))
config = self._config['is_mirror_intf'][source_if].get('mirror', None)
+ # Please do not clear the 'set $? = 0 '. It's meant to force a return of 0
# Remove existing mirroring rules
- delete_tc_cmd = f'tc qdisc del dev {source_if} handle ffff: ingress; '
- delete_tc_cmd += f'tc qdisc del dev {source_if} handle 1: root prio'
+ delete_tc_cmd = f'tc qdisc del dev {source_if} handle ffff: ingress 2> /dev/null;'
+ delete_tc_cmd += f'tc qdisc del dev {source_if} handle 1: root prio 2> /dev/null;'
+ delete_tc_cmd += 'set $?=0'
self._popen(delete_tc_cmd)
# Bail out early if nothing needs to be configured
@@ -1072,6 +1074,10 @@ class Interface(Control):
interface setup code and provide a single point of entry when workin
on any interface. """
+ if self.debug:
+ import pprint
+ pprint.pprint(config)
+
# Cache the configuration - it will be reused inside e.g. DHCP handler
# XXX: maybe pass the option via __init__ in the future and rename this
# method to apply()?
@@ -1102,9 +1108,10 @@ class Interface(Control):
self.del_addr('dhcp')
# always ensure DHCPv6 client is stopped (when not configured as client
- # for IPv6 address or prefix delegation
+ # for IPv6 address or prefix delegation)
dhcpv6pd = dict_search('dhcpv6_options.pd', config)
- if 'dhcpv6' not in new_addr or dhcpv6pd == None:
+ dhcpv6pd = dhcpv6pd != None and len(dhcpv6pd) != 0
+ if 'dhcpv6' not in new_addr and not dhcpv6pd:
self.del_addr('dhcpv6')
# determine IP addresses which are assigned to the interface and build a
@@ -1124,7 +1131,7 @@ class Interface(Control):
self.add_addr(addr)
# start DHCPv6 client when only PD was configured
- if dhcpv6pd != None:
+ if dhcpv6pd:
self.set_dhcpv6(True)
# There are some items in the configuration which can only be applied