summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/airbag.py17
-rw-r--r--python/vyos/component_versions.py2
-rw-r--r--python/vyos/config.py66
-rw-r--r--python/vyos/configdict.py425
-rw-r--r--python/vyos/debug.py37
-rw-r--r--python/vyos/dicts.py3
-rw-r--r--python/vyos/ifconfig/dhcp.py129
-rw-r--r--python/vyos/ifconfig/interface.py161
-rw-r--r--python/vyos/ifconfig/section.py21
-rw-r--r--python/vyos/ifconfig/vlan.py40
-rw-r--r--python/vyos/ifconfig/wireguard.py2
-rw-r--r--python/vyos/ifconfig_vlan.py159
-rw-r--r--python/vyos/template.py10
-rw-r--r--python/vyos/util.py167
-rw-r--r--python/vyos/validate.py68
15 files changed, 915 insertions, 392 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 6698aa404..b7838d8a2 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -26,6 +26,17 @@ from vyos.version import get_full_version_data
DISABLE = False
+_noteworthy = []
+
+def noteworthy(msg):
+ """
+ noteworthy can be use to take note things which we may not want to
+ report to the user may but be worth including in bug report
+ if something goes wrong later on
+ """
+ _noteworthy.append(msg)
+
+
# emulate a file object
class _IO(object):
def __init__(self, std, log):
@@ -58,11 +69,16 @@ def bug_report(dtype, value, trace):
information = get_full_version_data()
trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+ note = ''
+ if _noteworthy:
+ note = 'noteworthy:\n'
+ note += '\n'.join(_noteworthy)
information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'trace': trace,
'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'note': note,
})
sys.stdout.write(INTRO.format(**information))
@@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial}
Hardware UUID: {hardware_uuid}
{trace}
+{note}
"""
INTRO = """\
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
index ec54a1576..90b458aae 100644
--- a/python/vyos/component_versions.py
+++ b/python/vyos/component_versions.py
@@ -51,7 +51,7 @@ def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/co
"""
f = open(config_file_name, 'r')
for line_in_config in f:
- component_version = return_version(line_in_config)
+ component_version = get_component_version(line_in_config)
if component_version:
return component_version
raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 75055a603..54cb518c3 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -97,12 +97,12 @@ class Config(object):
self.__session_env = None
# Running config can be obtained either from op or conf mode, it always succeeds
- # (if config system is initialized at all).
+ # once the config system is initialized during boot;
+ # before initialization, set to empty string
if os.path.isfile('/tmp/vyos-config-status'):
running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
else:
- with open('/opt/vyatta/etc/config/config.boot') as f:
- running_config_text = f.read()
+ running_config_text = ''
# Session config ("active") only exists in conf mode.
# In op mode, we'll just use the same running config for both active and session configs.
@@ -112,7 +112,10 @@ class Config(object):
session_config_text = running_config_text
self._session_config = vyos.configtree.ConfigTree(session_config_text)
- self._running_config = vyos.configtree.ConfigTree(running_config_text)
+ if running_config_text:
+ self._running_config = vyos.configtree.ConfigTree(running_config_text)
+ else:
+ self._running_config = None
def _make_command(self, op, path):
args = path.split()
@@ -155,7 +158,7 @@ class Config(object):
``exists("system name-server"`` without ``set_level``.
Args:
- path (str): relative config path
+ path (str|list): relative config path
"""
# Make sure there's always a space between default path (level)
# and path supplied as method argument
@@ -166,7 +169,7 @@ class Config(object):
else:
self._level = []
elif isinstance(path, list):
- self._level = path
+ self._level = path.copy()
else:
raise TypeError("Level path must be either a whitespace-separated string or a list")
@@ -177,7 +180,7 @@ class Config(object):
Returns:
str: current edit level
"""
- return(self._level)
+ return(self._level.copy())
def exists(self, path):
"""
@@ -278,8 +281,12 @@ class Config(object):
Returns: a dict representation of the config
"""
res = self.show_config(self._make_path(path), effective=effective)
- config_tree = vyos.configtree.ConfigTree(res)
- config_dict = json.loads(config_tree.to_json())
+ if res:
+ config_tree = vyos.configtree.ConfigTree(res)
+ config_dict = json.loads(config_tree.to_json())
+ else:
+ config_dict = {}
+
return config_dict
def is_multi(self, path):
@@ -386,7 +393,7 @@ class Config(object):
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -407,7 +414,7 @@ class Config(object):
nodes = []
if not nodes:
- return(default)
+ return(default.copy())
else:
return(nodes)
@@ -425,7 +432,10 @@ class Config(object):
This function is safe to use in operational mode. In configuration mode,
it ignores uncommited changes.
"""
- return(self._running_config.exists(self._make_path(path)))
+ if self._running_config:
+ return(self._running_config.exists(self._make_path(path)))
+
+ return False
def return_effective_value(self, path, default=None):
"""
@@ -438,9 +448,12 @@ class Config(object):
Returns:
str: Node value
"""
- try:
- value = self._running_config.return_value(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ value = self._running_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
value = None
if not value:
@@ -448,7 +461,6 @@ class Config(object):
else:
return(value)
-
def return_effective_values(self, path, default=[]):
"""
Retrieve all values of a multi-value node in a running (effective) config
@@ -459,13 +471,16 @@ class Config(object):
Returns:
str list: A list of values
"""
- try:
- values = self._running_config.return_values(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ values = self._running_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -482,12 +497,15 @@ class Config(object):
Raises:
VyOSError: if the node is not a tag node
"""
- try:
- nodes = self._running_config.list_nodes(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ nodes = self._running_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
nodes = []
if not nodes:
- return(default)
+ return(default.copy())
else:
return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 24fe174d2..eec64e964 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -18,7 +18,13 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion.
"""
+from enum import Enum
+from copy import deepcopy
+
from vyos import ConfigError
+from vyos.ifconfig import Interface
+from vyos.validate import is_member
+from vyos.util import ifname_from_config
def retrieve_config(path_hash, base_path, config):
"""
@@ -97,172 +103,359 @@ def get_ethertype(ethertype_val):
else:
raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
+interface_default_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'description': '',
+ 'dhcp_client_id': '',
+ 'dhcp_hostname': '',
+ 'dhcp_vendor_class_id': '',
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ 'dhcpv6_pd': [],
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'ip_disable_arp_filter': 1,
+ 'ip_enable_arp_accept': 0,
+ 'ip_enable_arp_announce': 0,
+ 'ip_enable_arp_ignore': 0,
+ 'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
+ 'mac': '',
+ 'mtu': 1500,
+ 'vrf': ''
+}
+
+vlan_default = {
+ **interface_default_data,
+ 'egress_qos': '',
+ 'egress_qos_changed': False,
+ 'ingress_qos': '',
+ 'ingress_qos_changed': False,
+ 'vif_c': {},
+ 'vif_c_remove': []
+}
+
+# see: https://docs.python.org/3/library/enum.html#functional-api
+disable = Enum('disable','none was now both')
+
+def disable_state(conf, check=[3,5,7]):
+ """
+ return if and how a particual section of the configuration is has disable'd
+ using "disable" including if it was disabled by one of its parent.
+
+ check: a list of the level we should check, here 7,5 and 3
+ interfaces ethernet eth1 vif-s 1 vif-c 2 disable
+ interfaces ethernet eth1 vif 1 disable
+ interfaces ethernet eth1 disable
+
+ it returns an enum (none, was, now, both)
+ """
+
+ # save where we are in the config
+ current_level = conf.get_level()
+
+ # logic to figure out if the interface (or one of it parent is disabled)
+ eff_disable = False
+ act_disable = False
+
+ levels = check[:]
+ working_level = current_level[:]
+
+ while levels:
+ position = len(working_level)
+ if not position:
+ break
+ if position not in levels:
+ working_level = working_level[:-1]
+ continue
+
+ levels.remove(position)
+ conf.set_level(working_level)
+ working_level = working_level[:-1]
+
+ eff_disable = eff_disable or conf.exists_effective('disable')
+ act_disable = act_disable or conf.exists('disable')
-def vlan_to_dict(conf):
+ conf.set_level(current_level)
+
+ # how the disabling changed
+ if eff_disable and act_disable:
+ return disable.both
+ if eff_disable and not eff_disable:
+ return disable.was
+ if not eff_disable and act_disable:
+ return disable.now
+ return disable.none
+
+
+def intf_to_dict(conf, default):
"""
Common used function which will extract VLAN related information from config
and represent the result as Python dictionary.
Function call's itself recursively if a vif-s/vif-c pair is detected.
"""
- vlan = {
- 'id': conf.get_level()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
- 'address': [],
- 'address_remove': [],
- 'description': '',
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
- 'egress_qos': '',
- 'egress_qos_changed': False,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'ingress_qos': '',
- 'ingress_qos_changed': False,
- 'mac': '',
- 'mtu': 1500,
- 'vrf': ''
- }
- # retrieve configured interface addresses
- if conf.exists('address'):
- vlan['address'] = conf.return_values('address')
-
- # Determine interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed from the bond
- eff_addr = conf.return_effective_values('address')
- act_addr = conf.return_values('address')
- vlan['address_remove'] = list_diff(eff_addr, act_addr)
+
+ intf = deepcopy(default)
+ intf['intf'] = ifname_from_config(conf)
# retrieve interface description
- if conf.exists('description'):
- vlan['description'] = conf.return_value('description')
+ if conf.exists(['description']):
+ intf['description'] = conf.return_value(['description'])
# get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+ if conf.exists(['dhcp-options', 'client-id']):
+ intf['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id'])
# DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+ if conf.exists(['dhcp-options', 'host-name']):
+ intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name'])
# DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- vlan['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
+ if conf.exists(['dhcp-options', 'vendor-class-id']):
+ intf['dhcp_vendor_class_id'] = conf.return_value(
+ ['dhcp-options', 'vendor-class-id'])
# DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- vlan['dhcpv6_prm_only'] = True
+ if conf.exists(['dhcpv6-options', 'parameters-only']):
+ intf['dhcpv6_prm_only'] = True
+
+ # DHCPv6 prefix delegation (RFC3633)
+ current_level = conf.get_level()
+ if conf.exists(['dhcpv6-options', 'delegate']):
+ for interface in conf.list_nodes(['dhcpv6-options', 'delegate']):
+ conf.set_level(current_level + ['dhcpv6-options', 'delegate', interface])
+ pd = {
+ 'ifname': interface,
+ 'sla_id': '',
+ 'sla_len': '',
+ 'if_id': ''
+ }
+
+ if conf.exists(['sla-id']):
+ pd['sla_id'] = conf.return_value(['sla-id'])
+
+ if conf.exists(['sla-len']):
+ pd['sla_len'] = conf.return_value(['sla-len'])
+
+ if conf.exists(['interface-id']):
+ pd['if_id'] = conf.return_value(['interface-id'])
+
+ intf['dhcpv6_pd'].append(pd)
+
+ # re-set config level
+ conf.set_level(current_level)
# DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- vlan['dhcpv6_temporary'] = True
+ if conf.exists(['dhcpv6-options', 'temporary']):
+ intf['dhcpv6_temporary'] = True
# ignore link state changes
- if conf.exists('disable-link-detect'):
- vlan['disable_link_detect'] = 2
-
- # disable VLAN interface
- if conf.exists('disable'):
- vlan['disable'] = True
+ if conf.exists(['disable-link-detect']):
+ intf['disable_link_detect'] = 2
# ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- vlan['ip_disable_arp_filter'] = 0
+ if conf.exists(['ip', 'disable-arp-filter']):
+ intf['ip_disable_arp_filter'] = 0
# ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- vlan['ip_enable_arp_accept'] = 1
+ if conf.exists(['ip', 'enable-arp-accept']):
+ intf['ip_enable_arp_accept'] = 1
# ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- vlan['ip_enable_arp_announce'] = 1
+ if conf.exists(['ip', 'enable-arp-announce']):
+ intf['ip_enable_arp_announce'] = 1
# ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- vlan['ip_enable_arp_ignore'] = 1
+ if conf.exists(['ip', 'enable-arp-ignore']):
+ intf['ip_enable_arp_ignore'] = 1
# Enable Proxy ARP
- if conf.exists('ip enable-proxy-arp'):
- vlan['ip_proxy_arp'] = 1
+ if conf.exists(['ip', 'enable-proxy-arp']):
+ intf['ip_proxy_arp'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- vlan['ipv6_autoconf'] = 1
+ if conf.exists(['ipv6', 'address', 'autoconf']):
+ intf['ipv6_autoconf'] = 1
# Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- vlan['ipv6_forwarding'] = 0
+ if conf.exists(['ipv6', 'disable-forwarding']):
+ intf['ipv6_forwarding'] = 0
+
+ # check if interface is member of a bridge
+ intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge')
# IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- vlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ if conf.exists(['ipv6', 'dup-addr-detect-transmits']):
+ intf['ipv6_dup_addr_detect'] = int(
+ conf.return_value(['ipv6', 'dup-addr-detect-transmits']))
# Media Access Control (MAC) address
- if conf.exists('mac'):
- vlan['mac'] = conf.return_value('mac')
+ if conf.exists(['mac']):
+ intf['mac'] = conf.return_value(['mac'])
# Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- vlan['mtu'] = int(conf.return_value('mtu'))
+ if conf.exists(['mtu']):
+ intf['mtu'] = int(conf.return_value(['mtu']))
# retrieve VRF instance
- if conf.exists('vrf'):
- vlan['vrf'] = conf.return_value('vrf')
+ if conf.exists(['vrf']):
+ intf['vrf'] = conf.return_value(['vrf'])
- # VLAN egress QoS
- if conf.exists('egress-qos'):
- vlan['egress_qos'] = conf.return_value('egress-qos')
+ # egress QoS
+ if conf.exists(['egress-qos']):
+ intf['egress_qos'] = conf.return_value(['egress-qos'])
# egress changes QoS require VLAN interface recreation
- if conf.return_effective_value('egress-qos'):
- if vlan['egress_qos'] != conf.return_effective_value('egress-qos'):
- vlan['egress_qos_changed'] = True
+ if conf.return_effective_value(['egress-qos']):
+ if intf['egress_qos'] != conf.return_effective_value(['egress-qos']):
+ intf['egress_qos_changed'] = True
- # VLAN ingress QoS
- if conf.exists('ingress-qos'):
- vlan['ingress_qos'] = conf.return_value('ingress-qos')
+ # ingress QoS
+ if conf.exists(['ingress-qos']):
+ intf['ingress_qos'] = conf.return_value(['ingress-qos'])
# ingress changes QoS require VLAN interface recreation
- if conf.return_effective_value('ingress-qos'):
- if vlan['ingress_qos'] != conf.return_effective_value('ingress-qos'):
- vlan['ingress_qos_changed'] = True
+ if conf.return_effective_value(['ingress-qos']):
+ if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']):
+ intf['ingress_qos_changed'] = True
+
+ # Get the interface addresses
+ intf['address'] = conf.return_values(['address'])
+
+ # addresses to remove - difference between effective and working config
+ intf['address_remove'] = list_diff(
+ conf.return_effective_values(['address']), intf['address'])
+
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
+ intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64'])
+
+ # EUI64 to remove - difference between effective and working config
+ intf['ipv6_eui64_prefix_remove'] = list_diff(
+ conf.return_effective_values(['ipv6', 'address', 'eui64']),
+ intf['ipv6_eui64_prefix'])
+
+ # Determine if the interface should be disabled
+ disabled = disable_state(conf)
+ if disabled == disable.both:
+ # was and is still disabled
+ intf['disable'] = True
+ elif disabled == disable.now:
+ # it is now disable but was not before
+ intf['disable'] = True
+ elif disabled == disable.was:
+ # it was disable but not anymore
+ intf['disable'] = False
+ else:
+ # normal change
+ intf['disable'] = False
+
+ # Remove the default link-local address if no-default-link-local is set,
+ # if member of a bridge or if disabled (it may not have a MAC if it's down)
+ if ( conf.exists(['ipv6', 'address', 'no-default-link-local'])
+ or intf.get('is_bridge_member') or intf['disable'] ):
+ intf['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ intf['ipv6_eui64_prefix'].append('fe80::/64')
- # ethertype is mandatory on vif-s nodes and only exists here!
- # check if this is a vif-s node at all:
- if conf.get_level()[-2] == 'vif-s':
- vlan['vif_c'] = []
- vlan['vif_c_remove'] = []
-
- # ethertype uses a default of 0x88A8
- tmp = '0x88A8'
- if conf.exists('ethertype'):
- tmp = conf.return_value('ethertype')
- vlan['ethertype'] = get_ethertype(tmp)
-
- # get vif-c interfaces (currently effective) - to determine which vif-c
+ # If MAC has changed, remove and re-add all IPv6 EUI64 addresses
+ try:
+ interface = Interface(intf['intf'], create=False)
+ if intf['mac'] and intf['mac'] != interface.get_mac():
+ intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix']
+ except Exception:
+ # If the interface does not exist, it could not have changed
+ pass
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']:
+ intf['ipv6_accept_ra'] = 2
+
+ return intf, disable
+
+
+
+def add_to_dict(conf, disabled, ifdict, section, key):
+ """
+ parse a section of vif/vif-s/vif-c and add them to the dict
+ follow the convention to:
+ * use the "key" for what to add
+ * use the "key" what what to remove
+
+ conf: is the Config() already at the level we need to parse
+ disabled: is a disable enum so we know how to handle to data
+ intf: if the interface dictionary
+ section: is the section name to parse (vif/vif-s/vif-c)
+ key: is the dict key to use (vif/vifs/vifc)
+ """
+
+ if not conf.exists(section):
+ return ifdict
+
+ effect = conf.list_effective_nodes(section)
+ active = conf.list_nodes(section)
+
+ # the section to parse for vlan
+ sections = []
+
+ # determine which interfaces to add or remove based on disable state
+ if disabled == disable.both:
+ # was and is still disabled
+ ifdict[f'{key}_remove'] = []
+ elif disabled == disable.now:
+ # it is now disable but was not before
+ ifdict[f'{key}_remove'] = effect
+ elif disabled == disable.was:
+ # it was disable but not anymore
+ ifdict[f'{key}_remove'] = []
+ sections = active
+ else:
+ # normal change
+ # get interfaces (currently effective) - to determine which
# interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-c')
- act_intf = conf.list_nodes('vif-c')
- vlan['vif_c_remove'] = list_diff(eff_intf, act_intf)
-
- # check if there is a Q-in-Q vlan customer interface
- # and call this function recursively
- if conf.exists('vif-c'):
- cfg_level = conf.get_level()
- # add new key (vif-c) to dictionary
- for vif in conf.list_nodes('vif-c'):
- # set config level to vif interface
- conf.set_level(cfg_level + ['vif-c', vif])
- vlan['vif_c'].append(vlan_to_dict(conf))
+ ifdict[f'{key}_remove'] = list_diff(effect, active)
+ sections = active
+
+ current_level = conf.get_level()
+
+ # add each section, the key must already exists
+ for s in sections:
+ # set config level to vif interface
+ conf.set_level(current_level + [section, s])
+ # add the vlan config as a key (vlan id) - value (config) pair
+ ifdict[key][s] = vlan_to_dict(conf)
+
+ # re-set configuration level to leave things as found
+ conf.set_level(current_level)
+
+ return ifdict
+
+
+def vlan_to_dict(conf, default=vlan_default):
+ vlan, disabled = intf_to_dict(conf, default)
+
+ # if this is a not within vif-s node, we are done
+ if conf.get_level()[-2] != 'vif-s':
+ return vlan
+
+ # ethertype is mandatory on vif-s nodes and only exists here!
+ # ethertype uses a default of 0x88A8
+ tmp = '0x88A8'
+ if conf.exists('ethertype'):
+ tmp = conf.return_value('ethertype')
+ vlan['ethertype'] = get_ethertype(tmp)
+
+ # check if there is a Q-in-Q vlan customer interface
+ # and call this function recursively
+ add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c')
return vlan
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
index 20090fb85..6ce42b173 100644
--- a/python/vyos/debug.py
+++ b/python/vyos/debug.py
@@ -15,7 +15,7 @@
import os
import sys
-
+from datetime import datetime
def message(message, flag='', destination=sys.stdout):
"""
@@ -41,11 +41,12 @@ def message(message, flag='', destination=sys.stdout):
try:
# at boot the file is created as root:vyattacfg
# at runtime the file is created as user:vyattacfg
- # the default permission are 644
- mask = os.umask(0o113)
+ # but the helper scripts are not run as this so it
+ # need the default permission to be 666 (an not 660)
+ mask = os.umask(0o111)
with open(logfile, 'a') as f:
- f.write(_format('log', message))
+ f.write(_timed(_format('log', message)))
finally:
os.umask(mask)
@@ -80,10 +81,22 @@ def enabled(flag):
return _fromenv(flag) or _fromfile(flag)
+def _timed(message):
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ return f'{now} {message}'
+
+
+def _remove_invisible(string):
+ for char in ('\0', '\a', '\b', '\f', '\v'):
+ string = string.replace(char, '')
+ return string
+
+
def _format(flag, message):
"""
format a log message
"""
+ message = _remove_invisible(message)
return f'DEBUG/{flag.upper():<7} {message}\n'
@@ -133,7 +146,7 @@ def _contentenv(flag):
return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()
-def _contentfile(flag):
+def _contentfile(flag, default=''):
"""
Check if debug exist for a given debug flag name
@@ -153,7 +166,8 @@ def _contentfile(flag):
if not os.path.isfile(flagfile):
continue
with open(flagfile) as f:
- return f.readline().strip()
+ content = f.readline().strip()
+ return content or default
return ''
@@ -166,7 +180,7 @@ def _logfile(flag, default):
"""
# For log we return the location of the log file
- log_location = _contentenv(flag) or _contentfile(flag)
+ log_location = _contentenv(flag) or _contentfile(flag, default)
# it was not set
if not log_location:
@@ -177,6 +191,15 @@ def _logfile(flag, default):
not log_location.startswith('/config/') and \
not log_location.startswith('/var/log/'):
return default
+ # Do not allow to escape the folders
if '..' in log_location:
return default
+
+ if not os.path.exists(log_location):
+ return log_location
+
+ # this permission is unique the the config and var folder
+ stat = os.stat(log_location).st_mode
+ if stat != 0o100666:
+ return default
return log_location
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
index 79cab4a08..b12cda40f 100644
--- a/python/vyos/dicts.py
+++ b/python/vyos/dicts.py
@@ -15,6 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from vyos import ConfigError
+
+
class FixedDict(dict):
"""
FixedDict: A dictionnary not allowing new keys to be created after initialisation.
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
index d4ff9c2cd..f8fdeb6a9 100644
--- a/python/vyos/ifconfig/dhcp.py
+++ b/python/vyos/ifconfig/dhcp.py
@@ -19,28 +19,19 @@ from vyos.dicts import FixedDict
from vyos.ifconfig.control import Control
from vyos.template import render
-
-class _DHCP (Control):
- client_base = r'/var/lib/dhcp/dhclient_'
-
- def __init__(self, ifname, version, **kargs):
- super().__init__(**kargs)
- self.version = version
- self.file = {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.' + version + 'conf',
- 'pid': self.client_base + ifname + '.' + version + 'pid',
- 'lease': self.client_base + ifname + '.' + version + 'leases',
- }
-
-class _DHCPv4 (_DHCP):
+class _DHCPv4 (Control):
def __init__(self, ifname):
- super().__init__(ifname, '')
+ super().__init__()
+ config_base = r'/var/lib/dhcp/dhclient_'
self.options = FixedDict(**{
'ifname': ifname,
'hostname': '',
'client_id': '',
- 'vendor_class_id': ''
+ 'vendor_class_id': '',
+ 'conf_file': config_base + f'{ifname}.conf',
+ 'options_file': config_base + f'{ifname}.options',
+ 'pid_file': config_base + f'{ifname}.pid',
+ 'lease_file': config_base + f'{ifname}.leases',
})
# replace dhcpv4/v6 with systemd.networkd?
@@ -55,25 +46,16 @@ class _DHCPv4 (_DHCP):
>>> j = Interface('eth0')
>>> j.dhcp.v4.set()
"""
-
if not self.options['hostname']:
# read configured system hostname.
# maybe change to vyos hostd client ???
with open('/etc/hostname', 'r') as f:
self.options['hostname'] = f.read().rstrip('\n')
- render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options)
+ render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options)
+ render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options)
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- cmd += ' --exec /sbin/dhclient'
- cmd += ' --'
- # now pass arguments to dhclient binary
- cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}'
- return self._cmd(cmd.format(**self.file))
+ return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options))
def delete(self):
"""
@@ -86,44 +68,27 @@ class _DHCPv4 (_DHCP):
>>> j = Interface('eth0')
>>> j.dhcp.v4.delete()
"""
- if not os.path.isfile(self.file['pid']):
+ if not os.path.isfile(self.options['pid_file']):
self._debug_msg('No DHCP client PID found')
return None
- # with open(self.file['pid'], 'r') as f:
- # pid = int(f.read())
-
- # stop dhclient, we need to call dhclient and tell it should release the
- # aquired IP address. tcpdump tells me:
- # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000)
- # Client-IP 172.16.35.103
- # Client-Ethernet-Address 00:50:56:9d:11:df
- # Vendor-rfc1048 Extensions
- # Magic Cookie 0x63825363
- # DHCP-Message Option 53, length 1: Release
- # Server-ID Option 54, length 4: 172.16.35.254
- # Hostname Option 12, length 10: "vyos"
- #
- cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}'
- self._cmd(cmd.format(**self.file))
+ self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options))
# cleanup old config files
- for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self.file[name]):
- os.remove(self.file[name])
-
+ for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'):
+ if os.path.isfile(self.options[name]):
+ os.remove(self.options[name])
-class _DHCPv6 (_DHCP):
+class _DHCPv6 (Control):
def __init__(self, ifname):
- super().__init__(ifname, 'v6')
+ super().__init__()
self.options = FixedDict(**{
'ifname': ifname,
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
+ 'dhcpv6_pd': [],
})
- self.file.update({
- 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
- })
+ self._conf_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
def set(self):
"""
@@ -134,7 +99,7 @@ class _DHCPv6 (_DHCP):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.set_dhcpv6()
+ >>> j.dhcp.v6.set()
"""
# better save then sorry .. should be checked in interface script
@@ -143,29 +108,8 @@ class _DHCPv6 (_DHCP):
raise Exception(
'DHCPv6 temporary and parameters-only options are mutually exclusive!')
- render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options)
-
- # no longer accept router announcements on this interface
- self._write_sysfs(self.file['accept_ra'], 0)
-
- # assemble command-line to start DHCPv6 client (dhclient)
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- cmd += ' --exec /sbin/dhclient'
- cmd += ' --'
- # now pass arguments to dhclient binary
- cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}'
- # add optional arguments
- if self.options['dhcpv6_prm_only']:
- cmd += ' -S'
- if self.options['dhcpv6_temporary']:
- cmd += ' -T'
- cmd += ' {ifname}'
-
- return self._cmd(cmd.format(**self.file))
+ render(self._conf_file, 'dhcp-client/ipv6.tmpl', self.options, trim_blocks=True)
+ return self._cmd('systemctl restart dhcp6c@{ifname}.service'.format(**self.options))
def delete(self):
"""
@@ -176,33 +120,16 @@ class _DHCPv6 (_DHCP):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.del_dhcpv6()
+ >>> j.dhcp.v6.delete()
"""
- if not os.path.isfile(self.file['pid']):
- self._debug_msg('No DHCPv6 client PID found')
- return None
-
- # with open(self.file['pid'], 'r') as f:
- # pid = int(f.read())
-
- # stop dhclient
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- self._cmd(cmd.format(**self.file))
-
- # accept router announcements on this interface
- self._write_sysfs(self.options['accept_ra'], 1)
+ self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options))
# cleanup old config files
- for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self.file[name]):
- os.remove(self.file[name])
+ if os.path.isfile(self._conf_file):
+ os.remove(self._conf_file)
-class DHCP (object):
+class DHCP(object):
def __init__(self, ifname):
self.v4 = _DHCPv4(ifname)
self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 62c30dbf7..61f2c6482 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -42,6 +42,7 @@ from vyos.ifconfig.control import Control
from vyos.ifconfig.dhcp import DHCP
from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.operational import Operational
+from vyos.ifconfig import Section
class Interface(Control):
@@ -133,8 +134,12 @@ class Interface(Control):
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
},
+ 'ipv6_accept_ra': {
+ 'validate': lambda ara: assert_range(ara,0,3),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
'ipv6_autoconf': {
- 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'validate': lambda aco: assert_range(aco,0,2),
'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
},
'ipv6_forwarding': {
@@ -219,7 +224,7 @@ class Interface(Control):
else:
raise Exception('interface "{}" not found'.format(self.config['ifname']))
- # list of assigned IP addresses
+ # temporary list of assigned IP addresses
self._addr = []
self.operational = self.OperationalClass(ifname)
@@ -240,15 +245,11 @@ class Interface(Control):
>>> i = Interface('eth0')
>>> i.remove()
"""
- # stop DHCP(v6) if running
- self.dhcp.v4.delete()
- self.dhcp.v6.delete()
# remove all assigned IP addresses from interface - this is a bit redundant
# as the kernel will remove all addresses on interface deletion, but we
# can not delete ALL interfaces, see below
- for addr in self.get_addr():
- self.del_addr(addr)
+ self.flush_addrs()
# ---------------------------------------------------------------------
# Any class can define an eternal regex in its definition
@@ -412,6 +413,21 @@ class Interface(Control):
"""
return self.set_interface('arp_ignore', arp_ignore)
+ def set_ipv6_accept_ra(self, accept_ra):
+ """
+ Accept Router Advertisements; autoconfigure using them.
+
+ It also determines whether or not to transmit Router Solicitations.
+ If and only if the functional setting is to accept Router
+ Advertisements, Router Solicitations will be transmitted.
+
+ 0 - Do not accept Router Advertisements.
+ 1 - (default) Accept Router Advertisements if forwarding is disabled.
+ 2 - Overrule forwarding behaviour. Accept Router Advertisements even if
+ forwarding is enabled.
+ """
+ return self.set_interface('ipv6_accept_ra', accept_ra)
+
def set_ipv6_autoconf(self, autoconf):
"""
Autoconfigure addresses using Prefix Information in Router
@@ -419,39 +435,28 @@ class Interface(Control):
"""
return self.set_interface('ipv6_autoconf', autoconf)
- def set_ipv6_eui64_address(self, prefix):
+ def add_ipv6_eui64_address(self, prefix):
"""
Extended Unique Identifier (EUI), as per RFC2373, allows a host to
- assign iteslf a unique IPv6 address based on a given IPv6 prefix.
+ assign itself a unique IPv6 address based on a given IPv6 prefix.
- If prefix is passed address is assigned, if prefix is '' address is
- removed from interface.
+ Calculate the EUI64 from the interface's MAC, then assign it
+ with the given prefix to the interface.
"""
- # if prefix is an empty string convert it to None so mac2eui64 works
- # as expected
- if not prefix:
- prefix = None
eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.add_addr(f'{eui64}/{prefixlen}')
- if not prefix:
- # if prefix is empty - thus removed - we need to walk through all
- # interface IPv6 addresses and find the one with the calculated
- # EUI-64 identifier. The address is then removed
- for addr in self.get_addr():
- addr_wo_prefix = addr.split('/')[0]
- if is_ipv6(addr_wo_prefix):
- if eui64 in IPv6Address(addr_wo_prefix).exploded:
- self.del_addr(addr)
-
- return None
+ def del_ipv6_eui64_address(self, prefix):
+ """
+ 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}')
- # calculate and add EUI-64 IPv6 address
- if IPv6Network(prefix):
- # we also need to take the subnet length into account
- prefix = prefix.split('/')[1]
- eui64 = f'{eui64}/{prefix}'
- self.add_addr(eui64 )
def set_ipv6_forwarding(self, forwarding):
"""
@@ -632,7 +637,8 @@ class Interface(Control):
def add_addr(self, addr):
"""
Add IP(v6) address to interface. Address is only added if it is not
- already assigned to that interface.
+ already assigned to that interface. Address format must be validated
+ and compressed/normalized before calling this function.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: add IPv4 address to interface
@@ -640,6 +646,7 @@ class Interface(Control):
dhcp: start dhclient (IPv4) on interface
dhcpv6: start dhclient (IPv6) on interface
+ Returns False if address is already assigned and wasn't re-added.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
@@ -648,32 +655,41 @@ class Interface(Control):
>>> j.get_addr()
['192.0.2.1/24', '2001:db8::ffff/64']
"""
+ # XXX: normalize/compress with ipaddress if calling functions don't?
+ # is subnet mask always passed, and in the same way?
- # cache new IP address which is assigned to interface
- self._addr.append(addr)
+ # do not add same address twice
+ if addr in self._addr:
+ return False
- # we can not have both DHCP and static IPv4 addresses assigned to an interface
- if 'dhcp' in self._addr:
- for addr in self._addr:
- # do not change below 'if' ordering esle you will get an exception as:
- # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
- if addr != 'dhcp' and is_ipv4(addr):
- raise ConfigError(
- "Can't configure both static IPv4 and DHCP address on the same interface")
+ # we can't have both DHCP and static IPv4 addresses assigned
+ for a in self._addr:
+ if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or
+ ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ):
+ raise ConfigError((
+ "Can't configure both static IPv4 and DHCP address "
+ "on the same interface"))
+ # add to interface
if addr == 'dhcp':
self.dhcp.v4.set()
elif addr == 'dhcpv6':
self.dhcp.v6.set()
+ elif not is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"')
else:
- if not is_intf_addr_assigned(self.config['ifname'], addr):
- cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname'])
- return self._cmd(cmd)
+ return False
+
+ # add to cache
+ self._addr.append(addr)
+
+ return True
def del_addr(self, addr):
"""
- Delete IP(v6) address to interface. Address is only added if it is
- assigned to that interface.
+ Delete IP(v6) address from interface. Address is only deleted if it is
+ assigned to that interface. Address format must be exactly the same as
+ was used when adding the address.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: delete IPv4 address from interface
@@ -681,6 +697,7 @@ class Interface(Control):
dhcp: stop dhclient (IPv4) on interface
dhcpv6: stop dhclient (IPv6) on interface
+ Returns False if address isn't already assigned and wasn't deleted.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
@@ -692,11 +709,51 @@ class Interface(Control):
>>> j.get_addr()
['2001:db8::ffff/64']
"""
+
+ # remove from interface
if addr == 'dhcp':
self.dhcp.v4.delete()
elif addr == 'dhcpv6':
self.dhcp.v6.delete()
+ elif is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
else:
- if is_intf_addr_assigned(self.config['ifname'], addr):
- cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname'])
- return self._cmd(cmd)
+ return False
+
+ # remove from cache
+ if addr in self._addr:
+ self._addr.remove(addr)
+
+ return True
+
+ def flush_addrs(self):
+ """
+ Flush all addresses from an interface, including DHCP.
+
+ Will raise an exception on error.
+ """
+ # stop DHCP(v6) if running
+ self.dhcp.v4.delete()
+ self.dhcp.v6.delete()
+
+ # flush all addresses
+ self._cmd(f'ip addr flush dev "{self.ifname}"')
+
+ def add_to_bridge(self, br):
+ """
+ Adds the interface to the bridge with the passed port config.
+
+ Returns False if bridge doesn't exist.
+ """
+
+ # check if the bridge exists (on boot it doesn't)
+ if br not in Section.interfaces('bridge'):
+ return False
+
+ self.flush_addrs()
+ # add interface to bridge - use Section.klass to get BridgeIf class
+ Section.klass(br)(br, create=False).add_port(self.ifname)
+
+ # TODO: port config (STP)
+
+ return True
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 092236fef..926c22e8a 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -54,7 +54,7 @@ class Section:
name = name.rstrip('0123456789')
name = name.rstrip('.')
if vlan:
- name = name.rstrip('0123456789')
+ name = name.rstrip('0123456789.')
return name
@classmethod
@@ -137,3 +137,22 @@ class Section:
eth, lo, vxlan, dum, ...
"""
return list(cls._prefixes.keys())
+
+ @classmethod
+ def get_config_path(cls, name):
+ """
+ get config path to interface with .vif or .vif-s.vif-c
+ example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2'
+ Returns False if interface name is invalid (not found in sections)
+ """
+ sect = cls.section(name)
+ if sect:
+ splinterface = name.split('.')
+ intfpath = f'{sect} {splinterface[0]}'
+ if len(splinterface) == 2:
+ intfpath += f' vif {splinterface[1]}'
+ elif len(splinterface) == 3:
+ intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}'
+ return intfpath
+ else:
+ return False
diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py
index 7b1e00d87..d68e8f6cd 100644
--- a/python/vyos/ifconfig/vlan.py
+++ b/python/vyos/ifconfig/vlan.py
@@ -101,26 +101,26 @@ class VLAN:
>>> i.add_vlan(10)
"""
vlan_ifname = self.config['ifname'] + '.' + str(vlan_id)
- if not os.path.exists(f'/sys/class/net/{vlan_ifname}'):
- self._vlan_id = int(vlan_id)
-
- if ethertype:
- self._ethertype = ethertype
- ethertype = 'proto {}'.format(ethertype)
-
- # Optional ingress QOS mapping
- opt_i = ''
- if ingress_qos:
- opt_i = 'ingress-qos-map ' + ingress_qos
- # Optional egress QOS mapping
- opt_e = ''
- if egress_qos:
- opt_e = 'egress-qos-map ' + egress_qos
-
- # create interface in the system
- cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
- .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
- self._cmd(cmd)
+ if os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+ return self.__class__(vlan_ifname)
+
+ if ethertype:
+ self._ethertype = ethertype
+ ethertype = 'proto {}'.format(ethertype)
+
+ # Optional ingress QOS mapping
+ opt_i = ''
+ if ingress_qos:
+ opt_i = 'ingress-qos-map ' + ingress_qos
+ # Optional egress QOS mapping
+ opt_e = ''
+ if egress_qos:
+ opt_e = 'egress-qos-map ' + egress_qos
+
+ # create interface in the system
+ cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
+ .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
+ self._cmd(cmd)
# return new object mapping to the newly created interface
# we can now work on this object for e.g. IP address setting
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index fdf5d9347..027b5ea8c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -208,7 +208,7 @@ class WireGuardIf(Interface):
else:
cmd += aip
if self.config['endpoint']:
- cmd += " endpoint {}".format(self.config['endpoint'])
+ cmd += " endpoint '{}'".format(self.config['endpoint'])
cmd += " persistent-keepalive {}".format(self.config['keepalive'])
self._cmd(cmd)
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index 899fd17da..a53136ebf 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -16,6 +16,53 @@
from netifaces import interfaces
from vyos import ConfigError
+def apply_all_vlans(intf, intfconfig):
+ """
+ Function applies all VLANs to the passed interface.
+
+ intf: object of Interface class
+ intfconfig: dict with interface configuration
+ """
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in intfconfig['vif_s_remove']:
+ intf.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s_id, vif_s in intfconfig['vif_s'].items():
+ s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in intfconfig['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c_id, vif_c in vif_s['vif_c'].items():
+ c_vlan = s_vlan.add_vlan(vif_c_id)
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in intfconfig['vif_remove']:
+ intf.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif_id, vif in intfconfig['vif'].items():
+ # QoS priority mapping can only be set during interface creation
+ # so we delete the interface first if required.
+ if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
+ try:
+ # on system bootup the above condition is true but the interface
+ # does not exists, which throws an exception, but that's legal
+ intf.del_vlan(vif_id)
+ except:
+ pass
+
+ vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
+ apply_vlan_config(vlan, vif)
+
+
def apply_vlan_config(vlan, config):
"""
Generic function to apply a VLAN configuration from a dictionary
@@ -40,6 +87,9 @@ def apply_vlan_config(vlan, config):
if config['dhcpv6_temporary']:
vlan.dhcp.v6.options['dhcpv6_temporary'] = True
+ if config['dhcpv6_pd']:
+ vlan.dhcp.v6.options['dhcpv6_pd'] = config['dhcpv6_pd']
+
# update interface description used e.g. within SNMP
vlan.set_alias(config['description'])
# ignore link state changes
@@ -54,6 +104,8 @@ def apply_vlan_config(vlan, config):
vlan.set_arp_ignore(config['ip_enable_arp_ignore'])
# configure Proxy ARP
vlan.set_proxy_arp(config['ip_proxy_arp'])
+ # IPv6 accept RA
+ vlan.set_ipv6_accept_ra(config['ipv6_accept_ra'])
# IPv6 address autoconfiguration
vlan.set_ipv6_autoconf(config['ipv6_autoconf'])
# IPv6 forwarding
@@ -63,13 +115,23 @@ def apply_vlan_config(vlan, config):
# Maximum Transmission Unit (MTU)
vlan.set_mtu(config['mtu'])
- # assign/remove VRF
- vlan.set_vrf(config['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not config['is_bridge_member']:
+ vlan.set_vrf(config['vrf'])
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in config['ipv6_eui64_prefix_remove']:
+ vlan.del_ipv6_eui64_address(addr)
# Change VLAN interface MAC address
if config['mac']:
vlan.set_mac(config['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in config['ipv6_eui64_prefix']:
+ vlan.add_ipv6_eui64_address(addr)
+
# enable/disable VLAN interface
if config['disable']:
vlan.set_admin_state('down')
@@ -84,46 +146,97 @@ def apply_vlan_config(vlan, config):
for addr in config['address']:
vlan.add_addr(addr)
+ # re-add ourselves to any bridge we might have fallen out of
+ if config['is_bridge_member']:
+ vlan.add_to_bridge(config['is_bridge_member'])
+
def verify_vlan_config(config):
"""
Generic function to verify VLAN config consistency. Instead of re-
implementing this function in multiple places use single source \o/
"""
- for vif in config['vif']:
+ # config['vif'] is a dict with ids as keys and config dicts as values
+ for vif in config['vif'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = vif['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( vif['is_bridge_member']
+ and ( vif['address']
+ or vif['ipv6_eui64_prefix']
+ or vif['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif interface {vif["intf"]} '
+ f'which is a member of bridge {vif["is_bridge_member"]}'))
+
+ if vif['vrf']:
+ if vif['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif["vrf"]}" does not exist')
+
+ if vif['is_bridge_member']:
+ raise ConfigError((
+ f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} '
+ f'and bridge {vif["is_bridge_member"]} at the same time!'))
# e.g. wireless interface has no vif_s support
# thus we bail out eraly.
if 'vif_s' not in config.keys():
return
- for vif_s in config['vif_s']:
- for vif in config['vif']:
- if vif['id'] == vif_s['id']:
- raise ConfigError('Can not use identical ID on vif and vif-s interface')
+ # config['vif_s'] is a dict with ids as keys and config dicts as values
+ for vif_s_id, vif_s in config['vif_s'].items():
+ for vif_id, vif in config['vif'].items():
+ if vif_id == vif_s_id:
+ raise ConfigError((
+ f'Cannot use identical ID on vif "{vif["intf"]}" '
+ f'and vif-s "{vif_s["intf"]}"'))
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- vrf_name = vif_s['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
-
- for vif_c in vif_s['vif_c']:
+ raise ConfigError((
+ 'DHCPv6 temporary and parameters-only options are mutually '
+ 'exclusive!'))
+
+ if ( vif_s['is_bridge_member']
+ and ( vif_s['address']
+ or vif_s['ipv6_eui64_prefix']
+ or vif_s['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif-s interface {vif_s["intf"]} '
+ f'which is a member of bridge {vif_s["is_bridge_member"]}'))
+
+ if vif_s['vrf']:
+ if vif_s['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist')
+
+ if vif_s['is_bridge_member']:
+ raise ConfigError((
+ f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} '
+ f'and bridge {vif_s["is_bridge_member"]} at the same time!'))
+
+ # vif_c is a dict with ids as keys and config dicts as values
+ for vif_c in vif_s['vif_c'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- vrf_name = vif_c['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
-
+ raise ConfigError((
+ 'DHCPv6 temporary and parameters-only options are '
+ 'mutually exclusive!'))
+
+ if ( vif_c['is_bridge_member']
+ and ( vif_c['address']
+ or vif_c['ipv6_eui64_prefix']
+ or vif_c['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif-c interface {vif_c["intf"]} '
+ f'which is a member of bridge {vif_c["is_bridge_member"]}'))
+
+ if vif_c['vrf']:
+ if vif_c['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist')
+
+ if vif_c['is_bridge_member']:
+ raise ConfigError((
+ f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} '
+ f'and bridge {vif_c["is_bridge_member"]} at the same time!'))
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 6c73ce753..e4b253ed3 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -19,6 +19,7 @@ from jinja2 import Environment
from jinja2 import FileSystemLoader
from vyos.defaults import directories
+from vyos.util import chmod, chown, makedir
# reuse the same Environment to improve performance
@@ -32,7 +33,7 @@ _templates_mem = {
}
-def render(destination, template, content, trim_blocks=False, formater=None):
+def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None):
"""
render a template from the template directory, it will raise on any errors
destination: the file where the rendered template must be saved
@@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None):
(recovering the load time and overhead caused by having the file out of the code)
"""
+ # Create the directory if it does not exists
+ folder = os.path.dirname(destination)
+ makedir(folder, user, group)
+
# Setup a renderer for the given template
# This is cached and re-used for performance
if template not in _templates_mem[trim_blocks]:
@@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None):
# Write client config file
with open(destination, 'w') as f:
f.write(content)
+
+ chmod(destination, permission)
+ chown(destination, user, group)
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 4340332d3..381cd0358 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import sys
#
# NOTE: Do not import full classes here, move your import to the function
@@ -25,7 +26,7 @@ import os
# which all have slighty different behaviour
from subprocess import Popen, PIPE, STDOUT, DEVNULL
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
popen is a wrapper helper aound subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- STDOUT, send the data to be merged with stdout
- DEVNULL, discard the output
decode: specify the expected text encoding (utf-8, ascii, ...)
+ the default is explicitely utf-8 which is python's own default
usage:
to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
"""
from vyos import debug
+ from vyos import airbag
# log if the flag is set, otherwise log if command is set
if not debug.enabled(flag):
flag = 'command'
@@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
stdin=stdin, stdout=stdout, stderr=stderr,
env=env, shell=use_shell,
)
- tmp = p.communicate(input, timeout)
- out1 = b''
- out2 = b''
+
+ pipe = p.communicate(input, timeout)
+
+ pipe_out = b''
if stdout == PIPE:
- out1 = tmp[0]
+ pipe_out = pipe[0]
+
+ pipe_err = b''
if stderr == PIPE:
- out2 += tmp[1]
- decoded1 = out1.decode(decode) if decode else out1.decode()
- decoded2 = out2.decode(decode) if decode else out2.decode()
- decoded1 = decoded1.replace('\r\n', '\n').strip()
- decoded2 = decoded2.replace('\r\n', '\n').strip()
- nl = '\n' if decoded1 and decoded2 else ''
- decoded = decoded1 + nl + decoded2
- if decoded:
- ret_msg = f"returned:\n{decoded}"
- debug.message(ret_msg, flag)
- return decoded, p.returncode
+ pipe_err = pipe[1]
+
+ str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
+ str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
+
+ out_msg = f"returned (out):\n{str_out}"
+ if str_out:
+ debug.message(out_msg, flag)
+
+ if str_err:
+ err_msg = f"returned (err):\n{str_err}"
+ # this message will also be send to syslog via airbag
+ debug.message(err_msg, flag, destination=sys.stderr)
+
+ # should something go wrong, report this too via airbag
+ airbag.noteworthy(cmd_msg)
+ airbag.noteworthy(out_msg)
+ airbag.noteworthy(err_msg)
+
+ return str_out, p.returncode
def run(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=DEVNULL, stderr=None, decode=None):
+ stdout=DEVNULL, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which discard the stdout and
will return the error code of a command
@@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None,
- raising=None, message=''):
+ stdout=PIPE, stderr=PIPE, decode='utf-8',
+ raising=None, message='', expect=[0]):
"""
A wrapper around vyos.util.popen, which returns the stdout and
will raise the error code of a command
raising: specify which call should be used when raising (default is OSError)
the class should only require a string as parameter
+ expect: a list of error codes to consider as normal
"""
decoded, code = popen(
command, flag,
@@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
env=env, shell=shell,
decode=decode,
)
- if code != 0:
+ if code not in expect:
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
feedback += f'returned: {decoded}\n'
@@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
def call(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which print the stdout and
will return the error code of a command
@@ -197,10 +213,24 @@ def chown(path, user, group):
from pwd import getpwnam
from grp import getgrnam
- if os.path.exists(path):
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(path, uid, gid)
+ if user is None or group is None:
+ return False
+
+ if not os.path.exists(path):
+ return False
+
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+ return True
+
+
+def chmod(path, bitmask):
+ if not os.path.exists(path):
+ return
+ if bitmask is None:
+ return
+ os.chmod(path, bitmask)
def chmod_600(path):
@@ -231,6 +261,13 @@ def chmod_755(path):
os.chmod(path, bitmask)
+def makedir(path, user=None, group=None):
+ if os.path.exists(path):
+ return
+ os.mkdir(path)
+ chown(path, user, group)
+
+
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
@@ -352,12 +389,9 @@ def get_cfg_group_id():
def file_is_persistent(path):
import re
- if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)):
- warning = "Warning: file {0} is outside the /config directory\n".format(path)
- warning += "It will not be automatically migrated to a new image on system update"
- return (False, warning)
- else:
- return (True, None)
+ location = r'^(/config|/opt/vyatta/etc/config)'
+ absolute = os.path.abspath(os.path.dirname(path))
+ return re.match(location,absolute)
def commit_in_progress():
@@ -461,3 +495,74 @@ def get_half_cpus():
if cpu > 1:
cpu /= 2
return int(cpu)
+
+def ifname_from_config(conf):
+ """
+ Gets interface name with VLANs from current config level.
+ Level must be at the interface whose name we want.
+
+ Example:
+ >>> from vyos.util import ifname_from_config
+ >>> from vyos.config import Config
+ >>> conf = Config()
+ >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2')
+ >>> ifname_from_config(conf)
+ 'eth0.1.2'
+ """
+ level = conf.get_level()
+
+ # vlans
+ if level[-2] == 'vif' or level[-2] == 'vif-s':
+ return level[-3] + '.' + level[-1]
+ if level[-2] == 'vif-c':
+ return level[-5] + '.' + level[-3] + '.' + level[-1]
+
+ # no vlans
+ return level[-1]
+
+def get_bridge_member_config(conf, br, intf):
+ """
+ Gets bridge port (member) configuration
+
+ Arguments:
+ conf: Config
+ br: bridge name
+ intf: interface name
+
+ Returns:
+ dict with the configuration
+ False if bridge or bridge port doesn't exist
+ """
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ bridge = f'interfaces bridge {br}'
+ member = f'{bridge} member interface {intf}'
+ if not ( conf.exists(bridge) and conf.exists(member) ):
+ return False
+
+ # default bridge port configuration
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
+ memberconf = {
+ 'cost': 100,
+ 'priority': 32,
+ 'arp_cache_tmo': 30,
+ 'disable_link_detect': 1,
+ }
+
+ if conf.exists(f'{member} cost'):
+ memberconf['cost'] = int(conf.return_value(f'{member} cost'))
+
+ if conf.exists(f'{member} priority'):
+ memberconf['priority'] = int(conf.return_value(f'{member} priority'))
+
+ if conf.exists(f'{bridge} ip arp-cache-timeout'):
+ memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout'))
+
+ if conf.exists(f'{bridge} disable-link-detect'):
+ memberconf['disable_link_detect'] = 2
+
+ conf.set_level(old_level)
+ return memberconf
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 446f6e4ca..6304fa8de 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -205,7 +205,7 @@ def assert_number(n):
def assert_positive(n, smaller=0):
assert_number(n)
if int(n) < smaller:
- raise ValueError(f'{n} is smaller than {limit}')
+ raise ValueError(f'{n} is smaller than {smaller}')
def assert_mtu(mtu, min=68, max=9000):
@@ -241,26 +241,66 @@ def assert_mac(m):
if octets[:5] == (0, 0, 94, 0, 1):
raise ValueError(f'{m} is a VRRP MAC address')
-def is_bridge_member(conf, interface):
+def is_member(conf, interface, intftype=None):
"""
- Checks if passed interfaces is part of a bridge device or not.
-
- Returns a tuple:
- None -> Interface not a bridge member
- Bridge -> Interface is a member of this bridge
+ Checks if passed interface is member of other interface of specified type.
+ intftype is optional, if not passed it will search all known types
+ (currently bridge and bonding)
+
+ Returns:
+ None -> Interface is not a member
+ interface name -> Interface is a member of this interface
+ False -> interface type cannot have members
"""
ret_val = None
- old_level = conf.get_level()
+
+ if intftype not in ['bonding', 'bridge', None]:
+ raise ValueError((
+ f'unknown interface type "{intftype}" or it cannot '
+ f'have member interfaces'))
+
+ intftype = ['bonding', 'bridge'] if intftype == None else [intftype]
# set config level to root
+ old_level = conf.get_level()
conf.set_level([])
- base = ['interfaces', 'bridge']
- for bridge in conf.list_nodes(base):
- members = conf.list_nodes(base + [bridge, 'member', 'interface'])
- if interface in members:
- ret_val = bridge
- break
+
+ for it in intftype:
+ base = 'interfaces ' + it
+ for intf in conf.list_nodes(base):
+ memberintf = f'{base} {intf} member interface'
+ if conf.is_tag(memberintf):
+ if interface in conf.list_nodes(memberintf):
+ ret_val = intf
+ break
+ elif conf.is_leaf(memberintf):
+ if ( conf.exists(memberintf) and
+ interface in conf.return_values(memberintf) ):
+ ret_val = intf
+ break
old_level = conf.set_level(old_level)
return ret_val
+def has_address_configured(conf, intf):
+ """
+ Checks if interface has an address configured.
+ Checks the following config nodes:
+ 'address', 'ipv6 address eui64', 'ipv6 address autoconf'
+
+ Returns True if interface has address configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ intfpath = 'interfaces ' + Section.get_config_path(intf)
+ if ( conf.exists(f'{intfpath} address') or
+ conf.exists(f'{intfpath} ipv6 address autoconf') or
+ conf.exists(f'{intfpath} ipv6 address eui64') ):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret