summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl29
-rw-r--r--data/templates/openvpn/server.conf.tmpl2
-rw-r--r--interface-definitions/vpn_sstp.xml.in15
-rw-r--r--python/vyos/config.py15
-rw-r--r--python/vyos/configdict.py89
-rw-r--r--python/vyos/debug.py7
-rw-r--r--python/vyos/ifconfig/interface.py68
-rw-r--r--python/vyos/ifconfig/section.py21
-rw-r--r--python/vyos/ifconfig_vlan.py146
-rw-r--r--python/vyos/util.py71
-rw-r--r--python/vyos/validate.py66
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py159
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py83
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py36
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py100
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py22
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py28
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py41
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py84
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py43
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py28
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py53
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py43
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py37
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py16
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py56
-rwxr-xr-xsrc/services/vyos-hostsd13
27 files changed, 879 insertions, 492 deletions
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl
index c3dc83429..411fca489 100644
--- a/data/templates/accel-ppp/sstp.config.tmpl
+++ b/data/templates/accel-ppp/sstp.config.tmpl
@@ -9,6 +9,9 @@ chap-secrets
radius
{% endif -%}
ippool
+ipv6pool
+ipv6_nd
+ipv6_dhcp
{% for proto in auth_proto %}
{{proto}}
@@ -51,6 +54,14 @@ dns{{ loop.index }}={{ dns }}
{% endfor -%}
{% endif %}
+{% if dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+
{% if auth_mode == 'local' %}
[chap-secrets]
chap-secrets={{ chap_secrets_file }}
@@ -87,6 +98,9 @@ check-ip=1
{% if mtu %}
mtu={{ mtu }}
{% endif -%}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
{% if ppp_mppe %}
mppe={{ ppp_mppe }}
@@ -101,6 +115,21 @@ lcp-echo-failure={{ ppp_echo_failure }}
lcp-echo-timeout={{ ppp_echo_timeout }}
{% endif %}
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if client_ipv6_delegate_prefix %}
+[ipv6-dhcp]
+verbose=1
+{% endif %}
+
{% if radius_shaper_attr %}
[shaper]
verbose=1
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index 75ab602f8..401f8e04b 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -74,7 +74,7 @@ nobind
topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %}
{%- endif %}
-{%- if bridge_member %}
+{%- if is_bridge_member %}
mode server
tls-server
{%- else %}
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
index 7e4471015..f0c93b882 100644
--- a/interface-definitions/vpn_sstp.xml.in
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -207,19 +207,8 @@
</leafNode>
</children>
</node>
- <leafNode name="name-server">
- <properties>
- <help>DNS servers propagated to clients</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ #include <include/accel-name-server.xml.in>
#include <include/interface-mtu-68-1500.xml.i>
</children>
</node>
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 75055a603..0bc6be12a 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -155,7 +155,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 +166,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 +177,7 @@ class Config(object):
Returns:
str: current edit level
"""
- return(self._level)
+ return(self._level.copy())
def exists(self, path):
"""
@@ -386,7 +386,7 @@ class Config(object):
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -407,7 +407,7 @@ class Config(object):
nodes = []
if not nodes:
- return(default)
+ return(default.copy())
else:
return(nodes)
@@ -448,7 +448,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
@@ -465,7 +464,7 @@ class Config(object):
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -488,6 +487,6 @@ class Config(object):
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 e1b704a31..5ca369f66 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -23,7 +23,8 @@ 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):
"""
@@ -128,9 +129,10 @@ vlan_default = {
'ipv6_dup_addr_detect': 1,
'ingress_qos': '',
'ingress_qos_changed': False,
+ 'is_bridge_member': False,
'mac': '',
'mtu': 1500,
- 'vif_c': [],
+ 'vif_c': {},
'vif_c_remove': [],
'vrf': ''
}
@@ -197,10 +199,7 @@ def intf_to_dict(conf, default):
"""
intf = deepcopy(default)
-
- # retrieve configured interface addresses
- if conf.exists('address'):
- intf['address'] = conf.return_values('address')
+ intf['intf'] = ifname_from_config(conf)
# retrieve interface description
if conf.exists('description'):
@@ -255,23 +254,22 @@ def intf_to_dict(conf, default):
if conf.exists('ipv6 address autoconf'):
intf['ipv6_autoconf'] = 1
- # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- intf['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
-
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
intf['ipv6_forwarding'] = 0
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- intf['mac'] = conf.return_value('mac')
+ # 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'):
intf['ipv6_dup_addr_detect'] = int(
conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # Media Access Control (MAC) address
+ if conf.exists('mac'):
+ intf['mac'] = conf.return_value('mac')
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
intf['mtu'] = int(conf.return_value('mtu'))
@@ -298,60 +296,56 @@ def intf_to_dict(conf, default):
if intf['ingress_qos'] != conf.return_effective_value('ingress-qos'):
intf['ingress_qos_changed'] = True
- disabled = disable_state(conf)
+ # Get the interface addresses
+ intf['address'] = conf.return_values('address')
- # Get the interface IPs
- eff_addr = conf.return_effective_values('address')
- act_addr = 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)
- eff_eui = conf.return_effective_values('ipv6 address eui64')
- act_eui = conf.return_values('ipv6 address eui64')
+ 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 what should stay or be removed
+ # Determine if the interface should be disabled
+ disabled = disable_state(conf)
if disabled == disable.both:
# was and is still disabled
intf['disable'] = True
- intf['address'] = []
- intf['address_remove'] = []
- intf['ipv6_eui64_prefix'] = []
- intf['ipv6_eui64_prefix_remove'] = []
elif disabled == disable.now:
# it is now disable but was not before
intf['disable'] = True
- intf['address'] = []
- intf['address_remove'] = eff_addr
- intf['ipv6_eui64_prefix'] = []
- intf['ipv6_eui64_prefix_remove'] = eff_eui
elif disabled == disable.was:
# it was disable but not anymore
intf['disable'] = False
- intf['address'] = act_addr
- intf['address_remove'] = []
- intf['ipv6_eui64_prefix'] = act_eui
- intf['ipv6_eui64_prefix_remove'] = []
else:
# normal change
intf['disable'] = False
- intf['address'] = act_addr
- intf['address_remove'] = list_diff(eff_addr, act_addr)
- intf['ipv6_eui64_prefix'] = act_eui
- intf['ipv6_eui64_prefix_remove'] = list_diff(eff_eui, act_eui)
- # Remove the default link-local address if set.
- if conf.exists('ipv6 address no-default-link-local'):
+ # 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')
- # Find out if MAC has changed
+ # 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 exists, it can not have changed
+ # If the interface does not exist, it could not have changed
pass
return intf, disable
@@ -381,8 +375,7 @@ def add_to_dict(conf, disabled, ifdict, section, key):
# the section to parse for vlan
sections = []
- # Determine interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed from the bond
+ # determine which interfaces to add or remove based on disable state
if disabled == disable.both:
# was and is still disabled
ifdict[f'{key}_remove'] = []
@@ -395,7 +388,7 @@ def add_to_dict(conf, disabled, ifdict, section, key):
sections = active
else:
# normal change
- # get vif-s interfaces (currently effective) - to determine which vif-s
+ # get interfaces (currently effective) - to determine which
# interface is no longer present and needs to be removed
ifdict[f'{key}_remove'] = list_diff(effect, active)
sections = active
@@ -406,7 +399,8 @@ def add_to_dict(conf, disabled, ifdict, section, key):
for s in sections:
# set config level to vif interface
conf.set_level(current_level + [section, s])
- ifdict[f'{key}'].append(vlan_to_dict(conf))
+ # 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)
@@ -416,13 +410,9 @@ def add_to_dict(conf, disabled, ifdict, section, key):
def vlan_to_dict(conf, default=vlan_default):
vlan, disabled = intf_to_dict(conf, default)
- # get the '100' in 'interfaces bonding bond0 vif-s 100
- vlan['id'] = conf.get_level()[-1]
-
- current_level = conf.get_level()
# if this is a not within vif-s node, we are done
- if current_level[-2] != 'vif-s':
+ if conf.get_level()[-2] != 'vif-s':
return vlan
# ethertype is mandatory on vif-s nodes and only exists here!
@@ -434,7 +424,6 @@ def vlan_to_dict(conf, default=vlan_default):
# 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 60e5291c1..6ce42b173 100644
--- a/python/vyos/debug.py
+++ b/python/vyos/debug.py
@@ -86,10 +86,17 @@ def _timed(message):
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'
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index de5ca369f..7b42e3399 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -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):
@@ -638,6 +639,10 @@ class Interface(Control):
# XXX: normalize/compress with ipaddress if calling functions don't?
# is subnet mask always passed, and in the same way?
+ # do not add same address twice
+ if addr in self._addr:
+ return False
+
# 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
@@ -646,27 +651,20 @@ class Interface(Control):
"Can't configure both static IPv4 and DHCP address "
"on the same interface"))
- # do not add same address twice
- if addr in self._addr:
- return False
-
# add to interface
if addr == 'dhcp':
- self._addr.append(addr)
self.dhcp.v4.set()
- return True
-
- if addr == 'dhcpv6':
- self._addr.append(addr)
+ elif addr == 'dhcpv6':
self.dhcp.v6.set()
- return True
-
- if not is_intf_addr_assigned(self.ifname, addr):
- self._addr.append(addr)
+ elif not is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"')
- return True
+ else:
+ return False
+
+ # add to cache
+ self._addr.append(addr)
- return False
+ return True
def del_addr(self, addr):
"""
@@ -693,24 +691,21 @@ class Interface(Control):
['2001:db8::ffff/64']
"""
- # remove from cache (dhcp, and dhcpv6 can not be in it)
- if addr in self._addr:
- self._addr.remove(addr)
-
# remove from interface
if addr == 'dhcp':
self.dhcp.v4.delete()
- return True
-
- if addr == 'dhcpv6':
+ elif addr == 'dhcpv6':
self.dhcp.v6.delete()
- return True
-
- if is_intf_addr_assigned(self.ifname, addr):
+ elif is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
- return True
+ else:
+ return False
+
+ # remove from cache
+ if addr in self._addr:
+ self._addr.remove(addr)
- return False
+ return True
def flush_addrs(self):
"""
@@ -724,3 +719,22 @@ class Interface(Control):
# 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 ee009f7f9..09fb8c802 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
@@ -63,8 +110,10 @@ 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']:
@@ -92,46 +141,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}"'))
# 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/util.py b/python/vyos/util.py
index 92b6f7992..e598e0ff3 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -498,3 +498,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..e083604c5 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -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
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index a174e33e4..5a2ff9eef 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -20,12 +20,12 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BondIf, Section
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.ifconfig import BondIf
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.config import Config
from vyos.util import call, cmd
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, has_address_configured
from vyos import ConfigError
default_config_data = {
@@ -63,9 +63,9 @@ default_config_data = {
'shutdown_required': False,
'mtu': 1500,
'primary': '',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
@@ -111,15 +111,12 @@ def get_config():
bond = deepcopy(default_config_data)
bond['intf'] = ifname
bond['deleted'] = True
- # check if interface is member if a bridge
- bond['is_bridge_member'] = is_bridge_member(conf, ifname)
return bond
# set new configuration level
conf.set_level(cfg_base)
bond, disabled = intf_to_dict(conf, default_config_data)
- bond['intf'] = ifname
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
@@ -175,22 +172,38 @@ def get_config():
def verify(bond):
if bond['deleted']:
if bond['is_bridge_member']:
- interface = bond['intf']
- bridge = bond['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{bond["intf"]}" as it is a '
+ f'member of bridge "{bond["is_bridge_member"]}"!'))
+
return None
- if len (bond['arp_mon_tgt']) > 16:
- raise ConfigError('The maximum number of targets that can be specified is 16')
+ if len(bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
- raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in mode "{}"!'.format(bond['mode']))
+ raise ConfigError((
+ 'Mode dependency failed, primary not supported in mode '
+ f'"{bond["mode"]}"!'))
+
+ if ( bond['is_bridge_member']
+ and ( bond['address']
+ or bond['ipv6_eui64_prefix']
+ or bond['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{bond["intf"]}" '
+ f'as it is a member of bridge "{bond["is_bridge_member"]}"!'))
+
+ if bond['vrf']:
+ if bond['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{bond["vrf"]}" does not exist')
- vrf_name = bond['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if bond['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{bond["intf"]}" cannot be member of VRF '
+ f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(bond)
@@ -199,51 +212,55 @@ def verify(bond):
for intf in bond['member']:
# check if member interface is "real"
if intf not in interfaces():
- raise ConfigError('interface {} does not exist!'.format(intf))
+ raise ConfigError(f'Interface {intf} does not exist!')
# a bonding member interface is only allowed to be assigned to one bond!
all_bonds = conf.list_nodes('interfaces bonding')
# We do not need to check our own bond
all_bonds.remove(bond['intf'])
for tmp in all_bonds:
- if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which already ' \
- 'belongs to {}'.format(intf, tmp))
+ if conf.exists('interfaces bonding {tmp} member interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bond "{tmp}"!'))
# can not add interfaces with an assigned address to a bond
- if conf.exists('interfaces ethernet ' + intf + ' address'):
- raise ConfigError('can not enslave interface {} which has an address ' \
- 'assigned'.format(intf))
-
- # bond members are not allowed to be bridge members, too
- for tmp in conf.list_nodes('interfaces bridge'):
- if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'bridge {}'.format(intf, tmp))
-
- # bond members are not allowed to be vrrp members, too
+ if has_address_configured(conf, intf):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it has an address assigned!'))
+
+ # bond members are not allowed to be bridge members
+ tmp = is_member(conf, intf, 'bridge')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bridge "{tmp}"!'))
+
+ # bond members are not allowed to be vrrp members
for tmp in conf.list_nodes('high-availability vrrp group'):
- if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'VRRP group {}'.format(intf, tmp))
+ if conf.exists('high-availability vrrp group {tmp} interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of VRRP group "{tmp}"!'))
# bond members are not allowed to be underlaying psuedo-ethernet devices
for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
- if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'pseudo-ethernet {}'.format(intf, tmp))
+ if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of pseudo-ethernet "{tmp}"!'))
# bond members are not allowed to be underlaying vxlan devices
for tmp in conf.list_nodes('interfaces vxlan'):
- if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'vxlan {}'.format(intf, tmp))
-
+ if conf.exists('interfaces vxlan {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of VXLAN "{tmp}"!'))
if bond['primary']:
if bond['primary'] not in bond['member']:
- raise ConfigError('primary interface must be a member interface of {}' \
- .format(bond['intf']))
+ raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
@@ -256,7 +273,6 @@ def verify(bond):
return None
-
def generate(bond):
return None
@@ -365,12 +381,9 @@ def apply(bond):
# Add (enslave) interfaces to bond
for intf in bond['member']:
- # flushes only children of Interfaces class (e.g. vlan are not)
- if intf in Section.interfaces():
- klass = Section.klass(intf, vlan=False)
- klass(intf, create=False).flush_addrs()
- # flushes also vlan interfaces
- call(f'ip addr flush dev "{intf}"')
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
@@ -389,37 +402,17 @@ def apply(bond):
for addr in bond['address']:
b.add_addr(addr)
- # assign/remove VRF
- b.set_vrf(bond['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s_remove']:
- b.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s']:
- s_vlan = b.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 vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- 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 bond['vif_remove']:
- b.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in bond['vif']:
- vlan = b.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not bond['is_bridge_member']:
+ b.set_vrf(bond['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if bond['is_bridge_member']:
+ b.add_to_bridge(bond['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(b, bond)
return None
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 9d638653c..c43fae78b 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -23,8 +23,9 @@ from netifaces import interfaces
from vyos.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
+from vyos.validate import is_member, has_address_configured
from vyos.config import Config
-from vyos.util import cmd
+from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError
default_config_data = {
@@ -202,22 +203,12 @@ def get_config():
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
- # 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
- iface = {
- 'name': intf,
- 'cost': 100,
- 'priority': 32
- }
-
- if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
-
- if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
-
- bridge['member'].append(iface)
+ # defaults are stored in util.py (they can't be here as all interface
+ # scripts use the function)
+ memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
+ if memberconf:
+ memberconf['name'] = intf
+ bridge['member'].append(memberconf)
# Determine bridge member interface (currently effective) - to determine which
# interfaces is no longer assigend to the bridge and thus can be removed
@@ -248,30 +239,40 @@ def verify(bridge):
raise ConfigError(f'VRF "{vrf_name}" does not exist')
conf = Config()
- for br in conf.list_nodes('interfaces bridge'):
- # it makes no sense to verify ourself in this case
- if br == bridge['intf']:
- continue
-
- for intf in bridge['member']:
- tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
- if intf['name'] in tmp:
- raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
-
- # the interface must exist prior adding it to a bridge
for intf in bridge['member']:
+ # the interface must exist prior adding it to a bridge
if intf['name'] not in interfaces():
- raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ raise ConfigError((
+ f'Cannot add nonexistent interface "{intf["name"]}" '
+ f'to bridge "{bridge["intf"]}"'))
if intf['name'] == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
- # bridge members are not allowed to be bond members, too
- for intf in bridge['member']:
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
- raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
+ # bridge members aren't allowed to be members of another bridge
+ for br in conf.list_nodes('interfaces bridge'):
+ # it makes no sense to verify ourself in this case
+ if br == bridge['intf']:
+ continue
+
+ tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
+ if intf['name'] in tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))
+
+ # bridge members are not allowed to be bond members
+ tmp = is_member(conf, intf['name'], 'bonding')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))
+
+ # bridge members must not have an assigned address
+ if has_address_configured(conf, intf['name']):
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it has an address assigned!'))
return None
@@ -347,12 +348,8 @@ def apply(bridge):
# add interfaces to bridge
for member in bridge['member']:
- # flushes address of only children of Interfaces class
- # (e.g. vlan are not)
- if member['name'] in Section.interfaces():
- klass = Section.klass(member['name'], vlan=False)
- klass(member['name'], create=False).flush_addrs()
- # flushes all interfaces
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
cmd(f'ip addr flush dev "{member["name"]}"')
br.add_port(member['name'])
@@ -382,9 +379,9 @@ def apply(bridge):
for member in bridge['member']:
i = STPBridgeIf(member['name'])
# configure ARP cache timeout
- i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
+ i.set_arp_cache_tmo(member['arp_cache_tmo'])
# ignore link state changes
- i.set_link_detect(bridge['disable_link_detect'])
+ i.set_link_detect(member['disable_link_detect'])
# set bridge port path cost
i.set_path_cost(member['cost'])
# set bridge port path priority
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 23eaa4ecb..4a77b0c1a 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -23,7 +23,7 @@ from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -47,11 +47,12 @@ def get_config():
dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if we are a member of any bridge
+ dummy['is_bridge_member'] = is_member(conf, dummy['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
dummy['deleted'] = True
- # check if interface is member if a bridge
- dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf'])
return dummy
# set new configuration level
@@ -84,15 +85,26 @@ def get_config():
def verify(dummy):
if dummy['deleted']:
if dummy['is_bridge_member']:
- interface = dummy['intf']
- bridge = dummy['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{dummy["is_bridge_member"]}"!'))
return None
- vrf_name = dummy['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if dummy['vrf']:
+ if dummy['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{dummy["vrf"]}" does not exist')
+
+ if dummy['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be member of VRF '
+ f'"{dummy["vrf"]}" and bridge "{dummy["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ if dummy['is_bridge_member'] and dummy['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{dummy["intf"]}" '
+ f'as it is a member of bridge "{dummy["is_bridge_member"]}"!'))
return None
@@ -117,8 +129,10 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
- # assign/remove VRF
- d.set_vrf(dummy['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not dummy['is_bridge_member']:
+ d.set_vrf(dummy['vrf'])
# disable interface on demand
if dummy['disable']:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 3ddd394d7..955022042 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -20,9 +20,10 @@ from sys import exit
from copy import deepcopy
from netifaces import interfaces
-from vyos.ifconfig import EthernetIf, Section
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.ifconfig import EthernetIf
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.configdict import list_diff, intf_to_dict, add_to_dict
+from vyos.validate import is_member
from vyos.config import Config
from vyos import ConfigError
@@ -53,6 +54,8 @@ default_config_data = {
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
+ 'is_bond_member': False,
'intf': '',
'mac': '',
'mtu': 1500,
@@ -62,9 +65,9 @@ default_config_data = {
'offload_tso': 'off',
'offload_ufo': 'off',
'speed': 'auto',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
@@ -92,7 +95,6 @@ def get_config():
conf.set_level(cfg_base)
eth, disabled = intf_to_dict(conf, default_config_data)
- eth['intf'] = ifname
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
@@ -114,6 +116,9 @@ def get_config():
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
+ # check if we are a member of any bond
+ eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding')
+
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
eth['offload_gro'] = conf.return_value('offload-options generic-receive')
@@ -138,6 +143,11 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
+ # remove default IPv6 link-local address if member of a bond
+ if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']:
+ eth['ipv6_eui64_prefix'].remove('fe80::/64')
+ eth['ipv6_eui64_prefix_remove'].append('fe80::/64')
+
add_to_dict(conf, disabled, eth, 'vif', 'vif')
add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s')
@@ -162,18 +172,24 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = eth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member']
- conf = Config()
- # some options can not be changed when interface is enslaved to a bond
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
- if eth['intf'] in bond_member:
- if eth['address']:
- raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ if ( memberof
+ and ( eth['address']
+ or eth['ipv6_eui64_prefix']
+ or eth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{eth["intf"]}" '
+ f'as it is a member of "{memberof}"!'))
+
+ if eth['vrf']:
+ if eth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{eth["vrf"]}" does not exist')
+
+ if memberof:
+ raise ConfigError((
+ f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" '
+ f'and "{memberof}" at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(eth)
@@ -281,47 +297,17 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
- # assign/remove VRF
- e.set_vrf(eth['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s_remove']:
- e.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s']:
- s_vlan = e.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 vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- 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 eth['vif_remove']:
- e.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in eth['vif']:
- # 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
- e.del_vlan(vif['id'])
- except:
- pass
-
- vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge or bond,
+ # otherwise 'nomaster' removes it from it)
+ if not ( eth['is_bridge_member'] or eth['is_bond_member'] ):
+ e.set_vrf(eth['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if eth['is_bridge_member']:
+ e.add_to_bridge(eth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(e, eth)
return None
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 708a64474..e4109a221 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import GeneveIf
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -49,11 +49,12 @@ def get_config():
geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
geneve['deleted'] = True
- # check if interface is member if a bridge
- geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf'])
return geneve
# set new configuration level
@@ -97,12 +98,17 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
if geneve['is_bridge_member']:
- interface = geneve['intf']
- bridge = geneve['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{geneve["intf"]}" as it is a '
+ f'member of bridge "{geneve["is_bridge_member"]}"!'))
return None
+ if geneve['is_bridge_member'] and geneve['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{geneve["intf"]}" '
+ f'as it is a member of bridge "{geneve["is_bridge_member"]}"!'))
+
if not geneve['remote']:
raise ConfigError('GENEVE remote must be configured')
@@ -158,6 +164,10 @@ def apply(geneve):
if not geneve['disable']:
g.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if geneve['is_bridge_member']:
+ g.add_to_bridge(geneve['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 33cf62f70..26bb537e5 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
-from vyos.validate import is_bridge_member, is_addr_assigned
+from vyos.validate import is_member, is_addr_assigned
default_config_data = {
'address': [],
@@ -66,12 +66,13 @@ def get_config():
l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member of a bridge
+ l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
interface = l2tpv3['intf']
- # check if interface is member if a bridge
- l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
# to delete the l2tpv3 interface we need the current tunnel_id and session_id
if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
@@ -118,7 +119,8 @@ def get_config():
l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Remove the default link-local address if set.
- if not conf.exists('ipv6 address no-default-link-local'):
+ if not ( conf.exists('ipv6 address no-default-link-local') or
+ l2tpv3['is_bridge_member'] ):
# add the link-local by default to make IPv6 work
l2tpv3['ipv6_eui64_prefix'].append('fe80::/64')
@@ -166,9 +168,9 @@ def verify(l2tpv3):
if l2tpv3['deleted']:
if l2tpv3['is_bridge_member']:
- interface = l2tpv3['intf']
- bridge = l2tpv3['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{l2tpv3["is_bridge_member"]}"!'))
return None
@@ -193,6 +195,14 @@ def verify(l2tpv3):
if not l2tpv3['peer_session_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
+ if ( l2tpv3['is_bridge_member']
+ and ( l2tpv3['address']
+ or l2tpv3['ipv6_eui64_prefix']
+ or l2tpv3['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{l2tpv3["intf"]}" '
+ f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!'))
+
return None
@@ -254,6 +264,10 @@ def apply(l2tpv3):
if not l2tpv3['disable']:
l.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if l2tpv3['is_bridge_member']:
+ l.add_to_bridge(l2tpv3['is_bridge_member'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 029bc1d69..c0678764f 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -29,7 +29,7 @@ from vyos.configdict import list_diff
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.util import call, chown, chmod_600, chmod_755
-from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4
+from vyos.validate import is_addr_assigned, is_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
@@ -41,7 +41,6 @@ default_config_data = {
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
- 'bridge_member': [],
'compress_lzo': False,
'deleted': False,
'description': '',
@@ -199,21 +198,16 @@ def get_config():
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
- # check if interface is member if a bridge
- openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf'])
return openvpn
- # Check if we belong to any bridge interface
- for bridge in conf.list_nodes('interfaces bridge'):
- for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
- if intf == openvpn['intf']:
- openvpn['bridge_member'].append(intf)
-
# bridged server should not have a pool by default (but can be specified manually)
- if openvpn['bridge_member']:
+ if openvpn['is_bridge_member']:
openvpn['server_pool'] = False
openvpn['server_ipv6_pool'] = False
@@ -597,7 +591,7 @@ def get_config():
default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
if default_server:
# server-bridge doesn't require a pool so don't set defaults for it
- if openvpn['server_pool'] and not openvpn['bridge_member']:
+ if openvpn['server_pool'] and not openvpn['is_bridge_member']:
if not openvpn['server_pool_start']:
openvpn['server_pool_start'] = default_server['pool_start']
@@ -635,22 +629,15 @@ def get_config():
def verify(openvpn):
if openvpn['deleted']:
if openvpn['is_bridge_member']:
- interface = openvpn['intf']
- bridge = openvpn['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
-
- return None
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
+ return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
- # Checks which need to be performed on interface rmeoval
- if openvpn['deleted']:
- # OpenVPN interface can not be deleted if it's still member of a bridge
- if openvpn['bridge_member']:
- raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge))
-
# Check if we have disabled ncp and at the same time specified ncp-ciphers
if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
@@ -680,9 +667,9 @@ def verify(openvpn):
if openvpn['ncp_ciphers']:
raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
- if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']:
+ if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
- raise ConfigError('Must specify "local-address" or "bridge member interface"')
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
@@ -761,8 +748,8 @@ def verify(openvpn):
raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
else:
- if not openvpn['bridge_member']:
- raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode')
+ if not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
if openvpn['server_pool']:
if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index f0f893b44..4063b85b0 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -21,10 +21,9 @@ from sys import exit
from netifaces import interfaces
from vyos.config import Config
-from vyos.configdict import list_diff, vlan_to_dict, intf_to_dict, add_to_dict
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.ifconfig import MACVLANIf, Section
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.validate import is_bridge_member
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos import ConfigError
default_config_data = {
@@ -57,9 +56,9 @@ default_config_data = {
'source_interface_changed': False,
'mac': '',
'mode': 'private',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
@@ -77,15 +76,12 @@ def get_config():
if not conf.exists(cfg_base):
peth = deepcopy(default_config_data)
peth['deleted'] = True
- # check if interface is member if a bridge
- peth['is_bridge_member'] = is_bridge_member(conf, ifname)
return peth
# set new configuration level
conf.set_level(cfg_base)
peth, disabled = intf_to_dict(conf, default_config_data)
- peth['intf'] = ifname
# ARP cache entry timeout in seconds
if conf.exists(['ip', 'arp-cache-timeout']):
@@ -114,21 +110,37 @@ def get_config():
def verify(peth):
if peth['deleted']:
if peth['is_bridge_member']:
- interface = peth['intf']
- bridge = peth['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{peth["intf"]}" as it is a '
+ f'member of bridge "{peth["is_bridge_member"]}"!'))
return None
if not peth['source_interface']:
- raise ConfigError('source-interface must be set for virtual ethernet {}'.format(peth['intf']))
+ raise ConfigError((
+ f'Link device must be set for pseudo-ethernet "{peth["intf"]}"'))
if not peth['source_interface'] in interfaces():
- raise ConfigError('Pseudo-ethernet source-interface does not exist')
+ raise ConfigError((
+ f'Pseudo-ethernet "{peth["intf"]}" link device does not exist')
- vrf_name = peth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( peth['is_bridge_member']
+ and ( peth['address']
+ or peth['ipv6_eui64_prefix']
+ or peth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{peth["intf"]}" '
+ f'as it is a member of bridge "{peth["is_bridge_member"]}"!'))
+
+ if peth['vrf']:
+ if peth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{peth["vrf"]}" does not exist')
+
+ if peth['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{peth["intf"]}" cannot be member of VRF '
+ f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(peth)
@@ -203,8 +215,10 @@ def apply(peth):
# IPv6 Duplicate Address Detection (DAD) tries
p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
- # assign/remove VRF
- p.set_vrf(peth['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not peth['is_bridge_member']:
+ p.set_vrf(peth['vrf'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in peth['ipv6_eui64_prefix_remove']:
@@ -235,34 +249,12 @@ def apply(peth):
for addr in peth['address']:
p.add_addr(addr)
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s_remove']:
- p.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s']:
- s_vlan = p.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 vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- 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 peth['vif_remove']:
- p.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in peth['vif']:
- vlan = p.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # re-add ourselves to any bridge we might have fallen out of
+ if peth['is_bridge_member']:
+ p.add_to_bridge(peth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(b, bond)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index fc084814a..f4cd53981 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
from vyos.ifconfig.afi import IP4, IP6
from vyos.configdict import list_diff
-from vyos.validate import is_ipv4, is_ipv6, is_bridge_member
+from vyos.validate import is_ipv4, is_ipv6, is_member
from vyos import ConfigError
from vyos.dicts import FixedDict
@@ -410,7 +410,7 @@ def get_config():
options['tunnel'] = {}
# check for bridges
- options['bridge'] = is_bridge_member(conf, ifname)
+ options['bridge'] = is_member(conf, ifname, 'bridge')
options['interfaces'] = interfaces()
for name in ct:
@@ -436,11 +436,14 @@ def verify(conf):
if changes['section'] == 'delete':
if ifname in options['nhrp']:
- raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+ raise ConfigError((
+ f'Cannot delete interface tunnel {iftype} {ifname}, '
+ 'it is used by NHRP'))
- bridge = options['bridge']
- if bridge:
- raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!')
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
# done, bail out early
return None
@@ -525,10 +528,23 @@ def verify(conf):
print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
# vrf check
-
- vrf = options['vrf']
- if vrf and vrf not in options['interfaces']:
- raise ConfigError(f'VRF "{vrf}" does not exist')
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
# source-interface check
@@ -620,12 +636,17 @@ def apply(conf):
# set other interface properties
for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
- 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
if not options[option]:
# should never happen but better safe
continue
tunnel.set_interface(option, options[option])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
# Configure interface address(es)
for addr in options['addresses-del']:
tunnel.del_addr(addr)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 74eae4281..fabfaa9df 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -62,11 +62,12 @@ def get_config():
vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
- # check if interface is member if a bridge
- vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf'])
return vxlan
# set new configuration level
@@ -121,7 +122,8 @@ def get_config():
vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Remove the default link-local address if set.
- if not conf.exists('ipv6 address no-default-link-local'):
+ if not ( conf.exists('ipv6 address no-default-link-local')
+ or vxlan['is_bridge_member'] ):
# add the link-local by default to make IPv6 work
vxlan['ipv6_eui64_prefix'].append('fe80::/64')
@@ -163,9 +165,9 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
if vxlan['is_bridge_member']:
- interface = vxlan['intf']
- bridge = vxlan['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{vxlan["intf"]}" as it is a '
+ f'member of bridge "{vxlan["is_bridge_member"]}"!')
return None
@@ -193,6 +195,14 @@ def verify(vxlan):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
+ if ( vxlan['is_bridge_member']
+ and ( vxlan['address']
+ or vxlan['ipv6_eui64_prefix']
+ or vxlan['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{vxlan["intf"]}" '
+ f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!'))
+
return None
@@ -264,6 +274,10 @@ def apply(vxlan):
if not vxlan['disable']:
v.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if vxlan['is_bridge_member']:
+ v.add_to_bridge(vxlan['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 01f84260d..820b0a724 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
from vyos.util import chown, chmod_750, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
@@ -38,8 +38,8 @@ default_config_data = {
'listen_port': '',
'deleted': False,
'disable': False,
- 'is_bridge_member': False,
'fwmark': 0,
+ 'is_bridge_member': False,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
@@ -78,11 +78,12 @@ def get_config():
wg = deepcopy(default_config_data)
wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists(base + [wg['intf']]):
wg['deleted'] = True
- # check if interface is member if a bridge
- wg['is_bridge_member'] = is_bridge_member(conf, wg['intf'])
return wg
conf.set_level(base + [wg['intf']])
@@ -189,44 +190,52 @@ def get_config():
def verify(wg):
- interface = wg['intf']
-
if wg['deleted']:
if wg['is_bridge_member']:
- interface = wg['intf']
- bridge = wg['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wg["intf"]}" as it is a member '
+ f'of bridge "{wg["is_bridge_member"]}"!'))
return None
- vrf_name = wg['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if wg['is_bridge_member'] and wg['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wg["intf"]}" '
+ f'as it is a member of bridge "{wg["is_bridge_member"]}"!'))
+
+ if wg['vrf']:
+ if wg['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wg["vrf"]}" does not exist')
+
+ if wg['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wg["intf"]}" cannot be member of VRF '
+ f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} '
+ f'at the same time!'))
if not os.path.exists(wg['pk']):
raise ConfigError('No keys found, generate them by executing:\n' \
'"run generate wireguard [keypair|named-keypairs]"')
if not wg['address']:
- raise ConfigError(f'IP address required for interface "{interface}"!')
+ raise ConfigError(f'IP address required for interface "{wg["intf"]}"!')
if not wg['peer']:
- raise ConfigError(f'Peer required for interface "{interface}"!')
+ raise ConfigError(f'Peer required for interface "{wg["intf"]}"!')
# run checks on individual configured WireGuard peer
for peer in wg['peer']:
- peer_name = peer['name']
if not peer['allowed-ips']:
- raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
+ raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!')
if not peer['pubkey']:
- raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+ raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!')
if peer['address'] and not peer['port']:
- raise ConfigError(f'Peer "{peer_name}" port must be defined if address is defined!')
+ raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!')
if not peer['address'] and peer['port']:
- raise ConfigError(f'Peer "{peer_name}" address must be defined if port is defined!')
+ raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!')
def apply(wg):
@@ -252,8 +261,10 @@ def apply(wg):
# update interface description used e.g. within SNMP
w.set_alias(wg['description'])
- # assign/remove VRF
- w.set_vrf(wg['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wg['is_bridge_member']:
+ w.set_vrf(wg['vrf'])
# remove peers
for pub_key in wg['peer_remove']:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 148a7f6e0..4d61dc303 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -29,7 +29,7 @@ from vyos.ifconfig import WiFiIf, Section
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.template import render
from vyos.util import chown, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -134,12 +134,13 @@ def get_config():
wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge')
+
# check if wireless interface has been removed
cfg_base = 'interfaces wireless ' + wifi['intf']
if not conf.exists(cfg_base):
wifi['deleted'] = True
- # check if interface is member if a bridge
- wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
# we can not bail out early as wireless interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
@@ -378,8 +379,8 @@ def get_config():
eff_addr = conf.return_effective_values('ipv6 address eui64')
wifi['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, wifi['ipv6_eui64_prefix'])
- # Remove the default link-local address if set.
- if conf.exists('ipv6 address no-default-link-local'):
+ # Remove the default link-local address if set or if member of a bridge
+ if conf.exists('ipv6 address no-default-link-local') or wifi['is_bridge_member']:
wifi['ipv6_eui64_prefix_remove'].append('fe80::/64')
else:
# add the link-local by default to make IPv6 work
@@ -551,9 +552,9 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
if wifi['is_bridge_member']:
- interface = wifi['intf']
- bridge = wifi['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wifi["intf"]}" as it is a '
+ f'member of bridge "{wifi["is_bridge_member"]}"!'))
return None
@@ -598,9 +599,23 @@ def verify(wifi):
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
- vrf_name = wifi['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( wifi['is_bridge_member']
+ and ( wifi['address']
+ or wifi['ipv6_eui64_prefix']
+ or wifi['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{wifi["intf"]}" '
+ f'as it is a member of bridge "{wifi["is_bridge_member"]}"!'))
+
+ if wifi['vrf']:
+ if wifi['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist')
+
+ if wifi['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wifi["intf"]}" cannot be member of VRF '
+ f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
@@ -691,8 +706,10 @@ def apply(wifi):
# Finally create the new interface
w = WiFiIf(interface, **conf)
- # assign/remove VRF
- w.set_vrf(wifi['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wifi['is_bridge_member']:
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index a3a2a2648..975e21d9f 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -21,9 +21,10 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
+from vyos.ifconfig import BridgeIf, Section
from vyos.template import render
from vyos.util import chown, chmod_755, cmd, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -64,11 +65,12 @@ def get_config():
wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_member(conf, wwan['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
wwan['deleted'] = True
- # check if interface is member if a bridge
- wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf'])
return wwan
# set new configuration level
@@ -119,9 +121,9 @@ def get_config():
def verify(wwan):
if wwan['deleted']:
if wwan['is_bridge_member']:
- interface = wwan['intf']
- bridge = wwan['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wwan["intf"]}" as it is a '
+ f'member of bridge "{wwan["is_bridge_member"]}"!'))
return None
@@ -133,9 +135,20 @@ def verify(wwan):
if not os.path.exists(f"/dev/{wwan['device']}"):
raise ConfigError(f"Device {wwan['device']} does not exist")
- vrf_name = wwan['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF {vrf_name} does not exist')
+ if wwan['is_bridge_member'] and wwan['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wwan["intf"]}" '
+ f'as it is a member of bridge "{wwan["is_bridge_member"]}"!'))
+
+ if wwan['vrf']:
+ if wwan['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wwan["vrf"]}" does not exist')
+
+ if wwan['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wwan["intf"]}" cannot be member of VRF '
+ f'"{wwan["vrf"]}" and bridge {wwan["is_bridge_member"]} '
+ f'at the same time!'))
return None
@@ -193,6 +206,12 @@ def apply(wwan):
# make logfile owned by root / vyattacfg
chown(wwan['logfile'], 'root', 'vyattacfg')
+ # re-add ourselves to any bridge we might have fallen out of
+ # FIXME: wwan isn't under vyos.ifconfig so we can't call
+ # Interfaces.add_to_bridge() so STP settings won't get applied
+ if wwan['is_bridge_member'] in Section.interfaces('bridge'):
+ BridgeIf(wwan['is_bridge_member'], create=False).add_port(wwan['intf'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index b53692d37..84443ade3 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -112,28 +112,30 @@ def get_config():
'name': interface,
'mac': []
}
- for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']):
- mac = {
+ for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']):
+ client = {
'address': mac,
'rate_download': '',
'rate_upload': '',
'vlan_id': ''
}
- conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client])
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac])
if conf.exists(['rate-limit', 'download']):
- mac['rate_download'] = conf.return_value(['rate-limit', 'download'])
+ client['rate_download'] = conf.return_value(['rate-limit', 'download'])
if conf.exists(['rate-limit', 'upload']):
- mac['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+ client['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
if conf.exists(['vlan-id']):
- mac['vlan'] = conf.return_value(['vlan-id'])
+ client['vlan'] = conf.return_value(['vlan-id'])
- tmp['mac'].append(mac)
+ tmp['mac'].append(client)
ipoe['auth_interfaces'].append(tmp)
+ conf.set_level(base_path)
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index d250cd3b0..7c3e3f515 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -22,10 +22,10 @@ from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call, run, get_half_cpus
from vyos.template import render
-
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -35,7 +35,12 @@ default_config_data = {
'auth_mode' : 'local',
'auth_proto' : ['auth_mschap_v2'],
'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
'radius_server' : [],
'radius_acct_tmo' : '3',
'radius_max_try' : '3',
@@ -49,8 +54,6 @@ default_config_data = {
'ssl_ca' : '',
'ssl_cert' : '',
'ssl_key' : '',
- 'client_ip_pool' : [],
- 'dnsv4' : [],
'mtu' : '',
'ppp_mppe' : 'prefer',
'ppp_echo_failure' : '',
@@ -210,7 +213,7 @@ def get_config():
#
- # read in client ip pool settings
+ # read in client IPv4 pool
conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
if conf.exists(['subnet']):
sstp['client_ip_pool'] = conf.return_values(['subnet'])
@@ -219,10 +222,41 @@ def get_config():
sstp['client_gateway'] = conf.return_value(['gateway-address'])
#
+ # read in client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
# read in network settings
conf.set_level(base_path + ['network-settings'])
if conf.exists(['name-server']):
- sstp['dnsv4'] = conf.return_values(['name-server'])
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(name_server)
if conf.exists(['mtu']):
sstp['mtu'] = conf.return_value(['mtu'])
@@ -275,6 +309,14 @@ def verify(sstp):
if len(sstp['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
raise ConfigError('One or more SSL certificates missing')
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index a655762e9..647cbc8c1 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -25,6 +25,7 @@ import traceback
import re
import logging
import zmq
+import collections
import jinja2
@@ -79,8 +80,18 @@ resolv_tmpl_source = """
### Autogenerated by VyOS ###
### Do not edit, your changes will get overwritten ###
+# name server from static configuration
{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] == "static" %}
nameserver {{ns}}
+{%- endif %}
+{% endfor -%}
+
+{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] != "static" %}
+# name server from {{name_servers[ns]['tag']}}
+nameserver {{ns}}
+{%- endif %}
{% endfor -%}
{%- if domain_name %}
@@ -110,7 +121,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source)
# and re-created without having to track what needs
# to be changed
STATE = {
- "name_servers": {},
+ "name_servers": collections.OrderedDict({}),
"hosts": {},
"host_name": "vyos",
"domain_name": "",