diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/configsession.py | 4 | ||||
-rw-r--r-- | python/vyos/configverify.py | 72 | ||||
-rw-r--r-- | python/vyos/ethtool.py | 101 | ||||
-rw-r--r-- | python/vyos/frr.py | 35 | ||||
-rw-r--r-- | python/vyos/ifconfig/__init__.py | 2 | ||||
-rwxr-xr-x | python/vyos/ifconfig/erspan.py | 190 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 21 |
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 |