summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorLulu Cathrinus Grimalkin <me@erkin.party>2021-11-24 14:27:08 +0300
committerGitHub <noreply@github.com>2021-11-24 14:27:08 +0300
commitc0eff50f7be4ee365d0b5ce828f64a66574c4f06 (patch)
treebcffc2c52f29d12ef20854fbea2d2e60b7f7627e /python
parentc0b09fe341c7ddced42479e0192a28ca553e30d6 (diff)
parent771301fea060467945e6c55379dd8e761aa9ad9d (diff)
downloadvyos-1x-c0eff50f7be4ee365d0b5ce828f64a66574c4f06.tar.gz
vyos-1x-c0eff50f7be4ee365d0b5ce828f64a66574c4f06.zip
Merge branch 'vyos:equuleus' into equuleus
Diffstat (limited to 'python')
-rw-r--r--python/vyos/airbag.py8
-rw-r--r--python/vyos/configdict.py38
-rw-r--r--python/vyos/configsource.py2
-rw-r--r--python/vyos/configverify.py61
-rw-r--r--python/vyos/defaults.py8
-rw-r--r--python/vyos/ethtool.py200
-rw-r--r--python/vyos/frr.py54
-rw-r--r--python/vyos/ifconfig/bridge.py131
-rw-r--r--python/vyos/ifconfig/control.py19
-rw-r--r--python/vyos/ifconfig/ethernet.py217
-rw-r--r--python/vyos/ifconfig/interface.py229
-rw-r--r--python/vyos/ifconfig/section.py12
-rw-r--r--python/vyos/ifconfig/tunnel.py26
-rw-r--r--python/vyos/ifconfig/vrrp.py9
-rw-r--r--python/vyos/ifconfig/wireguard.py27
-rw-r--r--python/vyos/ifconfig/wwan.py17
-rw-r--r--python/vyos/migrator.py18
-rw-r--r--python/vyos/systemversions.py28
-rw-r--r--python/vyos/template.py15
-rw-r--r--python/vyos/util.py44
20 files changed, 744 insertions, 419 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 dba992d56..8e5781b81 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)):
@@ -151,18 +155,15 @@ def get_removed_vlans(conf, dict):
D.set_level(conf.get_level())
# get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_remove': [*keys]})
+ if keys: dict['vif_remove'] = [*keys]
# get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_s_remove': [*keys]})
+ if keys: dict['vif_s_remove'] = [*keys]
for vif in dict.get('vif_s', {}).keys():
keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
- if keys:
- dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}})
+ if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys]
return dict
@@ -343,8 +344,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 +372,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})
@@ -515,6 +519,11 @@ def get_accel_dict(config, base, chap_secrets):
if dict_search('authentication.local_users.username', default_values):
del default_values['authentication']['local_users']['username']
+ # T2665: defaults include IPv6 client-pool mask per TAG node which need to be
+ # added to individual local users instead - so we can simply delete them
+ if dict_search('client_ipv6_pool.prefix.mask', default_values):
+ del default_values['client_ipv6_pool']['prefix']['mask']
+
dict = dict_merge(default_values, dict)
# set CPUs cores to process requests
@@ -558,4 +567,13 @@ def get_accel_dict(config, base, chap_secrets):
dict['authentication']['local_users']['username'][username] = dict_merge(
default_values, dict['authentication']['local_users']['username'][username])
+ # Add individual IPv6 client-pool default mask if required
+ if dict_search('client_ipv6_pool.prefix', dict):
+ # T2665
+ default_values = defaults(base + ['client-ipv6-pool', 'prefix'])
+
+ for prefix in dict_search('client_ipv6_pool.prefix', dict):
+ dict['client_ipv6_pool']['prefix'][prefix] = dict_merge(
+ default_values, dict['client_ipv6_pool']['prefix'][prefix])
+
return dict
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 0b6e6fc13..3aece499e 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_tunnel(config):
"""
@@ -95,15 +95,12 @@ def verify_tunnel(config):
raise ConfigError('Must configure the tunnel encapsulation for '\
'{ifname}!'.format(**config))
- if 'source_address' not in config and 'dhcp_interface' not in config:
- raise ConfigError('source-address is mandatory for tunnel')
+ if 'source_address' not in config and 'source_interface' not in config:
+ raise ConfigError('source-address or source-interface required for tunnel!')
if 'remote' not in config and config['encapsulation'] != 'gre':
raise ConfigError('remote-ip address is mandatory for tunnel')
- if {'source_address', 'dhcp_interface'} <= set(config):
- raise ConfigError('Can not use both source-address and dhcp-interface')
-
if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
error_ipv6 = 'Encapsulation mode requires IPv6'
if 'source_address' in config and not is_ipv6(config['source_address']):
@@ -208,8 +205,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):
@@ -385,3 +382,29 @@ def verify_diffie_hellman_length(file, min_keysize):
return False
+def verify_common_route_maps(config):
+ """
+ Common helper function used by routing protocol implementations to perform
+ recurring validation if the specified route-map for either zebra to kernel
+ installation exists (this is the top-level route_map key) or when a route
+ is redistributed with a route-map that it exists!
+ """
+ # XXX: This function is called in combination with a previous call to:
+ # tmp = conf.get_config_dict(['policy']) - see protocols_ospf.py as example.
+ # We should NOT call this with the key_mangling option as this would rename
+ # route-map hypens '-' to underscores '_' and one could no longer distinguish
+ # what should have been the "proper" route-map name, as foo-bar and foo_bar
+ # are two entire different route-map instances!
+ for route_map in ['route-map', 'route_map']:
+ if route_map not in config:
+ continue
+ tmp = config[route_map]
+ # Check if the specified route-map exists, if not error out
+ if dict_search(f'policy.route-map.{tmp}', config) == None:
+ raise ConfigError(f'Specified route-map "{tmp}" does not exist!')
+
+ if 'redistribute' in config:
+ for protocol, protocol_config in config['redistribute'].items():
+ if 'route_map' in protocol_config:
+ verify_route_map(protocol_config['route_map'], config)
+
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9921e3b5f..dacdbdef2 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/",
@@ -22,7 +23,10 @@ directories = {
"migrate": "/opt/vyatta/etc/config-migrate/migrate",
"log": "/var/log/vyatta",
"templates": "/usr/share/vyos/templates/",
- "certbot": "/config/auth/letsencrypt"
+ "certbot": "/config/auth/letsencrypt",
+ "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/",
+ "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/"
+
}
cfg_group = 'vyattacfg'
@@ -31,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..e45b0f041 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -13,44 +13,89 @@
# 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)
+
+ # 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 +106,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/frr.py b/python/vyos/frr.py
index 3bab64301..df6849472 100644
--- a/python/vyos/frr.py
+++ b/python/vyos/frr.py
@@ -68,15 +68,27 @@ Apply the new configuration:
import tempfile
import re
from vyos import util
+from vyos.util import chown
+from vyos.util import cmd
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']
path_vtysh = '/usr/bin/vtysh'
path_frr_reload = '/usr/lib/frr/frr-reload.py'
+path_config = '/run/frr'
class FrrError(Exception):
@@ -175,21 +187,42 @@ 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('FRR configuration failed while running commit. Please ' \
+ 'enable debugging to examine logs.\n\n\n' \
+ 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \
+ 'and "sudo systemctl stop vyos-configd"')
elif code:
raise OSError(code, output)
return output
+def save_configuration():
+ """Save FRR configuration to /run/frr/config/frr.conf
+ It save configuration on each commit. T3217
+ """
+
+ cmd(f'{path_vtysh} -n -w')
+
+ return
+
+
def execute(command):
""" Run commands inside vtysh
command: str containing commands to execute inside a vtysh session
@@ -382,6 +415,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 +428,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 +453,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):
@@ -459,7 +506,8 @@ class FRRConfig:
start = _find_first_element(self.config, before_pattern)
if start < 0:
return False
-
+ for i, e in enumerate(addition, start=start):
+ LOG.debug(f'add_before: add {i:3} {e}')
self.config[start:start] = addition
return True
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index 65a4506c5..27073b266 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -22,6 +22,7 @@ from vyos.validate import assert_positive
from vyos.util import cmd
from vyos.util import dict_search
from vyos.configdict import get_vlan_ids
+from vyos.configdict import list_diff
@Interface.register
class BridgeIf(Interface):
@@ -33,7 +34,6 @@ class BridgeIf(Interface):
The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard.
"""
-
iftype = 'bridge'
definition = {
**Interface.definition,
@@ -267,21 +267,37 @@ class BridgeIf(Interface):
for member in (tmp or []):
if member in interfaces():
self.del_port(member)
- vlan_filter = 0
- vlan_del = set()
- vlan_add = set()
+ # enable/disable Vlan Filter
+ vlan_filter = '1' if 'enable_vlan' in config else '0'
+ self.set_vlan_filter(vlan_filter)
ifname = config['ifname']
+ if int(vlan_filter):
+ add_vlan = []
+ cur_vlan_ids = get_vlan_ids(ifname)
+
+ tmp = dict_search('vif', config)
+ if tmp:
+ for vif, vif_config in tmp.items():
+ add_vlan.append(vif)
+
+ # 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} self'
+ self._cmd(cmd)
+
+ for vlan in add_vlan:
+ cmd = f'bridge vlan add dev {ifname} vid {vlan} self'
+ self._cmd(cmd)
+
+ # VLAN of bridge parent interface is always 1
+ # VLAN 1 is the default VLAN for all unlabeled packets
+ cmd = f'bridge vlan add dev {ifname} vid 1 pvid untagged self'
+ self._cmd(cmd)
+
tmp = dict_search('member.interface', config)
if tmp:
- if self.get_vlan_filter():
- bridge_vlan_ids = get_vlan_ids(ifname)
- # Delete VLAN ID for the bridge
- if 1 in bridge_vlan_ids:
- bridge_vlan_ids.remove(1)
- for vlan in bridge_vlan_ids:
- vlan_del.add(str(vlan))
for interface, interface_config in tmp.items():
# if interface does yet not exist bail out early and
@@ -296,9 +312,15 @@ class BridgeIf(Interface):
# not have any addresses configured by CLI so just flush any
# remaining ones
lower.flush_addrs()
+
# enslave interface port to bridge
self.add_port(interface)
+ # always set private-vlan/port isolation
+ tmp = dict_search('isolated', interface_config)
+ value = 'on' if (tmp != None) else 'off'
+ lower.set_port_isolation(value)
+
# set bridge port path cost
if 'cost' in interface_config:
value = interface_config.get('cost')
@@ -309,62 +331,39 @@ class BridgeIf(Interface):
value = interface_config.get('priority')
lower.set_path_priority(value)
- tmp = dict_search('native_vlan_removed', interface_config)
-
- for vlan_id in (tmp or []):
- cmd = f'bridge vlan del dev {interface} vid {vlan_id}'
- self._cmd(cmd)
- cmd = f'bridge vlan add dev {interface} vid 1 pvid untagged master'
- self._cmd(cmd)
- vlan_del.add(vlan_id)
- vlan_add.add(1)
-
- tmp = dict_search('allowed_vlan_removed', interface_config)
-
- for vlan_id in (tmp or []):
- cmd = f'bridge vlan del dev {interface} vid {vlan_id}'
- self._cmd(cmd)
- vlan_del.add(vlan_id)
-
- if 'native_vlan' in interface_config:
- vlan_filter = 1
- cmd = f'bridge vlan del dev {interface} vid 1'
- self._cmd(cmd)
- vlan_id = interface_config['native_vlan']
- if int(vlan_id) != 1:
- if 1 in vlan_add:
- vlan_add.remove(1)
- vlan_del.add(1)
- cmd = f'bridge vlan add dev {interface} vid {vlan_id} pvid untagged master'
- self._cmd(cmd)
- vlan_add.add(vlan_id)
- if vlan_id in vlan_del:
- vlan_del.remove(vlan_id)
-
- if 'allowed_vlan' in interface_config:
- vlan_filter = 1
- if 'native_vlan' not in interface_config:
- cmd = f'bridge vlan del dev {interface} vid 1'
+ if int(vlan_filter):
+ add_vlan = []
+ native_vlan_id = None
+ allowed_vlan_ids= []
+ cur_vlan_ids = get_vlan_ids(interface)
+
+ if 'native_vlan' in interface_config:
+ vlan_id = interface_config['native_vlan']
+ add_vlan.append(vlan_id)
+ native_vlan_id = vlan_id
+
+ if 'allowed_vlan' in interface_config:
+ for vlan in interface_config['allowed_vlan']:
+ vlan_range = vlan.split('-')
+ if len(vlan_range) == 2:
+ for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1):
+ add_vlan.append(str(vlan_add))
+ allowed_vlan_ids.append(str(vlan_add))
+ 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 {interface} vid {vlan} master'
self._cmd(cmd)
- vlan_del.add(1)
- for vlan in interface_config['allowed_vlan']:
+
+ for vlan in allowed_vlan_ids:
cmd = f'bridge vlan add dev {interface} vid {vlan} master'
self._cmd(cmd)
- vlan_add.add(vlan)
- if vlan in vlan_del:
- vlan_del.remove(vlan)
-
- for vlan in vlan_del:
- cmd = f'bridge vlan del dev {ifname} vid {vlan} self'
- self._cmd(cmd)
-
- for vlan in vlan_add:
- cmd = f'bridge vlan add dev {ifname} vid {vlan} self'
- self._cmd(cmd)
-
- # enable/disable Vlan Filter
- self.set_vlan_filter(vlan_filter)
-
+ # Setting native VLAN to system
+ if native_vlan_id:
+ 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 43136f361..6815074f8 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 df6b96fbf..4ae350634 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
@@ -42,39 +44,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}',
},
}}
@@ -85,24 +77,26 @@ 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
+ def __init__(self, ifname, **kargs):
+ super().__init__(ifname, **kargs)
+ self.ethtool = Ethtool(ifname)
+ def remove(self):
+ """
+ Remove interface from config. Removing the interface deconfigures all
+ assigned IP addresses.
Example:
- >>> from vyos.ifconfig import EthernetIf
+ >>> from vyos.ifconfig import WWANIf
>>> i = EthernetIf('eth0')
- >>> i.get_driver_name()
- 'vmxnet3'
+ >>> i.remove()
"""
- 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
+
+ if self.exists(self.ifname):
+ # interface is placed in A/D state when removed from config! It
+ # will remain visible for the operating system.
+ self.set_admin_state('down')
+
+ super().remove()
def set_flow_control(self, enable):
"""
@@ -120,44 +114,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):
"""
@@ -179,40 +149,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:
@@ -229,8 +187,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):
"""
@@ -241,8 +206,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):
"""
@@ -253,12 +225,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:
@@ -283,8 +262,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):
"""
@@ -296,40 +282,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
@@ -358,9 +342,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')
@@ -369,8 +350,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 9c02af68f..036ca1413 100644
--- 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}')
@@ -354,6 +407,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):
@@ -367,6 +423,51 @@ 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"')
+
+ # XXX: T3894 - it seems not all systems have eth0 - get a list of all
+ # available Ethernet interfaces on the system (without VLAN subinterfaces)
+ # and then take the first one.
+ all_eth_ifs = [x for x in Section.interfaces('ethernet') if '.' not in x]
+ first_mac = Interface(all_eth_ifs[0]).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(first_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.
@@ -391,7 +492,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.
@@ -400,6 +501,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)
def set_arp_cache_tmo(self, tmo):
@@ -411,6 +517,10 @@ 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_arp_filter(self, arp_filter):
@@ -431,6 +541,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):
@@ -447,6 +560,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):
@@ -468,6 +584,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):
@@ -480,12 +599,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):
@@ -514,6 +637,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):
@@ -529,6 +655,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):
@@ -536,6 +665,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):
@@ -559,9 +691,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):
"""
@@ -588,6 +721,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):
@@ -595,6 +731,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):
@@ -617,6 +756,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):
@@ -641,6 +783,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):
@@ -716,6 +861,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):
@@ -742,6 +890,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):
@@ -878,6 +1029,8 @@ class Interface(Control):
>>> j.get_addr()
['2001:db8::ffff/64']
"""
+ if not addr:
+ raise ValueError()
# remove from interface
if addr == 'dhcp':
@@ -984,7 +1137,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:
@@ -1004,7 +1159,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]:
@@ -1021,17 +1176,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)
@@ -1048,12 +1204,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:
@@ -1124,16 +1282,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)
@@ -1224,16 +1382,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/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 e40756cc7..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,26 +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..3d6f4d7c6 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -32,14 +32,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',
+ 'vyos': '/run/keepalived/keepalived_config.dict',
}
_signal = {
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 2d2243b84..de1b56ce5 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
@@ -163,28 +160,8 @@ class WireGuardIf(Interface):
'allowed_ips', 'fwmark', 'endpoint', 'keepalive']
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/wwan.py b/python/vyos/ifconfig/wwan.py
index f18959a60..845c9bef9 100644
--- a/python/vyos/ifconfig/wwan.py
+++ b/python/vyos/ifconfig/wwan.py
@@ -26,3 +26,20 @@ class WWANIf(Interface):
'eternal': 'wwan[0-9]+$',
},
}
+
+ def remove(self):
+ """
+ Remove interface from config. Removing the interface deconfigures all
+ assigned IP addresses.
+ Example:
+ >>> from vyos.ifconfig import WWANIf
+ >>> i = WWANIf('wwan0')
+ >>> i.remove()
+ """
+
+ if self.exists(self.ifname):
+ # interface is placed in A/D state when removed from config! It
+ # will remain visible for the operating system.
+ self.set_admin_state('down')
+
+ super().remove()
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 b58f641e1..f9e754357 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -29,13 +29,17 @@ _FILTERS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
-def _get_environment():
+def _get_environment(location=None):
+ if location is None:
+ loc_loader=FileSystemLoader(directories["templates"])
+ else:
+ loc_loader=FileSystemLoader(location)
env = Environment(
# Don't check if template files were modified upon re-rendering
auto_reload=False,
# Cache up to this number of templates for quick re-rendering
cache_size=100,
- loader=FileSystemLoader(directories["templates"]),
+ loader=loc_loader,
trim_blocks=True,
)
env.filters.update(_FILTERS)
@@ -63,7 +67,7 @@ def register_filter(name, func=None):
return func
-def render_to_string(template, content, formater=None):
+def render_to_string(template, content, formater=None, location=None):
"""Render a template from the template directory, raise on any errors.
:param template: the path to the template relative to the template folder
@@ -78,7 +82,7 @@ def render_to_string(template, content, formater=None):
package is build (recovering the load time and overhead caused by having the
file out of the code).
"""
- template = _get_environment().get_template(template)
+ template = _get_environment(location).get_template(template)
rendered = template.render(content)
if formater is not None:
rendered = formater(rendered)
@@ -93,6 +97,7 @@ def render(
permission=None,
user=None,
group=None,
+ location=None,
):
"""Render a template from the template directory to a file, raise on any errors.
@@ -109,7 +114,7 @@ def render(
# As we are opening the file with 'w', we are performing the rendering before
# calling open() to not accidentally erase the file if rendering fails
- rendered = render_to_string(template, content, formater)
+ rendered = render_to_string(template, content, formater, location)
# Write to file
with open(destination, "w") as file:
diff --git a/python/vyos/util.py b/python/vyos/util.py
index f3451fd77..1834b78bd 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -676,20 +676,20 @@ 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)
@@ -704,8 +704,32 @@ def get_interface_config(interface):
tmp = loads(cmd(f'ip -d -j link show {interface}'))[0]
return tmp
+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 is_wwan_connected(interface):
+ """ Determine if a given WWAN interface, e.g. wwan0 is connected to the
+ carrier network or not """
+ import json
+
+ if not interface.startswith('wwan'):
+ raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
+
+ modem = interface.lstrip('wwan')
+
+ tmp = cmd(f'mmcli --modem {modem} --output-json')
+ tmp = json.loads(tmp)
+
+ # return True/False if interface is in connected state
+ return dict_search('modem.generic.state', tmp) == 'connected'