From 960e1f6d190210fbf67c83fd9f4f55bc31fbaf6d Mon Sep 17 00:00:00 2001
From: Thomas Mangin <thomas.mangin@exa.net.uk>
Date: Mon, 27 Apr 2020 20:00:38 +0100
Subject: vlan: T2372: de-indent add_vlan

---
 python/vyos/ifconfig/vlan.py | 40 ++++++++++++++++++++--------------------
 1 file changed, 20 insertions(+), 20 deletions(-)

(limited to 'python/vyos')

diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py
index 7b1e00d87..d68e8f6cd 100644
--- a/python/vyos/ifconfig/vlan.py
+++ b/python/vyos/ifconfig/vlan.py
@@ -101,26 +101,26 @@ class VLAN:
         >>> i.add_vlan(10)
         """
         vlan_ifname = self.config['ifname'] + '.' + str(vlan_id)
-        if not os.path.exists(f'/sys/class/net/{vlan_ifname}'):
-            self._vlan_id = int(vlan_id)
-
-            if ethertype:
-                self._ethertype = ethertype
-                ethertype = 'proto {}'.format(ethertype)
-
-            # Optional ingress QOS mapping
-            opt_i = ''
-            if ingress_qos:
-                opt_i = 'ingress-qos-map ' + ingress_qos
-            # Optional egress QOS mapping
-            opt_e = ''
-            if egress_qos:
-                opt_e = 'egress-qos-map ' + egress_qos
-
-            # create interface in the system
-            cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
-                .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
-            self._cmd(cmd)
+        if os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+            return self.__class__(vlan_ifname)
+
+        if ethertype:
+            self._ethertype = ethertype
+            ethertype = 'proto {}'.format(ethertype)
+
+        # Optional ingress QOS mapping
+        opt_i = ''
+        if ingress_qos:
+            opt_i = 'ingress-qos-map ' + ingress_qos
+        # Optional egress QOS mapping
+        opt_e = ''
+        if egress_qos:
+            opt_e = 'egress-qos-map ' + egress_qos
+
+        # create interface in the system
+        cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
+            .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
+        self._cmd(cmd)
 
         # return new object mapping to the newly created interface
         # we can now work on this object for e.g. IP address setting
-- 
cgit v1.2.3


From 3fdf0093ac0b859b5074e2a20c2a1a9276c2754e Mon Sep 17 00:00:00 2001
From: Thomas Mangin <thomas.mangin@exa.net.uk>
Date: Mon, 27 Apr 2020 23:03:43 +0100
Subject: configdict: T2372: correct disable support in vlan_to_dict

implement disable_state which looks if the current node, or some
designated parent node are set are 'disable' and thefore should
be ignored.

break down the function vlan_to_dict in it multiple components

add_to_dict which can parse vif, vif-s, or vif-c and add them
to the configuration dictionary

intf_to_dict which setup a base configuration dictionary from the
interface Config() with addresses, arp, disable, ...
it is used by vlan_to_dict but can and will be used by other interfaces
---
 python/vyos/configdict.py | 377 ++++++++++++++++++++++++++++++++--------------
 1 file changed, 264 insertions(+), 113 deletions(-)

(limited to 'python/vyos')

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 2ce8a795f..dea8a5d01 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -18,7 +18,12 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion.
 
 """
 
+from enum import Enum
+from copy import deepcopy
+
 from vyos import ConfigError
