summaryrefslogtreecommitdiff
path: root/python/vyos/ifconfig/interface.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos/ifconfig/interface.py')
-rw-r--r--python/vyos/ifconfig/interface.py296
1 files changed, 275 insertions, 21 deletions
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 1819ffc82..36f258301 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -16,7 +16,10 @@
import os
import re
import json
+import jmespath
+
from copy import deepcopy
+from glob import glob
from ipaddress import IPv4Network
from ipaddress import IPv6Address
@@ -28,6 +31,8 @@ from netifaces import AF_INET6
from vyos import ConfigError
from vyos.configdict import list_diff
+from vyos.configdict import dict_merge
+from vyos.template import render
from vyos.util import mac2eui64
from vyos.validate import is_ipv4
from vyos.validate import is_ipv6
@@ -40,11 +45,17 @@ from vyos.validate import assert_positive
from vyos.validate import assert_range
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
+def get_ethertype(ethertype_val):
+ if ethertype_val == '0x88A8':
+ return '802.1ad'
+ elif ethertype_val == '0x8100':
+ return '802.1q'
+ else:
+ raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
class Interface(Control):
# This is the class which will be used to create
@@ -72,8 +83,12 @@ class Interface(Control):
_command_get = {
'admin_state': {
'shellcmd': 'ip -json link show dev {ifname}',
- 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down',
- }
+ 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down',
+ },
+ 'vlan_protocol': {
+ 'shellcmd': 'ip -json -details link show dev {ifname}',
+ 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)),
+ },
}
_command_set = {
@@ -197,11 +212,11 @@ class Interface(Control):
# make sure the ifname is the first argument and not from the dict
self.config['ifname'] = ifname
+ self._admin_state_down_cnt = 0
# we must have updated config before initialising the Interface
super().__init__(**kargs)
self.ifname = ifname
- self.dhcp = DHCP(ifname)
if not self.exists(ifname):
# Any instance of Interface, such as Interface('eth0')
@@ -323,6 +338,10 @@ class Interface(Control):
self.set_interface('mac', mac)
+ # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion
+ if prev_state == 'up':
+ self.set_admin_state('up')
+
def set_vrf(self, vrf=''):
"""
Add/Remove interface from given VRF instance.
@@ -539,6 +558,17 @@ class Interface(Control):
"""
self.set_interface('alias', ifalias)
+ def get_vlan_protocol(self):
+ """
+ Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0.10').get_vlan_protocol()
+ '802.1Q'
+ """
+ return self.get_interface('vlan_protocol')
+
def get_admin_state(self):
"""
Get interface administrative state. Function will return 'up' or 'down'
@@ -560,7 +590,24 @@ class Interface(Control):
>>> Interface('eth0').get_admin_state()
'down'
"""
- return self.set_interface('admin_state', state)
+ # A VLAN interface can only be placed in admin up state when
+ # the lower interface is up, too
+ if self.get_vlan_protocol():
+ lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0]
+ with open(lower_interface, 'r') as f:
+ flags = f.read()
+ # If parent is not up - bail out as we can not bring up the VLAN.
+ # Flags are defined in kernel source include/uapi/linux/if.h
+ if not int(flags, 16) & 1:
+ return None
+
+ if state == 'up':
+ self._admin_state_down_cnt -= 1
+ if self._admin_state_down_cnt < 1:
+ return self.set_interface('admin_state', state)
+ else:
+ self._admin_state_down_cnt += 1
+ return self.set_interface('admin_state', state)
def set_proxy_arp(self, enable):
"""
@@ -663,21 +710,24 @@ class Interface(Control):
if addr in self._addr:
return False
+ addr_is_v4 = is_ipv4(addr)
+
# 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) ) ):
+ ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ):
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()
+ self.set_dhcp(True)
elif addr == 'dhcpv6':
- self.dhcp.v6.set()
+ self.set_dhcpv6(True)
elif not is_intf_addr_assigned(self.ifname, addr):
- self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"')
+ self._cmd(f'ip addr add "{addr}" '
+ f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
else:
return False
@@ -713,9 +763,9 @@ class Interface(Control):
# remove from interface
if addr == 'dhcp':
- self.dhcp.v4.delete()
+ self.set_dhcp(False)
elif addr == 'dhcpv6':
- self.dhcp.v6.delete()
+ self.set_dhcpv6(False)
elif is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
else:
@@ -734,8 +784,8 @@ class Interface(Control):
Will raise an exception on error.
"""
# stop DHCP(v6) if running
- self.dhcp.v4.delete()
- self.dhcp.v6.delete()
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
# flush all addresses
self._cmd(f'ip addr flush dev "{self.ifname}"')
@@ -759,21 +809,95 @@ class Interface(Control):
return True
+ def set_dhcp(self, enable):
+ """
+ Enable/Disable DHCP client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_base = r'/var/lib/dhcp/dhclient'
+ config_file = f'{config_base}_{ifname}.conf'
+ options_file = f'{config_base}_{ifname}.options'
+ pid_file = f'{config_base}_{ifname}.pid'
+ lease_file = f'{config_base}_{ifname}.leases'
+
+ if enable and 'disable' not in self._config:
+ if jmespath.search('dhcp_options.host_name', self._config) == None:
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ hostname = 'vyos'
+ with open('/etc/hostname', 'r') as f:
+ hostname = f.read().rstrip('\n')
+ tmp = {'dhcp_options' : { 'host_name' : hostname}}
+ self._config = dict_merge(tmp, self._config)
+
+ render(options_file, 'dhcp-client/daemon-options.tmpl',
+ self._config, trim_blocks=True)
+ render(config_file, 'dhcp-client/ipv4.tmpl',
+ self._config, trim_blocks=True)
+
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
+ return self._cmd(f'systemctl restart dhclient@{ifname}.service')
+ else:
+ self._cmd(f'systemctl stop dhclient@{ifname}.service')
+
+ # cleanup old config files
+ for file in [config_file, options_file, pid_file, lease_file]:
+ if os.path.isfile(file):
+ os.remove(file)
+
+
+ def set_dhcpv6(self, enable):
+ """
+ Enable/Disable DHCPv6 client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+
+ if enable and 'disable' not in self._config:
+ render(config_file, 'dhcp-client/ipv6.tmpl',
+ self._config, trim_blocks=True)
+
+ # We must ignore any return codes. This is required to enable DHCPv6-PD
+ # for interfaces which are yet not up and running.
+ return self._popen(f'systemctl restart dhcp6c@{ifname}.service')
+ else:
+ self._popen(f'systemctl stop dhcp6c@{ifname}.service')
+
+ if os.path.isfile(config_file):
+ os.remove(config_file)
+
+
def update(self, config):
""" General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
interface setup code and provide a single point of entry when workin
on any interface. """
+ # Cache the configuration - it will be reused inside e.g. DHCP handler
+ # XXX: maybe pass the option via __init__ in the future and rename this
+ # method to apply()?
+ self._config = config
+
# Update interface description
- self.set_alias(config.get('description', None))
+ self.set_alias(config.get('description', ''))
+
+ # Ignore link state changes
+ value = '2' if 'disable_link_detect' in config else '1'
+ self.set_link_detect(value)
# Configure assigned interface IP addresses. No longer
# configured addresses will be removed first
new_addr = config.get('address', [])
- # XXX workaround for T2636, convert IP address string to a list
- # with one element
+ # XXX: T2636 workaround: convert string to a list with one element
if isinstance(new_addr, str):
new_addr = [new_addr]
@@ -789,10 +913,140 @@ class Interface(Control):
# There are some items in the configuration which can only be applied
# if this instance is not bound to a bridge. This should be checked
# by the caller but better save then sorry!
- if not config.get('is_bridge_member', False):
- # Bind interface instance into VRF
+ if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config):
+ # Bind interface to given VRF or unbind it if vrf node is not set.
+ # unbinding will call 'ip link set dev eth0 nomaster' which will
+ # also drop the interface out of a bridge or bond - thus this is
+ # checked before
self.set_vrf(config.get('vrf', ''))
- # Interface administrative state
- state = 'down' if 'disable' in config.keys() else 'up'
- self.set_admin_state(state)
+ # Configure ARP cache timeout in milliseconds - has default value
+ tmp = jmespath.search('ip.arp_cache_timeout', config)
+ value = tmp if (tmp != None) else '30'
+ self.set_arp_cache_tmo(value)
+
+ # Configure ARP filter configuration
+ tmp = jmespath.search('ip.disable_arp_filter', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_arp_filter(value)
+
+ # Configure ARP accept
+ tmp = jmespath.search('ip.enable_arp_accept', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_accept(value)
+
+ # Configure ARP announce
+ tmp = jmespath.search('ip.enable_arp_announce', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_announce(value)
+
+ # Configure ARP ignore
+ tmp = jmespath.search('ip.enable_arp_ignore', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_ignore(value)
+
+ # Enable proxy-arp on this interface
+ tmp = jmespath.search('ip.enable_proxy_arp', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp(value)
+
+ # Enable private VLAN proxy ARP on this interface
+ tmp = jmespath.search('ip.proxy_arp_pvlan', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp_pvlan(value)
+
+ # IPv6 forwarding
+ tmp = jmespath.search('ipv6.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_ipv6_forwarding(value)
+
+ # IPv6 router advertisements
+ tmp = jmespath.search('ipv6.address.autoconf', config)
+ value = '2' if (tmp != None) else '1'
+ if 'dhcpv6' in new_addr:
+ value = '2'
+ self.set_ipv6_accept_ra(value)
+
+ # IPv6 address autoconfiguration
+ tmp = jmespath.search('ipv6.address.autoconf', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_ipv6_autoconf(value)
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ tmp = jmespath.search('ipv6.dup_addr_detect_transmits', config)
+ value = tmp if (tmp != None) else '1'
+ self.set_ipv6_dad_messages(value)
+
+ # MTU - Maximum Transfer Unit
+ if 'mtu' in config:
+ self.set_mtu(config.get('mtu'))
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ tmp = jmespath.search('ipv6.address.eui64_old', config)
+ if tmp:
+ for addr in tmp:
+ self.del_ipv6_eui64_address(addr)
+
+ # Change interface MAC address - re-set to real hardware address (hw-id)
+ # if custom mac is removed. Skip if bond member.
+ if 'is_bond_member' not in config:
+ mac = config.get('hw_id')
+ if 'mac' in config:
+ mac = config.get('mac')
+ if mac:
+ self.set_mac(mac)
+
+ # Manage IPv6 link-local addresses
+ tmp = jmespath.search('ipv6.address.no_default_link_local', config)
+ # we must check explicitly for None type as if the key is set we will
+ # get an empty dict (<class 'dict'>)
+ if tmp is not None:
+ self.del_ipv6_eui64_address('fe80::/64')
+ else:
+ self.add_ipv6_eui64_address('fe80::/64')
+
+ # Add IPv6 EUI-based addresses
+ tmp = jmespath.search('ipv6.address.eui64', config)
+ if tmp:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ for addr in tmp:
+ self.add_ipv6_eui64_address(addr)
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if 'is_bridge_member' in config:
+ bridge = config.get('is_bridge_member')
+ self.add_to_bridge(bridge)
+
+ # remove no longer required 802.1ad (Q-in-Q VLANs)
+ for vif_s_id in config.get('vif_s_remove', {}):
+ self.del_vlan(vif_s_id)
+
+ # create/update 802.1ad (Q-in-Q VLANs)
+ ifname = config['ifname']
+ for vif_s_id, vif_s in config.get('vif_s', {}).items():
+ tmp=get_ethertype(vif_s.get('ethertype', '0x88A8'))
+ s_vlan = self.add_vlan(vif_s_id, ethertype=tmp)
+ vif_s['ifname'] = f'{ifname}.{vif_s_id}'
+ s_vlan.update(vif_s)
+
+ # remove no longer required client VLAN (vif-c)
+ for vif_c_id in vif_s.get('vif_c_remove', {}):
+ s_vlan.del_vlan(vif_c_id)
+
+ # create/update client VLAN (vif-c) interface
+ for vif_c_id, vif_c in vif_s.get('vif_c', {}).items():
+ c_vlan = s_vlan.add_vlan(vif_c_id)
+ vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}'
+ c_vlan.update(vif_c)
+
+ # remove no longer required 802.1q VLAN interfaces
+ for vif_id in config.get('vif_remove', {}):
+ self.del_vlan(vif_id)
+
+ # create/update 802.1q VLAN interfaces
+ for vif_id, vif in config.get('vif', {}).items():
+ vlan = self.add_vlan(vif_id)
+ vif['ifname'] = f'{ifname}.{vif_id}'
+ vlan.update(vif)