From a25d7095e009469d8ef60b63deddd94d30921723 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 19 Jul 2020 20:45:29 +0200
Subject: bridge: ifconfig: T2653: move to get_config_dict()

The current VyOS CLI parser code written in Python contains a ton of duplicates
which I can also hold myself accountable for - or maybe mainly me - depends on
the angle of judge.

While providing a new update() method in vyos.ifconfig.interfaces() this is
extended for bridge interfaces in the derived bridge class.

Signed-off-by: Christian Poessinger <christian@poessinger.com>
---
 python/vyos/configdict.py         | 97 +++++++++++++++++++++++++++++++++++++++
 python/vyos/ifconfig/bridge.py    | 68 ++++++++++++++++++++++++++-
 python/vyos/ifconfig/ethernet.py  | 20 --------
 python/vyos/ifconfig/interface.py | 25 ++++++++++
 python/vyos/ifconfig_vlan.py      |  9 ++--
 python/vyos/util.py               |  2 +-
 6 files changed, 195 insertions(+), 26 deletions(-)

(limited to 'python')

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 682caed8f..4fca426cd 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -17,6 +17,7 @@
 A library for retrieving value dicts from VyOS configs in a declarative fashion.
 
 """
+import jmespath
 
 from enum import Enum
 from copy import deepcopy
@@ -132,6 +133,102 @@ def T2665_default_dict_cleanup(dict):
 
     return dict
 
+def leaf_node_changed(conf, key):
+    """
+    Check if a leaf node was altered. If it has been altered - values has been
+    changed, or it was added/removed, we will return the old value. If nothing
+    has been changed, None is returned
+    """
+    from vyos.configdiff import get_config_diff
+
+    D = get_config_diff(conf, key_mangling=('-', '_'))
+    D.set_level(conf.get_level())
+    (new, old) = D.get_value_diff(key)
+    if new != old:
+        if isinstance(old, str):
+            return old
+        elif isinstance(old, list):
+            if isinstance(new, str):
+                new = [new]
+            elif isinstance(new, type(None)):
+                new = []
+            return list_diff(old, new)
+
+    return None
+
+def get_interface_dict(config, base, ifname):
+    """
+    Common utility function to retrieve and mandgle the interfaces available
+    in CLI configuration. All interfaces have a common base ground where the
+    value retrival is identical - so it can and should be reused
+
+    Will return a dictionary with the necessary interface configuration
+    """
+    from vyos.xml import defaults
+    from vyos.ifconfig_vlan import get_removed_vlans
+
+    # retrieve interface default values
+    default_values = defaults(base)
+
+    # setup config level which is extracted in get_removed_vlans()
+    config.set_level(base + [ifname])
+    dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+
+    # Check if interface has been removed
+    if dict == {}:
+        dict.update({'deleted' : ''})
+
+    # Add interface instance name into dictionary
+    dict.update({'ifname': ifname})
+
+    # We have gathered the dict representation of the CLI, but there are
+    # default options which we need to update into the dictionary
+    # retrived.
+    dict = dict_merge(default_values, dict)
+
+    # Check if we are a member of a bridge device
+    bridge = is_member(config, ifname, 'bridge')
+    if bridge:
+        dict.update({'is_bridge_member' : bridge})
+
+    # Check if we are a member of a bond device
+    bond = is_member(config, ifname, 'bonding')
+    if bond:
+        dict.update({'is_bond_member' : bond})
+
+    mac = leaf_node_changed(config, ['mac'])
+    if mac:
+        dict.update({'mac_old' : mac})
+
+    eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64'])
+    if eui64:
+        # XXX: T2636 workaround: convert string to a list with one element
+        if isinstance(eui64, str):
+            eui64 = [eui64]
+        tmp = jmespath.search('ipv6.address', dict)
+        if not tmp:
+            dict.update({'ipv6': {'address': {'eui64_old': eui64}}})
+        else:
+            dict['ipv6']['address'].update({'eui64_old': eui64})
+
+    # remove wrongly inserted values
+    dict = T2665_default_dict_cleanup(dict)
+
+    # The values are identical for vif, vif-s and vif-c as the all include the same
+    # XML definitions which hold the defaults
+    default_vif_values = defaults(base + ['vif'])
+    for vif, vif_config in dict.get('vif', {}).items():
+        vif_config.update(default_vif_values)
+    for vif_s, vif_s_config in dict.get('vif_s', {}).items():
+        vif_s_config.update(default_vif_values)
+        for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
+            vif_c_config.update(default_vif_values)
+
+    # Check vif, vif-s/vif-c VLAN interfaces for removal
+    dict = get_removed_vlans(config, dict)
+
+    return dict
+
 def get_ethertype(ethertype_val):
     if ethertype_val == '0x88A8':
         return '802.1ad'
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index 44b92c1db..af950b35d 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -13,12 +13,13 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
+import jmespath
 
 from vyos.ifconfig.interface import Interface
-
+from vyos.ifconfig.stp import STP
 from vyos.validate import assert_boolean
 from vyos.validate import assert_positive
-
+from vyos.util import cmd
 
 @Interface.register
 class BridgeIf(Interface):
@@ -187,3 +188,66 @@ class BridgeIf(Interface):
         >>> BridgeIf('br0').del_port('eth1')
         """
         return self.set_interface('del_port', interface)