+from vyos.ifconfig import Interface
+
 
 def retrieve_config(path_hash, base_path, config):
     """
@@ -98,192 +103,338 @@ def get_ethertype(ethertype_val):
         raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
 
 
-def vlan_to_dict(conf):
+vlan_default = {
+    'address': [],
+    'address_remove': [],
+    'description': '',
+    'dhcp_client_id': '',
+    'dhcp_hostname': '',
+    'dhcp_vendor_class_id': '',
+    'dhcpv6_prm_only': False,
+    'dhcpv6_temporary': False,
+    'disable': False,
+    'disable_link_detect': 1,
+    'egress_qos': '',
+    'egress_qos_changed': False,
+    'ip_disable_arp_filter': 1,
+    'ip_enable_arp_accept': 0,
+    'ip_enable_arp_announce': 0,
+    'ip_enable_arp_ignore': 0,
+    'ip_proxy_arp': 0,
+    'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': [],
+    'ipv6_eui64_prefix_remove': [],
+    'ipv6_forwarding': 1,
+    'ipv6_dup_addr_detect': 1,
+    'ingress_qos': '',
+    'ingress_qos_changed': False,
+    'mac': '',
+    'mtu': 1500,
+    'vif_c': [],
+    'vif_c_remove': [],
+    'vrf': ''
+}
+
+# see: https://docs.python.org/3/library/enum.html#functional-api
+disable = Enum('disable','none was now both')
+
+def disable_state(conf, check=[3,5,7]):
+    """
+    return if and how a particual section of the configuration is has disable'd
+    using "disable" including if it was disabled by one of its parent.
+
+    check: a list of the level we should check, here 7,5 and 3
+          interfaces ethernet eth1 vif-s 1 vif-c 2 disable
+          interfaces ethernet eth1 vif 1 disable
+          interfaces ethernet eth1 disable
+
+    it returns an enum (none, was, now, both)
+    """
+
+    # save where we are in the config
+    current_level = conf.get_level()
+
+    # logic to figure out if the interface (or one of it parent is disabled)
+    eff_disable = False
+    act_disable = False
+
+    levels = check[:]
+    working_level = current_level[:]
+
+    while levels:
+        position = len(working_level)
+        if not position:
+            break
+        if position not in levels:
+            working_level = working_level[:-1]
+            continue
+
+        levels.remove(position)
+        conf.set_level(working_level)
+        working_level = working_level[:-1]
+
+        eff_disable = eff_disable or conf.exists_effective('disable')
+        act_disable = act_disable or conf.exists('disable')
+
+    conf.set_level(current_level)
+
+    # how the disabling changed
+    if eff_disable and act_disable:
+        return disable.both
+    if eff_disable and not eff_disable:
+        return disable.was
+    if not eff_disable and act_disable:
+        return disable.now
+    return disable.none
+
+
+def intf_to_dict(conf, default):
     """
     Common used function which will extract VLAN related information from config
     and represent the result as Python dictionary.
 
     Function call's itself recursively if a vif-s/vif-c pair is detected.
     """
-    vlan = {
-        'id': conf.get_level()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
-        'address': [],
-        'address_remove': [],
-        'description': '',
-        'dhcp_client_id': '',
-        'dhcp_hostname': '',
-        'dhcp_vendor_class_id': '',
-        'dhcpv6_prm_only': False,
-        'dhcpv6_temporary': False,
-        'disable': False,
-        'disable_link_detect': 1,
-        'egress_qos': '',
-        'egress_qos_changed': False,
-        'ip_disable_arp_filter': 1,
-        'ip_enable_arp_accept': 0,
-        'ip_enable_arp_announce': 0,
-        'ip_enable_arp_ignore': 0,
-        'ip_proxy_arp': 0,
-        'ipv6_autoconf': 0,
-        'ipv6_eui64_prefix': [],
-        'ipv6_eui64_prefix_remove': [],
-        'ipv6_forwarding': 1,
-        'ipv6_dup_addr_detect': 1,
-        'ingress_qos': '',
-        'ingress_qos_changed': False,
-        'mac': '',
-        'mtu': 1500,
-        'vrf': ''
-    }
+
+    intf = deepcopy(default)
+
     # retrieve configured interface addresses
     if conf.exists('address'):
-        vlan['address'] = conf.return_values('address')
-
-    # Determine interface addresses (currently effective) - to determine which
-    # address is no longer valid and needs to be removed from the bond
-    eff_addr = conf.return_effective_values('address')
-    act_addr = conf.return_values('address')
-    vlan['address_remove'] = list_diff(eff_addr, act_addr)
+        intf['address'] = conf.return_values('address')
 
     # retrieve interface description
     if conf.exists('description'):
-        vlan['description'] = conf.return_value('description')
+        intf['description'] = conf.return_value('description')
 
     # get DHCP client identifier
     if conf.exists('dhcp-options client-id'):
-        vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+        intf['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
 
     # DHCP client host name (overrides the system host name)
     if conf.exists('dhcp-options host-name'):
-        vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+        intf['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
 
     # DHCP client vendor identifier
     if conf.exists('dhcp-options vendor-class-id'):
-        vlan['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
+        intf['dhcp_vendor_class_id'] = conf.return_value(
+            'dhcp-options vendor-class-id')
 
     # DHCPv6 only acquire config parameters, no address
     if conf.exists('dhcpv6-options parameters-only'):
-        vlan['dhcpv6_prm_only'] = True
+        intf['dhcpv6_prm_only'] = True
 
     # DHCPv6 temporary IPv6 address
     if conf.exists('dhcpv6-options temporary'):
-        vlan['dhcpv6_temporary'] = True
+        intf['dhcpv6_temporary'] = True
 
     # ignore link state changes
     if conf.exists('disable-link-detect'):
-        vlan['disable_link_detect'] = 2
-
-    # disable VLAN interface
-    if conf.exists('disable'):
-        vlan['disable'] = True
+        intf['disable_link_detect'] = 2
 
     # ARP filter configuration
     if conf.exists('ip disable-arp-filter'):
-        vlan['ip_disable_arp_filter'] = 0
+        intf['ip_disable_arp_filter'] = 0
 
     # ARP enable accept
     if conf.exists('ip enable-arp-accept'):
-        vlan['ip_enable_arp_accept'] = 1
+        intf['ip_enable_arp_accept'] = 1
 
     # ARP enable announce
     if conf.exists('ip enable-arp-announce'):
-        vlan['ip_enable_arp_announce'] = 1
+        intf['ip_enable_arp_announce'] = 1
 
     # ARP enable ignore
     if conf.exists('ip enable-arp-ignore'):
-        vlan['ip_enable_arp_ignore'] = 1
+        intf['ip_enable_arp_ignore'] = 1
 
     # Enable Proxy ARP
     if conf.exists('ip enable-proxy-arp'):
-        vlan['ip_proxy_arp'] = 1
+        intf['ip_proxy_arp'] = 1
 
     # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
     if conf.exists('ipv6 address autoconf'):
-        vlan['ipv6_autoconf'] = 1
+        intf['ipv6_autoconf'] = 1
 
     # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
     if conf.exists('ipv6 address eui64'):
-        vlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
-
-    # Determine currently effective EUI64 addresses - to determine which
-    # address is no longer valid and needs to be removed
-    eff_addr = conf.return_effective_values('ipv6 address eui64')
-    vlan['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, vlan['ipv6_eui64_prefix'])
-
-    # Remove the default link-local address if set.
-    if conf.exists('ipv6 address no-default-link-local'):
-        vlan['ipv6_eui64_prefix_remove'].append('fe80::/64')
-    else:
-        # add the link-local by default to make IPv6 work
-        vlan['ipv6_eui64_prefix'].append('fe80::/64')
+        intf['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
 
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
-        vlan['ipv6_forwarding'] = 0
-
-    # IPv6 Duplicate Address Detection (DAD) tries
-    if conf.exists('ipv6 dup-addr-detect-transmits'):
-        vlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+        intf['ipv6_forwarding'] = 0
 
     # Media Access Control (MAC) address
     if conf.exists('mac'):
-        vlan['mac'] = conf.return_value('mac')
-        # always recreate EUI64 addresses if mac is set
-        # I'm not sure how to check if a vlan interface exists or how to get its current mac.
-        vlan['ipv6_eui64_prefix_remove'] += vlan['ipv6_eui64_prefix']
+        intf['mac'] = conf.return_value('mac')
+
+    # 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'))
 
     # Maximum Transmission Unit (MTU)
     if conf.exists('mtu'):
-        vlan['mtu'] = int(conf.return_value('mtu'))
+        intf['mtu'] = int(conf.return_value('mtu'))
 
     # retrieve VRF instance
     if conf.exists('vrf'):
-        vlan['vrf'] = conf.return_value('vrf')
+        intf['vrf'] = conf.return_value('vrf')
 
-    # VLAN egress QoS
+    #  egress QoS
     if conf.exists('egress-qos'):
-        vlan['egress_qos'] = conf.return_value('egress-qos')
+        intf['egress_qos'] = conf.return_value('egress-qos')
 
     # egress changes QoS require VLAN interface recreation
     if conf.return_effective_value('egress-qos'):
-        if vlan['egress_qos'] != conf.return_effective_value('egress-qos'):
-            vlan['egress_qos_changed'] = True
+        if intf['egress_qos'] != conf.return_effective_value('egress-qos'):
+            intf['egress_qos_changed'] = True
 
-    # VLAN ingress QoS
+    # ingress QoS
     if conf.exists('ingress-qos'):
-        vlan['ingress_qos'] = conf.return_value('ingress-qos')
+        intf['ingress_qos'] = conf.return_value('ingress-qos')
 
     # ingress changes QoS require VLAN interface recreation
     if conf.return_effective_value('ingress-qos'):
-        if vlan['ingress_qos'] != conf.return_effective_value('ingress-qos'):
-            vlan['ingress_qos_changed'] = True
+        if intf['ingress_qos'] != conf.return_effective_value('ingress-qos'):
+            intf['ingress_qos_changed'] = True
 
-    # ethertype is mandatory on vif-s nodes and only exists here!
-    # check if this is a vif-s node at all:
-    if conf.get_level()[-2] == 'vif-s':
-        vlan['vif_c'] = []
-        vlan['vif_c_remove'] = []
-
-        # ethertype uses a default of 0x88A8
-        tmp = '0x88A8'
-        if conf.exists('ethertype'):
-             tmp = conf.return_value('ethertype')
-        vlan['ethertype'] = get_ethertype(tmp)
-
-        # get vif-c interfaces (currently effective) - to determine which vif-c
+    disabled = disable_state(conf)
+
+    # Get the interface IPs
+    eff_addr = conf.return_effective_values('address')
+    act_addr = conf.return_values('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')
+
+    # Determine what should stay or be removed
+    if disabled == disable.both:
+        # was and is still disabled
+        intf['disable'] = True
+        intf['address_remove'] = []
+        intf['address'] = []
+        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_remove'] = eff_addr
+        intf['address'] = []
+        intf['ipv6_eui64_prefix'] = eff_eui
+        intf['ipv6_eui64_prefix_remove'] = []
+    elif disabled == disable.was:
+        # it was disable but not anymore
+        intf['disable'] = False
+        intf['address_remove'] = []
+        intf['address'] = act_addr
+        intf['ipv6_eui64_prefix'] = []
+        intf['ipv6_eui64_prefix_remove'] = act_eui
+    else:
+        # normal change
+        intf['disable'] = False
+        intf['address_remove'] = list_diff(eff_addr, act_addr)
+        intf['address'] = act_addr
+        intf['ipv6_eui64_prefix_remove'] = list_diff(eff_eui, act_eui)
+        intf['ipv6_eui64_prefix'] = act_eui
+
+    # Remove the default link-local address if set.
+    if conf.exists('ipv6 address no-default-link-local'):
+        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
+    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
+        pass
+
+    return intf, disable
+
+
+
+def add_to_dict(conf, disabled, ifdict, section, key):
+    """
+    parse a section of vif/vif-s/vif-c and add them to the dict
+    follow the convention to:
+    * use the "key" for what to add
+    * use the "key" what what to remove
+
+    conf:     is the Config() already at the level we need to parse
+    disabled: is a disable enum so we know how to handle to data
+    intf:     if the interface dictionary
+    section:  is the section name to parse (vif/vif-s/vif-c)
+    key:      is the dict key to use (vif/vifs/vifc)
+    """
+
+    if not conf.exists(section):
+        return ifdict
+
+    effect = conf.list_effective_nodes(section)
+    active = conf.list_nodes(section)
+
+    # the section to parse for vlan
+    sections = []
+
+    # Determine interface addresses (currently effective) - to determine which
+    # address is no longer valid and needs to be removed from the bond
+    if disabled == disable.both:
+        # was and is still disabled
+        ifdict[f'{key}_remove'] = []
+    elif disabled == disable.now:
+        # it is now disable but was not before
+        ifdict[f'{key}_remove'] = effect
+    elif disabled == disable.was:
+        # it was disable but not anymore
+        ifdict[f'{key}_remove'] = []
+        sections = active
+    else:
+        # normal change
+        # get vif-s interfaces (currently effective) - to determine which vif-s
         # interface is no longer present and needs to be removed
-        eff_intf = conf.list_effective_nodes('vif-c')
-        act_intf = conf.list_nodes('vif-c')
-        vlan['vif_c_remove'] = list_diff(eff_intf, act_intf)
-
-        # check if there is a Q-in-Q vlan customer interface
-        # and call this function recursively
-        if conf.exists('vif-c'):
-            cfg_level = conf.get_level()
-            # add new key (vif-c) to dictionary
-            for vif in conf.list_nodes('vif-c'):
-                # set config level to vif interface
-                conf.set_level(cfg_level + ['vif-c', vif])
-                vlan['vif_c'].append(vlan_to_dict(conf))
+        ifdict[f'{key}_remove'] = list_diff(effect, active)
+        sections = active
+
+    current_level = conf.get_level()
+
+    # add each section, the key must already exists
+    for s in sections:
+        # set config level to vif interface
+        conf.set_level(current_level + [section, s])
+        ifdict[f'{key}'].append(vlan_to_dict(conf))
+
+    # re-set configuration level to leave things as found
+    conf.set_level(current_level)
+
+    return ifdict
+
+
+def vlan_to_dict(conf, default=vlan_default):
+    vlan, disabled = intf_to_dict(conf, default)
+    # 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':
+        return vlan
+
+    # ethertype is mandatory on vif-s nodes and only exists here!
+    # ethertype uses a default of 0x88A8
+    tmp = '0x88A8'
+    if conf.exists('ethertype'):
+        tmp = conf.return_value('ethertype')
+    vlan['ethertype'] = get_ethertype(tmp)
+
+    # check if there is a Q-in-Q vlan customer interface
+    # and call this function recursively
+
+    add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c')
 
     return vlan
-- 
cgit v1.2.3