summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
authorKim <kim.sidney@gmail.com>2021-10-07 16:52:56 +0200
committerGitHub <noreply@github.com>2021-10-07 16:52:56 +0200
commit2274dbf9047493a00a6f30346b38dacd8cfcf965 (patch)
treef431f5f6f1b2770c98ed9047e1cec9209e536366 /python/vyos
parent2acfffab8b98238e7d869673a858a4ae21651f0b (diff)
parentadc7ef387d40e92bd7163ee6b401e99e554394a3 (diff)
downloadvyos-1x-2274dbf9047493a00a6f30346b38dacd8cfcf965.tar.gz
vyos-1x-2274dbf9047493a00a6f30346b38dacd8cfcf965.zip
Merge branch 'current' into 2fa
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/airbag.py8
-rw-r--r--python/vyos/configdict.py15
-rw-r--r--python/vyos/configsource.py2
-rw-r--r--python/vyos/configverify.py35
-rw-r--r--python/vyos/defaults.py7
-rw-r--r--python/vyos/ethtool.py203
-rw-r--r--python/vyos/ifconfig/bridge.py1
-rw-r--r--python/vyos/ifconfig/control.py19
-rw-r--r--python/vyos/ifconfig/ethernet.py208
-rwxr-xr-xpython/vyos/ifconfig/interface.py296
-rw-r--r--python/vyos/ifconfig/pppoe.py122
-rw-r--r--python/vyos/ifconfig/section.py12
-rw-r--r--python/vyos/ifconfig/tunnel.py28
-rw-r--r--python/vyos/ifconfig/vrrp.py31
-rw-r--r--python/vyos/ifconfig/vti.py5
-rw-r--r--python/vyos/ifconfig/wireguard.py27
-rw-r--r--python/vyos/migrator.py18
-rw-r--r--python/vyos/systemversions.py28
-rw-r--r--python/vyos/template.py13
-rw-r--r--python/vyos/util.py87
-rw-r--r--python/vyos/xml/__init__.py2
-rw-r--r--python/vyos/xml/definition.py6
-rw-r--r--python/vyos/xml/kw.py1
-rw-r--r--python/vyos/xml/load.py17
24 files changed, 816 insertions, 375 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index a20f44207..3c7a144b7 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -125,14 +125,14 @@ def _intercepting_exceptions(_singleton=[False]):
# if the key before the value has not time, syslog takes that as the source of the message
FAULT = """\
-Report Time: {date}
-Image Version: VyOS {version}
-Release Train: {release_train}
+Report time: {date}
+Image version: VyOS {version}
+Release train: {release_train}
Built by: {built_by}
Built on: {built_on}
Build UUID: {build_uuid}
-Build Commit ID: {build_git}
+Build commit ID: {build_git}
Architecture: {system_arch}
Boot via: {boot_via}
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 0969a5353..5c6836e97 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -108,16 +108,20 @@ def leaf_node_changed(conf, path):
"""
Check if a leaf node was altered. If it has been altered - values has been
changed, or it was added/removed, we will return a list containing the old
- value(s). If nothing has been changed, None is returned
+ value(s). If nothing has been changed, None is returned.
+
+ NOTE: path must use the real CLI node name (e.g. with a hyphen!)
"""
from vyos.configdiff import get_config_diff
D = get_config_diff(conf, key_mangling=('-', '_'))
D.set_level(conf.get_level())
(new, old) = D.get_value_diff(path)
if new != old:
+ if old is None:
+ return []
if isinstance(old, str):
return [old]
- elif isinstance(old, list):
+ if isinstance(old, list):
if isinstance(new, str):
new = [new]
elif isinstance(new, type(None)):
@@ -343,8 +347,8 @@ def get_interface_dict(config, base, ifname=''):
# setup config level which is extracted in get_removed_vlans()
config.set_level(base + [ifname])
- dict = config.get_config_dict([], key_mangling=('-', '_'),
- get_first_key=True)
+ dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
# Check if interface has been removed. We must use exists() as
# get_config_dict() will always return {} - even when an empty interface
@@ -371,6 +375,9 @@ def get_interface_dict(config, base, ifname=''):
# XXX: T2665: blend in proper DHCPv6-PD default values
dict = T2665_set_dhcpv6pd_defaults(dict)
+ address = leaf_node_changed(config, ['address'])
+ if address: dict.update({'address_old' : address})
+
# Check if we are a member of a bridge device
bridge = is_member(config, ifname, 'bridge')
if bridge: dict.update({'is_bridge_member' : bridge})
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index 50222e385..b0981d25e 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -161,7 +161,7 @@ class ConfigSourceSession(ConfigSource):
if p.returncode != 0:
raise VyOSError()
else:
- return out.decode('ascii')
+ return out.decode('ascii', 'ignore')
def set_level(self, path):
"""
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 58028b604..8aca76568 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -67,22 +67,22 @@ def verify_mtu_ipv6(config):
min_mtu = 1280
if int(config['mtu']) < min_mtu:
interface = config['ifname']
- error_msg = f'IPv6 address will be configured on interface "{interface}" ' \
- f'thus the minimum MTU requirement is {min_mtu}!'
+ error_msg = f'IPv6 address will be configured on interface "{interface}",\n' \
+ f'the required minimum MTU is {min_mtu}!'
- for address in (dict_search('address', config) or []):
- if address in ['dhcpv6'] or is_ipv6(address):
- raise ConfigError(error_msg)
+ if 'address' in config:
+ for address in config['address']:
+ if address in ['dhcpv6'] or is_ipv6(address):
+ raise ConfigError(error_msg)
- tmp = dict_search('ipv6.address', config)
- if tmp and 'no_default_link_local' not in tmp:
- raise ConfigError('link-local ' + error_msg)
+ tmp = dict_search('ipv6.address.no_default_link_local', config)
+ if tmp == None: raise ConfigError('link-local ' + error_msg)
- if tmp and 'autoconf' in tmp:
- raise ConfigError(error_msg)
+ tmp = dict_search('ipv6.address.autoconf', config)
+ if tmp != None: raise ConfigError(error_msg)
- if tmp and 'eui64' in tmp:
- raise ConfigError(error_msg)
+ tmp = dict_search('ipv6.address.eui64', config)
+ if tmp != None: raise ConfigError(error_msg)
def verify_vrf(config):
"""
@@ -152,11 +152,10 @@ def verify_eapol(config):
if 'certificate' not in config['eapol']:
raise ConfigError('Certificate must be specified when using EAPoL!')
- if 'certificate' not in config['pki']:
+ if 'pki' not in config or 'certificate' not in config['pki']:
raise ConfigError('Invalid certificate specified for EAPoL')
cert_name = config['eapol']['certificate']
-
if cert_name not in config['pki']['certificate']:
raise ConfigError('Invalid certificate specified for EAPoL')
@@ -237,8 +236,8 @@ def verify_interface_exists(ifname):
Common helper function used by interface implementations to perform
recurring validation if an interface actually exists.
"""
- from netifaces import interfaces
- if ifname not in interfaces():
+ import os
+ if not os.path.exists(f'/sys/class/net/{ifname}'):
raise ConfigError(f'Interface "{ifname}" does not exist!')
def verify_source_interface(config):
@@ -344,7 +343,7 @@ def verify_accel_ppp_base_service(config):
# vertify auth settings
if dict_search('authentication.mode', config) == 'local':
if not dict_search('authentication.local_users', config):
- raise ConfigError('PPPoE local auth mode requires local users to be configured!')
+ raise ConfigError('Authentication mode local requires local users to be configured!')
for user in dict_search('authentication.local_users.username', config):
user_config = config['authentication']['local_users']['username'][user]
@@ -368,7 +367,7 @@ def verify_accel_ppp_base_service(config):
raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
if 'gateway_address' not in config:
- raise ConfigError('PPPoE server requires gateway-address to be configured!')
+ raise ConfigError('Server requires gateway-address to be configured!')
if 'name_server_ipv4' in config:
if len(config['name_server_ipv4']) > 2:
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 03006c383..00b14a985 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -13,6 +13,7 @@
# 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/>.
+import os
directories = {
"data": "/usr/share/vyos/",
@@ -24,8 +25,8 @@ directories = {
"templates": "/usr/share/vyos/templates/",
"certbot": "/config/auth/letsencrypt",
"api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/",
- "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/"
-
+ "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/",
+ "vyos_udev_dir": "/run/udev/vyos"
}
cfg_group = 'vyattacfg'
@@ -34,7 +35,7 @@ cfg_vintage = 'vyos'
commit_lock = '/opt/vyatta/config/.lock'
-version_file = '/usr/share/vyos/component-versions.json'
+component_version_json = os.path.join(directories['data'], 'component-versions.json')
https_data = {
'listen_addresses' : { '*': ['_'] }
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index bc103959a..eb5b0a456 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -13,44 +13,92 @@
# 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/>.
+import os
+import re
+
from vyos.util import popen
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},
+ # 'tls-hw-tx-offload': {'fixed': True, 'enabled': False},
+ # 'tx-checksum-fcoe-crc': {'fixed': True, 'enabled': False},
+ # 'tx-checksum-ip-generic': {'fixed': False, 'enabled': True},
+ # 'tx-checksum-ipv4': {'fixed': True, 'enabled': False},
+ # 'tx-checksum-ipv6': {'fixed': True, 'enabled': False},
+ # 'tx-checksum-sctp': {'fixed': True, 'enabled': False},
+ # 'tx-checksumming': {'fixed': False, 'enabled': True},
+ # 'tx-esp-segmentation': {'fixed': True, 'enabled': False},
# }
- features = { }
- ring_buffers = { }
+ _features = { }
+ # dictionary containing available interface speed and duplex settings
+ # {
+ # '10' : {'full': '', 'half': ''},
+ # '100' : {'full': '', 'half': ''},
+ # '1000': {'full': ''}
+ # }
+ _speed_duplex = { }
+ _ring_buffers = { }
+ _ring_buffers_max = { }
+ _driver_name = None
+ _auto_negotiation = False
+ _flow_control = False
+ _flow_control_enabled = None
def __init__(self, ifname):
+ # Get driver used for interface
+ sysfs_file = f'/sys/class/net/{ifname}/device/driver/module'
+ if os.path.exists(sysfs_file):
+ link = os.readlink(sysfs_file)
+ self._driver_name = os.path.basename(link)
+
+ if not self._driver_name:
+ raise ValueError(f'Could not determine driver for interface {ifname}!')
+
+ # Build a dictinary of supported link-speed and dupley settings.
+ out, err = popen(f'ethtool {ifname}')
+ reading = False
+ pattern = re.compile(r'\d+base.*')
+ for line in out.splitlines()[1:]:
+ line = line.lstrip()
+ if 'Supported link modes:' in line:
+ reading = True
+ if 'Supported pause frame use:' in line:
+ reading = False
+ if reading:
+ for block in line.split():
+ if pattern.search(block):
+ speed = block.split('base')[0]
+ duplex = block.split('/')[-1].lower()
+ if speed not in self._speed_duplex:
+ self._speed_duplex.update({ speed : {}})
+ if duplex not in self._speed_duplex[speed]:
+ self._speed_duplex[speed].update({ duplex : ''})
+ if 'Auto-negotiation:' in line:
+ # Split the following string: Auto-negotiation: off
+ # we are only interested in off or on
+ tmp = line.split()[-1]
+ self._auto_negotiation = bool(tmp == 'on')
+
# Now populate features dictionaty
- out, err = popen(f'ethtool -k {ifname}')
+ out, err = popen(f'ethtool --show-features {ifname}')
# skip the first line, it only says: "Features for eth0":
for line in out.splitlines()[1:]:
if ":" in line:
key, value = [s.strip() for s in line.strip().split(":", 1)]
- fixed = "fixed" in value
+ fixed = bool('fixed' in value)
if fixed:
value = value.split()[0].strip()
- self.features[key.strip()] = {
- "on": value == "on",
- "fixed": fixed
+ self._features[key.strip()] = {
+ 'enabled' : bool(value == 'on'),
+ 'fixed' : fixed
}
- out, err = popen(f'ethtool -g {ifname}')
+ out, err = popen(f'ethtool --show-ring {ifname}')
# We are only interested in line 2-5 which contains the device maximum
# ringbuffers
for line in out.splitlines()[2:6]:
@@ -61,45 +109,104 @@ class Ethtool:
# output format from 0 -> n/a. As we are only interested in the
# tx/rx keys we do not care about RX Mini/Jumbo.
if value.isdigit():
- self.ring_buffers[key] = int(value)
+ self._ring_buffers_max[key] = value
+ # Now we wan't to get the current RX/TX ringbuffer values - used for
+ for line in out.splitlines()[7:11]:
+ if ':' in line:
+ key, value = [s.strip() for s in line.strip().split(":", 1)]
+ key = key.lower().replace(' ', '_')
+ # T3645: ethtool version used on Debian Bullseye changed the
+ # output format from 0 -> n/a. As we are only interested in the
+ # tx/rx keys we do not care about RX Mini/Jumbo.
+ if value.isdigit():
+ self._ring_buffers[key] = value
+
+ # Get current flow control settings, but this is not supported by
+ # all NICs (e.g. vmxnet3 does not support is)
+ out, err = popen(f'ethtool --show-pause {ifname}')
+ if len(out.splitlines()) > 1:
+ self._flow_control = True
+ # read current flow control setting, this returns:
+ # ['Autonegotiate:', 'on']
+ self._flow_control_enabled = out.splitlines()[1].split()[-1]
+
+ def get_auto_negotiation(self):
+ return self._auto_negotiation
+
+ def get_driver_name(self):
+ return self._driver_name
+ def _get_generic(self, feature):
+ """
+ Generic method to read self._features and return a tuple for feature
+ enabled and feature is fixed.
- 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)
+ In case of a missing key, return "fixed = True and enabled = False"
+ """
+ fixed = True
+ enabled = False
+ if feature in self._features:
+ if 'enabled' in self._features[feature]:
+ enabled = self._features[feature]['enabled']
+ if 'fixed' in self._features[feature]:
+ fixed = self._features[feature]['fixed']
+ return enabled, fixed
- 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 get_generic_receive_offload(self):
+ return self._get_generic('generic-receive-offload')
- 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 get_generic_segmentation_offload(self):
+ return self._get_generic('generic-segmentation-offload')
- 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 get_large_receive_offload(self):
+ return self._get_generic('large-receive-offload')
- 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 get_scatter_gather(self):
+ return self._get_generic('scatter-gather')
- 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_tcp_segmentation_offload(self):
+ return self._get_generic('tcp-segmentation-offload')
- def get_rx_buffer(self):
- # Configuration of RX ring-buffers is not supported on every device,
+ def get_ring_buffer_max(self, rx_tx):
+ # Configuration of RX/TX ring-buffers is not supported on every device,
# thus when it's impossible return None
- return self.ring_buffers.get('rx', None)
+ if rx_tx not in ['rx', 'tx']:
+ ValueError('Ring-buffer type must be either "rx" or "tx"')
+ return self._ring_buffers_max.get(rx_tx, None)
- def get_tx_buffer(self):
- # Configuration of TX ring-buffers is not supported on every device,
+ def get_ring_buffer(self, rx_tx):
+ # Configuration of RX/TX ring-buffers is not supported on every device,
# thus when it's impossible return None
- return self.ring_buffers.get('tx', None)
+ if rx_tx not in ['rx', 'tx']:
+ ValueError('Ring-buffer type must be either "rx" or "tx"')
+ return str(self._ring_buffers.get(rx_tx, None))
+
+ def check_speed_duplex(self, speed, duplex):
+ """ Check if the passed speed and duplex combination is supported by
+ the underlaying network adapter. """
+ if isinstance(speed, int):
+ speed = str(speed)
+ if speed != 'auto' and not speed.isdigit():
+ raise ValueError(f'Value "{speed}" for speed is invalid!')
+ if duplex not in ['auto', 'full', 'half']:
+ raise ValueError(f'Value "{duplex}" for duplex is invalid!')
+
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ return False
+
+ if speed in self._speed_duplex:
+ if duplex in self._speed_duplex[speed]:
+ return True
+ return False
+
+ def check_flow_control(self):
+ """ Check if the NIC supports flow-control """
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ return False
+ return self._flow_control
+
+ def get_flow_control(self):
+ if self._flow_control_enabled == None:
+ raise ValueError('Interface does not support changing '\
+ 'flow-control settings!')
+ return self._flow_control_enabled
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index 14f64a8de..27073b266 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -366,5 +366,4 @@ class BridgeIf(Interface):
cmd = f'bridge vlan add dev {interface} vid {native_vlan_id} pvid untagged master'
self._cmd(cmd)
- # call base class first
super().update(config)
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index d41dfef47..7a6b36e7c 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -18,11 +18,12 @@ import os
from inspect import signature
from inspect import _empty
-from vyos import debug
+from vyos.ifconfig.section import Section
from vyos.util import popen
from vyos.util import cmd
-from vyos.ifconfig.section import Section
-
+from vyos.util import read_file
+from vyos.util import write_file
+from vyos import debug
class Control(Section):
_command_get = {}
@@ -116,20 +117,18 @@ class Control(Section):
Provide a single primitive w/ error checking for reading from sysfs.
"""
value = None
- with open(filename, 'r') as f:
- value = f.read().rstrip('\n')
-
- self._debug_msg("read '{}' < '{}'".format(value, filename))
+ if os.path.exists(filename):
+ value = read_file(filename)
+ self._debug_msg("read '{}' < '{}'".format(value, filename))
return value
def _write_sysfs(self, filename, value):
"""
Provide a single primitive w/ error checking for writing to sysfs.
"""
- self._debug_msg("write '{}' > '{}'".format(value, filename))
if os.path.isfile(filename):
- with open(filename, 'w') as f:
- f.write(str(value))
+ write_file(filename, str(value))
+ self._debug_msg("write '{}' > '{}'".format(value, filename))
return True
return False
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 07b31a12a..2e59a7afc 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -16,9 +16,11 @@
import os
import re
+from vyos.ethtool import Ethtool
from vyos.ifconfig.interface import Interface
from vyos.util import run
from vyos.util import dict_search
+from vyos.util import read_file
from vyos.validate import assert_list
@Interface.register
@@ -41,39 +43,29 @@ class EthernetIf(Interface):
@staticmethod
def feature(ifname, option, value):
- run(f'ethtool -K {ifname} {option} {value}','ifconfig')
+ run(f'ethtool --features {ifname} {option} {value}')
return False
_command_set = {**Interface._command_set, **{
'gro': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'gro', v),
- # 'shellcmd': 'ethtool -K {ifname} gro {value}',
},
'gso': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'gso', v),
- # 'shellcmd': 'ethtool -K {ifname} gso {value}',
},
'lro': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'lro', v),
- # 'shellcmd': 'ethtool -K {ifname} lro {value}',
},
'sg': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'sg', v),
- # 'shellcmd': 'ethtool -K {ifname} sg {value}',
},
'tso': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'tso', v),
- # 'shellcmd': 'ethtool -K {ifname} tso {value}',
- },
- 'ufo': {
- 'validate': lambda v: assert_list(v, ['on', 'off']),
- 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v),
- # 'shellcmd': 'ethtool -K {ifname} ufo {value}',
},
}}
@@ -84,24 +76,9 @@ class EthernetIf(Interface):
},
}}
- def get_driver_name(self):
- """
- Return the driver name used by NIC. Some NICs don't support all
- features e.g. changing link-speed, duplex
-
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.get_driver_name()
- 'vmxnet3'
- """
- ifname = self.config['ifname']
- sysfs_file = f'/sys/class/net/{ifname}/device/driver/module'
- if os.path.exists(sysfs_file):
- link = os.readlink(sysfs_file)
- return os.path.basename(link)
- else:
- return None
+ def __init__(self, ifname, **kargs):
+ super().__init__(ifname, **kargs)
+ self.ethtool = Ethtool(ifname)
def set_flow_control(self, enable):
"""
@@ -119,44 +96,20 @@ class EthernetIf(Interface):
if enable not in ['on', 'off']:
raise ValueError("Value out of range")
- driver_name = self.get_driver_name()
- if driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']:
- self._debug_msg(f'{driver_name} driver does not support changing '\
- 'flow control settings!')
- return
-
- # Get current flow control settings:
- cmd = f'ethtool --show-pause {ifname}'
- output, code = self._popen(cmd)
- if code == 76:
- # the interface does not support it
- return ''
- if code:
- # never fail here as it prevent vyos to boot
- print(f'unexpected return code {code} from {cmd}')
- return ''
-
- # The above command returns - with tabs:
- #
- # Pause parameters for eth0:
- # Autonegotiate: on
- # RX: off
- # TX: off
- if re.search("Autonegotiate:\ton", output):
- if enable == "on":
- # flowcontrol is already enabled - no need to re-enable it again
- # this will prevent the interface from flapping as applying the
- # flow-control settings will take the interface down and bring
- # it back up every time.
- return ''
-
- # Assemble command executed on system. Unfortunately there is no way
- # to change this setting via sysfs
- cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
- output, code = self._popen(cmd)
- if code:
- print(f'could not set flowcontrol for {ifname}')
- return output
+ if not self.ethtool.check_flow_control():
+ self._debug_msg(f'NIC driver does not support changing flow control settings!')
+ return False
+
+ current = self.ethtool.get_flow_control()
+ if current != enable:
+ # Assemble command executed on system. Unfortunately there is no way
+ # to change this setting via sysfs
+ cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
+ output, code = self._popen(cmd)
+ if code:
+ print(f'Could not set flowcontrol for {ifname}')
+ return output
+ return None
def set_speed_duplex(self, speed, duplex):
"""
@@ -178,40 +131,28 @@ class EthernetIf(Interface):
if duplex not in ['auto', 'full', 'half']:
raise ValueError("Value out of range (duplex)")
- driver_name = self.get_driver_name()
- if driver_name in ['vmxnet3', 'virtio_net', 'xen_netfront']:
- self._debug_msg(f'{driver_name} driver does not support changing '\
- 'speed/duplex settings!')
+ if not self.ethtool.check_speed_duplex(speed, duplex):
+ self._debug_msg(f'NIC driver does not support changing speed/duplex settings!')
return
# Get current speed and duplex settings:
ifname = self.config['ifname']
- cmd = f'ethtool {ifname}'
- tmp = self._cmd(cmd)
-
- if re.search("\tAuto-negotiation: on", tmp):
+ if self.ethtool.get_auto_negotiation():
if speed == 'auto' and duplex == 'auto':
# bail out early as nothing is to change
return
else:
- # read in current speed and duplex settings
- cur_speed = 0
- cur_duplex = ''
- for line in tmp.splitlines():
- if line.lstrip().startswith("Speed:"):
- non_decimal = re.compile(r'[^\d.]+')
- cur_speed = non_decimal.sub('', line)
- continue
-
- if line.lstrip().startswith("Duplex:"):
- cur_duplex = line.split()[-1].lower()
- break
-
+ # XXX: read in current speed and duplex settings
+ # There are some "nice" NICs like AX88179 which do not support
+ # reading the speed thus we simply fallback to the supplied speed
+ # to not cause any change here and raise an exception.
+ cur_speed = read_file(f'/sys/class/net/{ifname}/speed', speed)
+ cur_duplex = read_file(f'/sys/class/net/{ifname}/duplex', duplex)
if (cur_speed == speed) and (cur_duplex == duplex):
# bail out early as nothing is to change
return
- cmd = f'ethtool -s {ifname}'
+ cmd = f'ethtool --change {ifname}'
if speed == 'auto' or duplex == 'auto':
cmd += ' autoneg on'
else:
@@ -228,8 +169,15 @@ class EthernetIf(Interface):
>>> i.set_gro(True)
"""
if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('gro', 'on' if state else 'off')
+ raise ValueError('Value out of range')
+
+ enabled, fixed = self.ethtool.get_generic_receive_offload()
+ if enabled != state:
+ if not fixed:
+ return self.set_interface('gro', 'on' if state else 'off')
+ else:
+ print('Adapter does not support changing generic-receive-offload settings!')
+ return False
def set_gso(self, state):
"""
@@ -240,8 +188,15 @@ class EthernetIf(Interface):
>>> i.set_gso(True)
"""
if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('gso', 'on' if state else 'off')
+ raise ValueError('Value out of range')
+
+ enabled, fixed = self.ethtool.get_generic_segmentation_offload()
+ if enabled != state:
+ if not fixed:
+ return self.set_interface('gso', 'on' if state else 'off')
+ else:
+ print('Adapter does not support changing generic-segmentation-offload settings!')
+ return False
def set_lro(self, state):
"""
@@ -252,12 +207,19 @@ class EthernetIf(Interface):
>>> i.set_lro(True)
"""
if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('lro', 'on' if state else 'off')
+ raise ValueError('Value out of range')
+
+ enabled, fixed = self.ethtool.get_large_receive_offload()
+ if enabled != state:
+ if not fixed:
+ return self.set_interface('gro', 'on' if state else 'off')
+ else:
+ print('Adapter does not support changing large-receive-offload settings!')
+ return False
def set_rps(self, state):
if not isinstance(state, bool):
- raise ValueError("Value out of range")
+ raise ValueError('Value out of range')
rps_cpus = '0'
if state:
@@ -282,8 +244,15 @@ class EthernetIf(Interface):
>>> i.set_sg(True)
"""
if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('sg', 'on' if state else 'off')
+ raise ValueError('Value out of range')
+
+ enabled, fixed = self.ethtool.get_scatter_gather()
+ if enabled != state:
+ if not fixed:
+ return self.set_interface('gro', 'on' if state else 'off')
+ else:
+ print('Adapter does not support changing scatter-gather settings!')
+ return False
def set_tso(self, state):
"""
@@ -295,40 +264,38 @@ class EthernetIf(Interface):
>>> i.set_tso(False)
"""
if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('tso', 'on' if state else 'off')
-
- def set_ufo(self, state):
- """
- Enable UDP fragmentation offloading. State can be either True or False.
-
- Example:
- >>> from vyos.ifconfig import EthernetIf
- >>> i = EthernetIf('eth0')
- >>> i.set_udp_offload(True)
- """
- if not isinstance(state, bool):
- raise ValueError("Value out of range")
- return self.set_interface('ufo', 'on' if state else 'off')
+ raise ValueError('Value out of range')
+
+ enabled, fixed = self.ethtool.get_tcp_segmentation_offload()
+ if enabled != state:
+ if not fixed:
+ return self.set_interface('gro', 'on' if state else 'off')
+ else:
+ print('Adapter does not support changing tcp-segmentation-offload settings!')
+ return False
- def set_ring_buffer(self, b_type, b_size):
+ def set_ring_buffer(self, rx_tx, size):
"""
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_ring_buffer('rx', '4096')
"""
+ current_size = self.ethtool.get_ring_buffer(rx_tx)
+ if current_size == size:
+ # bail out early if nothing is about to change
+ return None
+
ifname = self.config['ifname']
- cmd = f'ethtool -G {ifname} {b_type} {b_size}'
+ cmd = f'ethtool --set-ring {ifname} {rx_tx} {size}'
output, code = self._popen(cmd)
# ethtool error codes:
# 80 - value already setted
# 81 - does not possible to set value
if code and code != 80:
- print(f'could not set "{b_type}" ring-buffer for {ifname}')
+ print(f'could not set "{rx_tx}" ring-buffer for {ifname}')
return output
-
def update(self, config):
""" General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
@@ -357,9 +324,6 @@ class EthernetIf(Interface):
# TSO (TCP segmentation offloading)
self.set_tso(dict_search('offload.tso', config) != None)
- # UDP fragmentation offloading
- self.set_ufo(dict_search('offload.ufo', config) != None)
-
# Set physical interface speed and duplex
if {'speed', 'duplex'} <= set(config):
speed = config.get('speed')
@@ -368,8 +332,8 @@ class EthernetIf(Interface):
# Set interface ring buffer
if 'ring_buffer' in config:
- for b_type in config['ring_buffer']:
- self.set_ring_buffer(b_type, config['ring_buffer'][b_type])
+ for rx_tx, size in config['ring_buffer'].items():
+ self.set_ring_buffer(rx_tx, size)
# call base class first
super().update(config)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index a1928ba51..e6dbd861b 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -37,7 +37,9 @@ 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.util import is_systemd_service_active
from vyos.template import is_ipv4
+from vyos.template import is_ipv6
from vyos.validate import is_intf_addr_assigned
from vyos.validate import is_ipv6_link_local
from vyos.validate import assert_boolean
@@ -52,6 +54,9 @@ from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.operational import Operational
from vyos.ifconfig import Section
+from netaddr import EUI
+from netaddr import mac_unix_expanded
+
class Interface(Control):
# This is the class which will be used to create
# self.operational, it allows subclasses, such as
@@ -103,6 +108,10 @@ class Interface(Control):
'shellcmd': 'ip -json -detail link list dev {ifname}',
'format': lambda j: jmespath.search('[*].operstate | [0]', json.loads(j)),
},
+ 'vrf': {
+ 'shellcmd': 'ip -json -detail link list dev {ifname}',
+ 'format': lambda j: jmespath.search('[*].master | [0]', json.loads(j)),
+ },
}
_command_set = {
@@ -134,7 +143,6 @@ class Interface(Control):
_sysfs_set = {
'arp_cache_tmo': {
- 'convert': lambda tmo: (int(tmo) * 1000),
'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms',
},
'arp_filter': {
@@ -204,6 +212,51 @@ class Interface(Control):
},
}
+ _sysfs_get = {
+ 'arp_cache_tmo': {
+ 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms',
+ },
+ 'arp_filter': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter',
+ },
+ 'arp_accept': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept',
+ },
+ 'arp_announce': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce',
+ },
+ 'arp_ignore': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
+ },
+ 'ipv4_forwarding': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding',
+ },
+ 'rp_filter': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter',
+ },
+ 'ipv6_accept_ra': {
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
+ 'ipv6_autoconf': {
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
+ },
+ 'ipv6_forwarding': {
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding',
+ },
+ 'ipv6_dad_transmits': {
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits',
+ },
+ 'proxy_arp': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp',
+ },
+ 'proxy_arp_pvlan': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan',
+ },
+ 'link_detect': {
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter',
+ },
+ }
+
@classmethod
def exists(cls, ifname):
return os.path.exists(f'/sys/class/net/{ifname}')
@@ -322,9 +375,7 @@ class Interface(Control):
'info_data', {}).get('table')
# Add map element with interface and zone ID
if vrf_table_id:
- self._cmd(
- f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}'
- )
+ self._cmd(f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}')
else:
nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
# Check if deleting is possible first to avoid raising errors
@@ -376,6 +427,9 @@ class Interface(Control):
>>> Interface('eth0').get_mtu()
'1400'
"""
+ tmp = self.get_interface('mtu')
+ if str(tmp) == mtu:
+ return None
return self.set_interface('mtu', mtu)
def get_mac(self):
@@ -389,6 +443,47 @@ class Interface(Control):
"""
return self.get_interface('mac')
+ def get_mac_synthetic(self):
+ """
+ Get a synthetic MAC address. This is a common method which can be called
+ from derived classes to overwrite the get_mac() call in a generic way.
+
+ NOTE: Tunnel interfaces have no "MAC" address by default. The content
+ of the 'address' file in /sys/class/net/device contains the
+ local-ip thus we generate a random MAC address instead
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_mac()
+ '00:50:ab:cd:ef:00'
+ """
+ from hashlib import sha256
+
+ # Get processor ID number
+ cpu_id = self._cmd('sudo dmidecode -t 4 | grep ID | head -n1 | sed "s/.*ID://;s/ //g"')
+ # Get system eth0 base MAC address - every system has eth0
+ eth0_mac = Interface('eth0').get_mac()
+
+ sha = sha256()
+ # Calculate SHA256 sum based on the CPU ID number, eth0 mac address and
+ # this interface identifier - this is as predictable as an interface
+ # MAC address and thus can be used in the same way
+ sha.update(cpu_id.encode())
+ sha.update(eth0_mac.encode())
+ sha.update(self.ifname.encode())
+ # take the most significant 48 bits from the SHA256 string
+ tmp = sha.hexdigest()[:12]
+ # Convert pseudo random string into EUI format which now represents a
+ # MAC address
+ tmp = EUI(tmp).value
+ # set locally administered bit in MAC address
+ tmp |= 0xf20000000000
+ # convert integer to "real" MAC address representation
+ mac = EUI(hex(tmp).split('x')[-1])
+ # change dialect to use : as delimiter instead of -
+ mac.dialect = mac_unix_expanded
+ return str(mac)
+
def set_mac(self, mac):
"""
Set interface MAC (Media Access Contrl) address to given value.
@@ -413,7 +508,7 @@ class Interface(Control):
if prev_state == 'up':
self.set_admin_state('up')
- def set_vrf(self, vrf=''):
+ def set_vrf(self, vrf):
"""
Add/Remove interface from given VRF instance.
@@ -422,6 +517,11 @@ class Interface(Control):
>>> Interface('eth0').set_vrf('foo')
>>> Interface('eth0').set_vrf()
"""
+
+ tmp = self.get_interface('vrf')
+ if tmp == vrf:
+ return None
+
self.set_interface('vrf', vrf)
self._set_vrf_ct_zone(vrf)
@@ -434,8 +534,68 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_arp_cache_tmo(40)
"""
+ tmo = str(int(tmo) * 1000)
+ tmp = self.get_interface('arp_cache_tmo')
+ if tmp == tmo:
+ return None
return self.set_interface('arp_cache_tmo', tmo)
+ def set_tcp_ipv4_mss(self, mss):
+ """
+ Set IPv4 TCP MSS value advertised when TCP SYN packets leave this
+ interface. Value is in bytes.
+
+ A value of 0 will disable the MSS adjustment
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_tcp_ipv4_mss(1340)
+ """
+ iptables_bin = 'iptables'
+ base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
+ out = self._cmd(f'{iptables_bin}-save -t mangle')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ # remove OLD MSS mangling configuration
+ line = line.replace('-A FORWARD', '-D FORWARD')
+ self._cmd(f'{iptables_bin} -t mangle {line}')
+
+ cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+ if mss == 'clamp-mss-to-pmtu':
+ self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+ elif int(mss) > 0:
+ # probably add option to clamp only if bigger:
+ low_mss = str(int(mss) + 1)
+ self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+
+ def set_tcp_ipv6_mss(self, mss):
+ """
+ Set IPv6 TCP MSS value advertised when TCP SYN packets leave this
+ interface. Value is in bytes.
+
+ A value of 0 will disable the MSS adjustment
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_tcp_mss(1320)
+ """
+ iptables_bin = 'ip6tables'
+ base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
+ out = self._cmd(f'{iptables_bin}-save -t mangle')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ # remove OLD MSS mangling configuration
+ line = line.replace('-A FORWARD', '-D FORWARD')
+ self._cmd(f'{iptables_bin} -t mangle {line}')
+
+ cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+ if mss == 'clamp-mss-to-pmtu':
+ self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+ elif int(mss) > 0:
+ # probably add option to clamp only if bigger:
+ low_mss = str(int(mss) + 1)
+ self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+
def set_arp_filter(self, arp_filter):
"""
Filter ARP requests
@@ -454,6 +614,9 @@ class Interface(Control):
particular interfaces. Only for more complex setups like load-
balancing, does this behaviour cause problems.
"""
+ tmp = self.get_interface('arp_filter')
+ if tmp == arp_filter:
+ return None
return self.set_interface('arp_filter', arp_filter)
def set_arp_accept(self, arp_accept):
@@ -470,6 +633,9 @@ class Interface(Control):
gratuitous arp frame, the arp table will be updated regardless
if this setting is on or off.
"""
+ tmp = self.get_interface('arp_accept')
+ if tmp == arp_accept:
+ return None
return self.set_interface('arp_accept', arp_accept)
def set_arp_announce(self, arp_announce):
@@ -491,6 +657,9 @@ class Interface(Control):
receiving answer from the resolved target while decreasing
the level announces more valid sender's information.
"""
+ tmp = self.get_interface('arp_announce')
+ if tmp == arp_announce:
+ return None
return self.set_interface('arp_announce', arp_announce)
def set_arp_ignore(self, arp_ignore):
@@ -503,12 +672,16 @@ class Interface(Control):
1 - reply only if the target IP address is local address
configured on the incoming interface
"""
+ tmp = self.get_interface('arp_ignore')
+ if tmp == arp_ignore:
+ return None
return self.set_interface('arp_ignore', arp_ignore)
def set_ipv4_forwarding(self, forwarding):
- """
- Configure IPv4 forwarding.
- """
+ """ Configure IPv4 forwarding. """
+ tmp = self.get_interface('ipv4_forwarding')
+ if tmp == forwarding:
+ return None
return self.set_interface('ipv4_forwarding', forwarding)
def set_ipv4_source_validation(self, value):
@@ -537,6 +710,9 @@ class Interface(Control):
print(f'WARNING: Global source-validation is set to "{global_setting}\n"' \
'this overrides per interface setting!')
+ tmp = self.get_interface('rp_filter')
+ if int(tmp) == value:
+ return None
return self.set_interface('rp_filter', value)
def set_ipv6_accept_ra(self, accept_ra):
@@ -552,6 +728,9 @@ class Interface(Control):
2 - Overrule forwarding behaviour. Accept Router Advertisements even if
forwarding is enabled.
"""
+ tmp = self.get_interface('ipv6_accept_ra')
+ if tmp == accept_ra:
+ return None
return self.set_interface('ipv6_accept_ra', accept_ra)
def set_ipv6_autoconf(self, autoconf):
@@ -559,6 +738,9 @@ class Interface(Control):
Autoconfigure addresses using Prefix Information in Router
Advertisements.
"""
+ tmp = self.get_interface('ipv6_autoconf')
+ if tmp == autoconf:
+ return None
return self.set_interface('ipv6_autoconf', autoconf)
def add_ipv6_eui64_address(self, prefix):
@@ -582,9 +764,10 @@ class Interface(Control):
Delete the address based on the interface's MAC-based EUI64
combined with the prefix address.
"""
- eui64 = mac2eui64(self.get_mac(), prefix)
- prefixlen = prefix.split('/')[1]
- self.del_addr(f'{eui64}/{prefixlen}')
+ if is_ipv6(prefix):
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.del_addr(f'{eui64}/{prefixlen}')
def set_ipv6_forwarding(self, forwarding):
"""
@@ -611,6 +794,9 @@ class Interface(Control):
3. Router Advertisements are ignored unless accept_ra is 2.
4. Redirects are ignored.
"""
+ tmp = self.get_interface('ipv6_forwarding')
+ if tmp == forwarding:
+ return None
return self.set_interface('ipv6_forwarding', forwarding)
def set_ipv6_dad_messages(self, dad):
@@ -618,6 +804,9 @@ class Interface(Control):
The amount of Duplicate Address Detection probes to send.
Default: 1
"""
+ tmp = self.get_interface('ipv6_dad_transmits')
+ if tmp == dad:
+ return None
return self.set_interface('ipv6_dad_transmits', dad)
def set_link_detect(self, link_filter):
@@ -640,6 +829,9 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_link_detect(1)
"""
+ tmp = self.get_interface('link_detect')
+ if tmp == link_filter:
+ return None
return self.set_interface('link_detect', link_filter)
def get_alias(self):
@@ -664,6 +856,9 @@ class Interface(Control):
>>> Interface('eth0').set_ifalias('')
"""
+ tmp = self.get_interface('alias')
+ if tmp == ifalias:
+ return None
self.set_interface('alias', ifalias)
def get_admin_state(self):
@@ -739,6 +934,9 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_proxy_arp(1)
"""
+ tmp = self.get_interface('proxy_arp')
+ if tmp == enable:
+ return None
self.set_interface('proxy_arp', enable)
def set_proxy_arp_pvlan(self, enable):
@@ -765,6 +963,9 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_proxy_arp_pvlan(1)
"""
+ tmp = self.get_interface('proxy_arp_pvlan')
+ if tmp == enable:
+ return None
self.set_interface('proxy_arp_pvlan', enable)
def get_addr_v4(self):
@@ -899,6 +1100,8 @@ class Interface(Control):
>>> j.get_addr()
['2001:db8::ffff/64']
"""
+ if not addr:
+ raise ValueError()
# remove from interface
if addr == 'dhcp':
@@ -1005,7 +1208,9 @@ class Interface(Control):
lease_file = f'{config_base}_{ifname}.leases'
# Stop client with old config files to get the right IF_METRIC.
- self._cmd(f'systemctl stop dhclient@{ifname}.service')
+ systemd_service = f'dhclient@{ifname}.service'
+ if is_systemd_service_active(systemd_service):
+ self._cmd(f'systemctl stop {systemd_service}')
if enable and 'disable' not in self._config:
if dict_search('dhcp_options.host_name', self._config) == None:
@@ -1025,7 +1230,7 @@ class Interface(Control):
# 'up' check is mandatory b/c even if the interface is A/D, as soon as
# the DHCP client is started the interface will be placed in u/u state.
# This is not what we intended to do when disabling an interface.
- return self._cmd(f'systemctl start dhclient@{ifname}.service')
+ return self._cmd(f'systemctl restart {systemd_service}')
else:
# cleanup old config files
for file in [config_file, options_file, pid_file, lease_file]:
@@ -1042,17 +1247,18 @@ class Interface(Control):
ifname = self.ifname
config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+ systemd_service = f'dhcp6c@{ifname}.service'
if enable and 'disable' not in self._config:
render(config_file, 'dhcp-client/ipv6.tmpl',
self._config)
- # We must ignore any return codes. This is required to enable DHCPv6-PD
- # for interfaces which are yet not up and running.
- return self._popen(f'systemctl restart dhcp6c@{ifname}.service')
+ # We must ignore any return codes. This is required to enable
+ # DHCPv6-PD for interfaces which are yet not up and running.
+ return self._popen(f'systemctl restart {systemd_service}')
else:
- self._popen(f'systemctl stop dhcp6c@{ifname}.service')
-
+ if is_systemd_service_active(systemd_service):
+ self._cmd(f'systemctl stop {systemd_service}')
if os.path.isfile(config_file):
os.remove(config_file)
@@ -1069,12 +1275,14 @@ 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 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)
+ # Check configuration stored by old perl code before delete T3782
+ if not 'redirect' in self._config:
+ # 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 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
if not config:
@@ -1097,7 +1305,6 @@ class Interface(Control):
mirror_cmd += f'tc filter add dev {source_if} parent {parent} protocol all prio 10 u32 match u32 0 0 flowid 1:1 action mirred egress mirror dev {mirror_if}'
self._popen(mirror_cmd)
-
def set_xdp(self, state):
"""
Enable Kernel XDP support. State can be either True or False.
@@ -1174,16 +1381,16 @@ class Interface(Control):
# determine IP addresses which are assigned to the interface and build a
# list of addresses which are no longer in the dict so they can be removed
- cur_addr = self.get_addr()
- for addr in list_diff(cur_addr, new_addr):
- # we will delete all interface specific IP addresses if they are not
- # explicitly configured on the CLI
- if is_ipv6_link_local(addr):
- eui64 = mac2eui64(self.get_mac(), 'fe80::/64')
- if addr != f'{eui64}/64':
+ if 'address_old' in config:
+ for addr in list_diff(config['address_old'], new_addr):
+ # we will delete all interface specific IP addresses if they are not
+ # explicitly configured on the CLI
+ if is_ipv6_link_local(addr):
+ eui64 = mac2eui64(self.get_mac(), 'fe80::/64')
+ if addr != f'{eui64}/64':
+ self.del_addr(addr)
+ else:
self.del_addr(addr)
- else:
- self.del_addr(addr)
for addr in new_addr:
self.add_addr(addr)
@@ -1202,6 +1409,16 @@ class Interface(Control):
# checked before
self.set_vrf(config.get('vrf', ''))
+ # Configure MSS value for IPv4 TCP connections
+ tmp = dict_search('ip.adjust_mss', config)
+ value = tmp if (tmp != None) else '0'
+ self.set_tcp_ipv4_mss(value)
+
+ # Configure MSS value for IPv6 TCP connections
+ tmp = dict_search('ipv6.adjust_mss', config)
+ value = tmp if (tmp != None) else '0'
+ self.set_tcp_ipv6_mss(value)
+
# Configure ARP cache timeout in milliseconds - has default value
tmp = dict_search('ip.arp_cache_timeout', config)
value = tmp if (tmp != None) else '30'
@@ -1274,16 +1491,11 @@ class Interface(Control):
self.set_mtu(config.get('mtu'))
# Delete old IPv6 EUI64 addresses before changing MAC
- tmp = dict_search('ipv6.address.eui64_old', config)
- if tmp:
- for addr in tmp:
- self.del_ipv6_eui64_address(addr)
+ for addr in (dict_search('ipv6.address.eui64_old', config) or []):
+ self.del_ipv6_eui64_address(addr)
# Manage IPv6 link-local addresses
- tmp = dict_search('ipv6.address.no_default_link_local', config)
- # we must check explicitly for None type as if the key is set we will
- # get an empty dict (<class 'dict'>)
- if isinstance(tmp, dict):
+ if dict_search('ipv6.address.no_default_link_local', config) != None:
self.del_ipv6_eui64_address('fe80::/64')
else:
self.add_ipv6_eui64_address('fe80::/64')
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
index 65575cf99..1d13264bf 100644
--- a/python/vyos/ifconfig/pppoe.py
+++ b/python/vyos/ifconfig/pppoe.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
@@ -14,12 +14,11 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
from vyos.ifconfig.interface import Interface
+from vyos.util import get_interface_config
@Interface.register
class PPPoEIf(Interface):
- default = {
- 'type': 'pppoe',
- }
+ iftype = 'pppoe'
definition = {
**Interface.definition,
**{
@@ -28,7 +27,31 @@ class PPPoEIf(Interface):
},
}
- # stub this interface is created in the configure script
+ def _remove_routes(self, vrf=''):
+ # Always delete default routes when interface is removed
+ if vrf:
+ vrf = f'-c "vrf {vrf}"'
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "no ip route 0.0.0.0/0 {self.ifname} tag 210"')
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "no ipv6 route ::/0 {self.ifname} tag 210"')
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('pppoe0')
+ >>> i.remove()
+ """
+
+ tmp = get_interface_config(self.ifname)
+ vrf = ''
+ if 'master' in tmp:
+ self._remove_routes(tmp['master'])
+
+ # remove bond master which places members in disabled state
+ super().remove()
def _create(self):
# we can not create this interface as it is managed outside
@@ -37,3 +60,92 @@ class PPPoEIf(Interface):
def _delete(self):
# we can not create this interface as it is managed outside
pass
+
+ def del_addr(self, addr):
+ # we can not create this interface as it is managed outside
+ pass
+
+ def get_mac(self):
+ """ Get a synthetic MAC address. """
+ return self.get_mac_synthetic()
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # 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()?
+ #
+ # We need to copy this from super().update() as we utilize self.set_dhcpv6()
+ # before this is done by the base class.
+ self._config = config
+
+ # remove old routes from an e.g. old VRF assignment
+ vrf = ''
+ if 'vrf_old' in config:
+ vrf = config['vrf_old']
+ self._remove_routes(vrf)
+
+ # DHCPv6 PD handling is a bit different on PPPoE interfaces, as we do
+ # not require an 'address dhcpv6' CLI option as with other interfaces
+ if 'dhcpv6_options' in config and 'pd' in config['dhcpv6_options']:
+ self.set_dhcpv6(True)
+ else:
+ self.set_dhcpv6(False)
+
+ super().update(config)
+
+ if 'default_route' not in config or config['default_route'] == 'none':
+ return
+
+ #
+ # Set default routes pointing to pppoe interface
+ #
+ vrf = ''
+ sed_opt = '^ip route'
+
+ install_v4 = True
+ install_v6 = True
+
+ # generate proper configuration string when VRFs are in use
+ if 'vrf' in config:
+ tmp = config['vrf']
+ vrf = f'-c "vrf {tmp}"'
+ sed_opt = f'vrf {tmp}'
+
+ if config['default_route'] == 'auto':
+ # only add route if there is no default route present
+ tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"')
+ for line in tmp.splitlines():
+ line = line.lstrip()
+ if line.startswith('ip route 0.0.0.0/0'):
+ install_v4 = False
+ continue
+
+ if 'ipv6' in config and line.startswith('ipv6 route ::/0'):
+ install_v6 = False
+ continue
+
+ elif config['default_route'] == 'force':
+ # Force means that all static routes are replaced with the ones from this interface
+ tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"')
+ for line in tmp.splitlines():
+ if self.ifname in line:
+ # It makes no sense to remove a route with our interface and the later re-add it.
+ # This will only make traffic disappear - which is a no-no!
+ continue
+
+ line = line.lstrip()
+ if line.startswith('ip route 0.0.0.0/0'):
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"')
+
+ if 'ipv6' in config and line.startswith('ipv6 route ::/0'):
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"')
+
+ if install_v4:
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "ip route 0.0.0.0/0 {self.ifname} tag 210"')
+ if install_v6 and 'ipv6' in config:
+ self._cmd(f'vtysh -c "conf t" {vrf} -c "ipv6 route ::/0 {self.ifname} tag 210"')
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 173a90bb4..0e4447b9e 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -46,7 +46,7 @@ class Section:
return klass
@classmethod
- def _basename (cls, name, vlan):
+ def _basename(cls, name, vlan, vrrp):
"""
remove the number at the end of interface name
name: name of the interface
@@ -56,16 +56,18 @@ class Section:
name = name.rstrip('.')
if vlan:
name = name.rstrip('0123456789.')
+ if vrrp:
+ name = name.rstrip('0123456789v')
return name
@classmethod
- def section(cls, name, vlan=True):
+ def section(cls, name, vlan=True, vrrp=True):
"""
return the name of a section an interface should be under
name: name of the interface (eth0, dum1, ...)
vlan: should we try try to remove the VLAN from the number
"""
- name = cls._basename(name, vlan)
+ name = cls._basename(name, vlan, vrrp)
if name in cls._prefixes:
return cls._prefixes[name].definition['section']
@@ -79,8 +81,8 @@ class Section:
return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes]))
@classmethod
- def klass(cls, name, vlan=True):
- name = cls._basename(name, vlan)
+ def klass(cls, name, vlan=True, vrrp=True):
+ name = cls._basename(name, vlan, vrrp)
if name in cls._prefixes:
return cls._prefixes[name]
raise ValueError(f'No type found for interface name: {name}')
diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
index 64c735824..5258a2cb1 100644
--- a/python/vyos/ifconfig/tunnel.py
+++ b/python/vyos/ifconfig/tunnel.py
@@ -16,10 +16,6 @@
# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/
# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel
-from netaddr import EUI
-from netaddr import mac_unix_expanded
-from random import getrandbits
-
from vyos.ifconfig.interface import Interface
from vyos.util import dict_search
from vyos.validate import assert_list
@@ -163,28 +159,8 @@ class TunnelIf(Interface):
self._cmd(cmd.format(**self.config))
def get_mac(self):
- """
- Get current interface MAC (Media Access Contrl) address used.
-
- NOTE: Tunnel interfaces have no "MAC" address by default. The content
- of the 'address' file in /sys/class/net/device contains the
- local-ip thus we generate a random MAC address instead
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').get_mac()
- '00:50:ab:cd:ef:00'
- """
- # we choose 40 random bytes for the MAC address, this gives
- # us e.g. EUI('00-EA-EE-D6-A3-C8') or EUI('00-41-B9-0D-F2-2A')
- tmp = EUI(getrandbits(48)).value
- # set locally administered bit in MAC address
- tmp |= 0xf20000000000
- # convert integer to "real" MAC address representation
- mac = EUI(hex(tmp).split('x')[-1])
- # change dialect to use : as delimiter instead of -
- mac.dialect = mac_unix_expanded
- return str(mac)
+ """ Get a synthetic MAC address. """
+ return self.get_mac_synthetic()
def update(self, config):
""" General helper function which works on a dictionary retrived by
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
index b522cc1ab..47aaadecd 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -22,6 +22,7 @@ from time import sleep
from tabulate import tabulate
from vyos import util
+from vyos.configquery import ConfigTreeQuery
class VRRPError(Exception):
pass
@@ -32,14 +33,13 @@ class VRRPNoData(VRRPError):
class VRRP(object):
_vrrp_prefix = '00:00:5E:00:01:'
location = {
- 'pid': '/run/keepalived.pid',
- 'fifo': '/run/keepalived_notify_fifo',
+ 'pid': '/run/keepalived/keepalived.pid',
+ 'fifo': '/run/keepalived/keepalived_notify_fifo',
'state': '/tmp/keepalived.data',
'stats': '/tmp/keepalived.stats',
'json': '/tmp/keepalived.json',
'daemon': '/etc/default/keepalived',
- 'config': '/etc/keepalived/keepalived.conf',
- 'vyos': '/run/keepalived_config.dict',
+ 'config': '/run/keepalived/keepalived.conf',
}
_signal = {
@@ -111,17 +111,20 @@ class VRRP(object):
@classmethod
def disabled(cls):
- if not os.path.exists(cls.location['vyos']):
- return []
-
disabled = []
- config = json.loads(util.read_file(cls.location['vyos']))
-
- # add disabled groups to the list
- for group in config['vrrp_groups']:
- if group['disable']:
- disabled.append(
- [group['name'], group['interface'], group['vrid'], 'DISABLED', ''])
+ base = ['high-availability', 'vrrp']
+ conf = ConfigTreeQuery()
+ if conf.exists(base):
+ # Read VRRP configuration directly from CLI
+ vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ # add disabled groups to the list
+ if 'group' in vrrp_config_dict:
+ for group, group_config in vrrp_config_dict['group'].items():
+ if 'disable' not in group_config:
+ continue
+ disabled.append([group, group_config['interface'], group_config['vrid'], 'DISABLED', ''])
# return list with disabled instances
return disabled
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index 470ebbff3..c50cd5ce9 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -35,8 +35,11 @@ class VTIIf(Interface):
mapping = {
'source_interface' : 'dev',
}
-
if_id = self.ifname.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
cmd = f'ip link add {self.ifname} type xfrm if_id {if_id}'
for vyos_key, iproute2_key in mapping.items():
# dict_search will return an empty dict "{}" for valueless nodes like
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index c4cf2fbbf..28b5e2991 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -17,9 +17,6 @@ import os
import time
from datetime import timedelta
-from netaddr import EUI
-from netaddr import mac_unix_expanded
-from random import getrandbits
from hurry.filesize import size
from hurry.filesize import alternative
@@ -159,28 +156,8 @@ class WireGuardIf(Interface):
}
def get_mac(self):
- """
- Get current interface MAC (Media Access Contrl) address used.
-
- NOTE: Tunnel interfaces have no "MAC" address by default. The content
- of the 'address' file in /sys/class/net/device contains the
- local-ip thus we generate a random MAC address instead
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> Interface('eth0').get_mac()
- '00:50:ab:cd:ef:00'
- """
- # we choose 40 random bytes for the MAC address, this gives
- # us e.g. EUI('00-EA-EE-D6-A3-C8') or EUI('00-41-B9-0D-F2-2A')
- tmp = EUI(getrandbits(48)).value
- # set locally administered bit in MAC address
- tmp |= 0xf20000000000
- # convert integer to "real" MAC address representation
- mac = EUI(hex(tmp).split('x')[-1])
- # change dialect to use : as delimiter instead of -
- mac.dialect = mac_unix_expanded
- return str(mac)
+ """ Get a synthetic MAC address. """
+ return self.get_mac_synthetic()
def update(self, config):
""" General helper function which works on a dictionary retrived by
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index 9a5fdef2f..4574bb6d1 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -15,6 +15,7 @@
import sys
import os
+import json
import subprocess
import vyos.version
import vyos.defaults
@@ -165,6 +166,20 @@ class Migrator(object):
versions_string,
os_version_string)
+ def save_json_record(self, component_versions: dict):
+ """
+ Write component versions to a json file
+ """
+ mask = os.umask(0o113)
+ version_file = vyos.defaults.component_version_json
+ try:
+ with open(version_file, 'w') as f:
+ f.write(json.dumps(component_versions, indent=2, sort_keys=True))
+ except OSError:
+ pass
+ finally:
+ os.umask(mask)
+
def run(self):
"""
Gather component versions from config file and system.
@@ -182,6 +197,9 @@ class Migrator(object):
sys_versions = systemversions.get_system_versions()
+ # save system component versions in json file for easy reference
+ self.save_json_record(sys_versions)
+
rev_versions = self.run_migration_scripts(cfg_versions, sys_versions)
if rev_versions != cfg_versions:
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
index 5c4deca29..9b3f4f413 100644
--- a/python/vyos/systemversions.py
+++ b/python/vyos/systemversions.py
@@ -16,15 +16,12 @@
import os
import re
import sys
-import json
-
import vyos.defaults
def get_system_versions():
"""
- Get component versions from running system: read vyatta directory
- structure for versions, then read vyos JSON file. It is a critical
- error if either migration directory or JSON file is unreadable.
+ Get component versions from running system; critical failure if
+ unable to read migration directory.
"""
system_versions = {}
@@ -39,25 +36,4 @@ def get_system_versions():
pair = info.split('@')
system_versions[pair[0]] = int(pair[1])
- version_dict = {}
- path = vyos.defaults.version_file
-
- if os.path.isfile(path):
- with open(path, 'r') as f:
- try:
- version_dict = json.load(f)
- except ValueError as err:
- print(f"\nValue error in {path}: {err}")
- sys.exit(1)
-
- for k, v in version_dict.items():
- if not isinstance(v, int):
- print(f"\nType error in {path}; expecting Dict[str, int]")
- sys.exit(1)
- existing = system_versions.get(k)
- if existing is None:
- system_versions[k] = v
- elif v > existing:
- system_versions[k] = v
-
return system_versions
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 08a5712af..d13915766 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -393,8 +393,15 @@ def get_ip(interface):
from vyos.ifconfig import Interface
return Interface(interface).get_addr()
+def get_first_ike_dh_group(ike_group):
+ if ike_group and 'proposal' in ike_group:
+ for priority, proposal in ike_group['proposal'].items():
+ if 'dh_group' in proposal:
+ return 'dh-group' + proposal['dh_group']
+ return 'dh-group2' # Fallback on dh-group2
+
@register_filter('get_esp_ike_cipher')
-def get_esp_ike_cipher(group_config):
+def get_esp_ike_cipher(group_config, ike_group=None):
pfs_lut = {
'dh-group1' : 'modp768',
'dh-group2' : 'modp1024',
@@ -406,7 +413,7 @@ def get_esp_ike_cipher(group_config):
'dh-group18' : 'modp8192',
'dh-group19' : 'ecp256',
'dh-group20' : 'ecp384',
- 'dh-group21' : 'ecp512',
+ 'dh-group21' : 'ecp521',
'dh-group22' : 'modp1024s160',
'dh-group23' : 'modp2048s224',
'dh-group24' : 'modp2048s256',
@@ -433,7 +440,7 @@ def get_esp_ike_cipher(group_config):
elif 'pfs' in group_config and group_config['pfs'] != 'disable':
group = group_config['pfs']
if group_config['pfs'] == 'enable':
- group = 'dh-group2'
+ group = get_first_ike_dh_group(ike_group)
tmp += '-' + pfs_lut[group]
ciphers.append(tmp)
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 05643a223..849b27d3b 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -562,12 +562,13 @@ def commit_in_progress():
# Since this will be used in scripts that modify the config outside of the CLI
# framework, those knowingly have root permissions.
# For everything else, we add a safeguard.
- from psutil import process_iter, NoSuchProcess
+ from psutil import process_iter
+ from psutil import NoSuchProcess
+ from getpass import getuser
from vyos.defaults import commit_lock
- idu = cmd('/usr/bin/id -u')
- if idu != '0':
- raise OSError("This functions needs root permissions to return correct results")
+ if getuser() != 'root':
+ raise OSError('This functions needs to be run as root to return correct results!')
for proc in process_iter():
try:
@@ -691,21 +692,21 @@ def find_device_file(device):
return None
-def dict_search(path, my_dict):
- """ Traverse Python dictionary (my_dict) delimited by dot (.).
+def dict_search(path, dict_object):
+ """ Traverse Python dictionary (dict_object) delimited by dot (.).
Return value of key if found, None otherwise.
- This is faster implementation then jmespath.search('foo.bar', my_dict)"""
- if not isinstance(my_dict, dict) or not path:
+ This is faster implementation then jmespath.search('foo.bar', dict_object)"""
+ if not isinstance(dict_object, dict) or not path:
return None
parts = path.split('.')
inside = parts[:-1]
if not inside:
- if path not in my_dict:
+ if path not in dict_object:
return None
- return my_dict[path]
- c = my_dict
+ return dict_object[path]
+ c = dict_object
for p in parts[:-1]:
c = c.get(p, {})
return c.get(parts[-1], None)
@@ -723,6 +724,23 @@ def dict_search_args(dict_object, *path):
dict_object = dict_object[item]
return dict_object
+def dict_search_recursive(dict_object, key):
+ """ Traverse a dictionary recurisvely and return the value of the key
+ we are looking for.
+
+ Thankfully copied from https://stackoverflow.com/a/19871956
+ """
+ if isinstance(dict_object, list):
+ for i in dict_object:
+ for x in dict_search_recursive(i, key):
+ yield x
+ elif isinstance(dict_object, dict):
+ if key in dict_object:
+ yield dict_object[key]
+ for j in dict_object.values():
+ for x in dict_search_recursive(j, key):
+ yield x
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
@@ -805,8 +823,49 @@ def make_incremental_progressbar(increment: float):
while True:
yield
+def is_systemd_service_active(service):
+ """ Test is a specified systemd service is activated.
+ Returns True if service is active, false otherwise.
+ Copied from: https://unix.stackexchange.com/a/435317 """
+ tmp = cmd(f'systemctl show --value -p ActiveState {service}')
+ return bool((tmp == 'active'))
+
def is_systemd_service_running(service):
""" Test is a specified systemd service is actually running.
- Returns True if service is running, false otherwise. """
- tmp = run(f'systemctl is-active --quiet {service}')
- return bool((tmp == 0))
+ Returns True if service is running, false otherwise.
+ Copied from: https://unix.stackexchange.com/a/435317 """
+ tmp = cmd(f'systemctl show --value -p SubState {service}')
+ return bool((tmp == 'running'))
+
+def check_port_availability(ipaddress, port, protocol):
+ """
+ Check if port is available and not used by any service
+ Return False if a port is busy or IP address does not exists
+ Should be used carefully for services that can start listening
+ dynamically, because IP address may be dynamic too
+ """
+ from socketserver import TCPServer, UDPServer
+ from ipaddress import ip_address
+
+ # verify arguments
+ try:
+ ipaddress = ip_address(ipaddress).compressed
+ except:
+ raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address')
+ if port not in range(1, 65536):
+ raise ValueError(f'The port number {port} is not in the 1-65535 range')
+ if protocol not in ['tcp', 'udp']:
+ raise ValueError(
+ f'The protocol {protocol} is not supported. Only tcp and udp are allowed'
+ )
+
+ # check port availability
+ try:
+ if protocol == 'tcp':
+ server = TCPServer((ipaddress, port), None, bind_and_activate=True)
+ if protocol == 'udp':
+ server = UDPServer((ipaddress, port), None, bind_and_activate=True)
+ server.server_close()
+ return True
+ except:
+ return False
diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py
index 0ef0c85ce..e0eacb2d1 100644
--- a/python/vyos/xml/__init__.py
+++ b/python/vyos/xml/__init__.py
@@ -46,6 +46,8 @@ def is_tag(lpath):
def is_leaf(lpath, flat=True):
return load_configuration().is_leaf(lpath, flat)
+def component_versions():
+ return load_configuration().component_versions()
def defaults(lpath, flat=False):
return load_configuration().defaults(lpath, flat)
diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py
index f556c5ced..5e0d5282c 100644
--- a/python/vyos/xml/definition.py
+++ b/python/vyos/xml/definition.py
@@ -30,6 +30,7 @@ class XML(dict):
self[kw.owners] = {}
self[kw.default] = {}
self[kw.tags] = []
+ self[kw.component_version] = {}
dict.__init__(self)
@@ -248,6 +249,11 @@ class XML(dict):
# @lru_cache(maxsize=100)
# XXX: need to use cachetool instead - for later
+ def component_versions(self) -> dict:
+ sort_component = sorted(self[kw.component_version].items(),
+ key = lambda kv: kv[0])
+ return dict(sort_component)
+
def defaults(self, lpath, flat):
d = self[kw.default]
for k in lpath:
diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py
index 58d47e751..48226ce96 100644
--- a/python/vyos/xml/kw.py
+++ b/python/vyos/xml/kw.py
@@ -32,6 +32,7 @@ priorities = '[priorities]'
owners = '[owners]'
tags = '[tags]'
default = '[default]'
+component_version = '[component_version]'
# nodes
diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py
index 37479c6e1..c3022f3d6 100644
--- a/python/vyos/xml/load.py
+++ b/python/vyos/xml/load.py
@@ -115,7 +115,12 @@ def _format_nodes(inside, conf, xml):
nodetype = 'tagNode'
nodename = kw.tagNode
elif 'syntaxVersion' in conf.keys():
- conf.pop('syntaxVersion')
+ sv = conf.pop('syntaxVersion')
+ if isinstance(sv, list):
+ for v in sv:
+ xml[kw.component_version][v['@component']] = v['@version']
+ else:
+ xml[kw.component_version][sv['@component']] = sv['@version']
continue
else:
_fatal(conf.keys())
@@ -125,14 +130,20 @@ def _format_nodes(inside, conf, xml):
for node in nodes:
name = node.pop('@name')
into = inside + [name]
- r[name] = _format_node(into, node, xml)
+ if name in r:
+ r[name].update(_format_node(into, node, xml))
+ else:
+ r[name] = _format_node(into, node, xml)
r[name][kw.node] = nodename
xml[kw.tags].append(' '.join(into))
else:
node = nodes
name = node.pop('@name')
into = inside + [name]
- r[name] = _format_node(inside + [name], node, xml)
+ if name in r:
+ r[name].update(_format_node(inside + [name], node, xml))
+ else:
+ r[name] = _format_node(inside + [name], node, xml)
r[name][kw.node] = nodename
xml[kw.tags].append(' '.join(into))
return r