+
+    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. """
+
+        # now call the regular function from within our base class
+        super().update(config)
+
+        # Set ageing time
+        value = config.get('aging')
+        self.set_ageing_time(value)
+
+        # set bridge forward delay
+        value = config.get('forwarding_delay')
+        self.set_forward_delay(value)
+
+        # set hello time
+        value = config.get('hello_time')
+        self.set_hello_time(value)
+
+        # set max message age
+        value = config.get('max_age')
+        self.set_max_age(value)
+
+        # set bridge priority
+        value = config.get('priority')
+        self.set_priority(value)
+
+        # enable/disable spanning tree
+        value = '1' if 'stp' in config else '0'
+        self.set_stp(value)
+
+        # enable or disable IGMP querier
+        tmp = jmespath.search('igmp.querier', config)
+        value = '1' if (tmp != None) else '0'
+        self.set_multicast_querier(value)
+
+        # remove interface from bridge
+        tmp = jmespath.search('member.interface_remove', config)
+        if tmp:
+            for member in tmp:
+                self.del_port(member)
+
+        STPBridgeIf = STP.enable(BridgeIf)
+        tmp = jmespath.search('member.interface', config)
+        if tmp:
+            for interface, interface_config in tmp.items():
+                # 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 "{interface}"')
+                # enslave interface port to bridge
+                self.add_port(interface)
+
+                tmp = STPBridgeIf(interface)
+                # set bridge port path cost
+                value = interface_config.get('cost')
+                tmp.set_path_cost(value)
+
+                # set bridge port path priority
+                value = interface_config.get('priority')
+                tmp.set_path_priority(value)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 8a50a8699..1725116e2 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -299,26 +299,6 @@ class EthernetIf(Interface):
             duplex = config.get('duplex')
             self.set_speed_duplex(speed, duplex)
 
-        # Delete old IPv6 EUI64 addresses before changing MAC
-
-        # 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)
-
-        # 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')
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index be3617f7d..ea770af23 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -922,6 +922,31 @@ class Interface(Control):
         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)
+
+        # 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)
+
+
         # Interface administrative state
         state = 'down' if 'disable' in config else 'up'
         self.set_admin_state(state)
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index ecb6796fa..0e4ecda53 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -28,15 +28,18 @@ def get_removed_vlans(conf, dict):
     D.set_level(conf.get_level())
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
-    dict['vif_remove'] = [*keys]
+    if keys:
+        dict.update({'vif_remove': [*keys]})
 
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
-    dict['vif_s_remove'] = [*keys]
+    if keys:
+        dict.update({'vif_s_remove': [*keys]})
 
     for vif in dict.get('vif_s', {}).keys():
         keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
-        dict['vif_s'][vif]['vif_c_remove'] = [*keys]
+        if keys:
+            dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}})
 
     return dict
 
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 7234be6cb..7078762df 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -242,7 +242,7 @@ def chown(path, user, group):
 
     if not os.path.exists(path):
         return False
-	
+
     uid = getpwnam(user).pw_uid
     gid = getgrnam(group).gr_gid
     os.chown(path, uid, gid)
-- 
cgit v1.2.3