From d98e01da6b1805e8e1d53f631ea6f4ea46d0d218 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 15 Nov 2020 10:35:39 +0100
Subject: tunnel: T3072: migrate to get_config_dict()

---
 src/conf_mode/interfaces-tunnel.py | 781 ++++++-------------------------------
 1 file changed, 118 insertions(+), 663 deletions(-)

(limited to 'src/conf_mode')

diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index f1217b62d..461ac38ce 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -15,354 +15,110 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import netifaces
 
 from sys import exit
 from copy import deepcopy
-from netifaces import interfaces
 
 from vyos.config import Config
-from vyos.configdict import is_member
-from vyos.configdict import list_diff
-from vyos.dicts import FixedDict
-from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
-from vyos.ifconfig.afi import IP4, IP6
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.ifconfig import Interface
+from vyos.ifconfig import GREIf
+from vyos.ifconfig import GRETapIf
+from vyos.ifconfig import IPIPIf
+from vyos.ifconfig import IP6GREIf
+from vyos.ifconfig import IPIP6If
+from vyos.ifconfig import IP6IP6If
+from vyos.ifconfig import SitIf
+from vyos.ifconfig import Sit6RDIf
 from vyos.template import is_ipv4
 from vyos.template import is_ipv6
+from vyos.util import dict_search
 from vyos import ConfigError
-
-
 from vyos import airbag
 airbag.enable()
 
-
-class ConfigurationState(object):
+def get_config(config=None):
     """
-    The current API require a dict to be generated by get_config()
-    which is then consumed by verify(), generate() and apply()
-
-    ConfiguartionState is an helper class wrapping Config and providing
-    an common API to this dictionary structure
-
-    Its to_api() function return a dictionary containing three fields,
-    each a dict, called options, changes, actions.
-
-    options:
-
-    contains the configuration options for the dict and its value
-    {'options': {'commment': 'test'}} will be set if
-    'set interface dummy dum1 description test' was used and
-    the key 'commment' is used to index the description info.
-
-    changes:
-
-    per key, let us know how the data was modified using one of the action
-    a special key called 'section' is used to indicate what happened to the
-    section. for example:
-
-    'set interface dummy dum1 description test' when no interface was setup
-    will result in the following changes
-    {'changes': {'section': 'create', 'comment': 'create'}}
-
-    on an existing interface, depending if there was a description
-    'set interface dummy dum1 description test' will result in one of
-    {'changes': {'comment': 'create'}}  (not present before)
-    {'changes': {'comment': 'static'}}  (unchanged)
-    {'changes': {'comment': 'modify'}}  (changed from half)
-
-    and 'delete interface dummy dummy1 description' will result in:
-    {'changes': {'comment': 'delete'}}
-
-    actions:
-
-    for each action list the configuration key which were changes
-    in our example if we added the 'description' and added an IP we would have
-    {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}}
-
-    the actions are:
-        'create': it did not exist previously and was created
-        'modify': it did exist previously but its content changed
-        'static': it did exist and did not change
-        'delete': it was present but was removed from the configuration
-        'absent': it was not and is not present
-    which for each field represent how it was modified since the last commit
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
     """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'tunnel']
+    tunnel = get_interface_dict(conf, base)
+
+    # Wireguard is "special" the default MTU is 1420 - update accordingly
+    # as the config_level is already st in get_interface_dict() - we can use []
+    tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+    if 'mtu' not in tmp:
+        tunnel['mtu'] = '1476'
+
+    return tunnel
+
+def verify(tunnel):
+    if 'deleted' in tunnel:
+        verify_bridge_delete(tunnel)
+        # TODO: check for NHRP tunnel member
+        return None
 
-    def __init__(self, configuration, section, default):
-        """
-        initialise the class for a given configuration path:
-
-        >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1')
-        all further references to get_value(s) and get_effective(s)
-        will be for this part of the configuration (eth1)
-        """
-        self._conf = configuration
-
-        self.default = deepcopy(default)
-        self.options = FixedDict(**default)
-        self.actions = {
-            'create': [],  # the key did not exist and was added
-            'static': [],  # the key exists and its value was not modfied
-            'modify': [],  # the key exists and its value was modified
-            'absent': [],  # the key is not present
-            'delete': [],  # the key was present and was deleted
-        }
-        self.changes = {}
-        if not self._conf.exists(section):
-            self.changes['section'] = 'delete'
-        elif self._conf.exists_effective(section):
-            self.changes['section'] = 'modify'
-        else:
-            self.changes['section'] = 'create'
-
-        self.set_level(section)
-
-    def set_level(self, lpath):
-        self.section = lpath
-        self._conf.set_level(lpath)
-
-    def _act(self, section):
-        """
-        Returns for a given configuration field determine what happened to it
-
-        'create': it did not exist previously and was created
-        'modify': it did exist previously but its content changed
-        'static': it did exist and did not change
-        'delete': it was present but was removed from the configuration
-        'absent': it was not and is not present
-        """
-        if self._conf.exists(section):
-            if self._conf.exists_effective(section):
-                if self._conf.return_value(section) != self._conf.return_effective_value(section):
-                    return 'modify'
-                return 'static'
-            return 'create'
-        else:
-            if self._conf.exists_effective(section):
-                return 'delete'
-            return 'absent'
-
-    def _action(self, name, key):
-        action = self._act(key)
-        self.changes[name] = action
-        self.actions[action].append(name)
-        return action
-
-    def _get(self, name, key, default, getter):
-        value = getter(key)
-        if not value:
-            if default:
-                self.options[name] = default
-                return
-            self.options[name] = self.default[name]
-            return
-        self.options[name] = value
-
-    def get_value(self, name, key, default=None):
-        """
-        >>> conf.get_value('comment', 'description')
-        will place the string of 'interface dummy description test'
-        into the dictionnary entry 'comment' using Config.return_value
-        (the data in the configuration to apply)
-        """
-        if self._action(name, key) in ('delete', 'absent'):
-            return
-        return self._get(name, key, default, self._conf.return_value)
-
-    def get_values(self, name, key, default=None):
-        """
-        >>> conf.get_values('addresses', 'address')
-        will place a list of the new IP present in 'interface dummy dum1 address'
-        into the dictionnary entry "-add" (here 'addresses-add') using
-        Config.return_values and will add the the one which were removed in into
-        the entry "-del" (here addresses-del')
-        """
-        add_name = f'{name}-add'
-
-        if self._action(add_name, key) in ('delete', 'absent'):
-            return
-
-        self._get(add_name, key, default, self._conf.return_values)
-
-        # get the effective values to determine which data is no longer valid
-        self.options['addresses-del'] = list_diff(
-            self._conf.return_effective_values('address'),
-            self.options['addresses-add']
-        )
-
-    def get_effective(self, name, key, default=None):
-        """
-        >>> conf.get_value('comment', 'description')
-        will place the string of 'interface dummy description test'
-        into the dictionnary entry 'comment' using Config.return_effective_value
-        (the data in the configuration to apply)
-        """
-        self._action(name, key)
-        return self._get(name, key, default, self._conf.return_effective_value)
-
-    def get_effectives(self, name, key, default=None):
-        """
-        >>> conf.get_effectives('addresses-add', 'address')
-        will place a list made of the IP present in 'interface ethernet eth1 address'
-        into the dictionnary entry 'addresses-add' using Config.return_effectives_value
-        (the data in the un-modified configuration)
-        """
-        self._action(name, key)
-        return self._get(name, key, default, self._conf.return_effectives_value)
-
-    def load(self, mapping):
-        """
-        load will take a dictionary defining how we wish the configuration
-        to be parsed and apply this definition to set the data.
-
-        >>> mapping = {
-            'addresses-add' : ('address', True, None),
-            'comment' : ('description', False, 'auto'),
-        }
-        >>> conf.load(mapping)
-
-        mapping is a dictionary where each key represents the name we wish
-        to have (such as 'addresses-add'), with a list a content representing
-        how the data should be parsed:
-            - the configuration section name
-              such as 'address' under 'interface ethernet eth1'
-            - boolean indicating if this data can have multiple values
-              for 'address', True, as multiple IPs can be set
-              for 'description', False, as it is a single string
-            - default represent the default value if absent from the configuration
-              'None' indicate that no default should be set if the configuration
-              does not have the configuration section
-
-        """
-        for local_name, (config_name, multiple, default) in mapping.items():
-            if multiple:
-                self.get_values(local_name, config_name, default)
-            else:
-                self.get_value(local_name, config_name, default)
-
-    def remove_default(self,*options):
-        """
-        remove all the values which were not changed from the default
-        """
-        for option in options:
-            if not self._conf.exists(option):
-                del self.options[option]
-                continue
-
-            if self._conf.return_value(option) == self.default[option]:
-                del self.options[option]
-                continue
-
-            if self._conf.return_values(option) == self.default[option]:
-                del self.options[option]
-                continue
+    if 'encapsulation' not in tunnel:
+        raise ConfigError('Must configure the tunnel encapsulation for '\
+                          '{ifname}!'.format(**tunnel))
 
-    def as_dict(self, lpath):
-        l = self._conf.get_level()
-        self._conf.set_level([])
-        d = self._conf.get_config_dict(lpath)
-        # XXX: that not what I would have expected from get_config_dict
-        if lpath:
-            d = d[lpath[-1]]
-        # XXX: it should have provided me the content and not the key
-        self._conf.set_level(l)
-        return d
+    verify_mtu_ipv6(tunnel)
+    verify_address(tunnel)
+    verify_vrf(tunnel)
 
-    def to_api(self):
-        """
-        provide a dictionary with the generated data for the configuration
-        options: the configuration value for the key
-        changes: per key how they changed from the previous configuration
-        actions: per changes all the options which were changed
-        """
-        # as we have to use a dict() for the API for verify and apply the options
-        return {
-            'options': self.options,
-            'changes': self.changes,
-            'actions': self.actions,
-        }
+    if 'local_ip' not in tunnel and 'dhcp_interface' not in tunnel:
+        raise ConfigError('local-ip is mandatory for tunnel')
 
+    if 'remote_ip' not in tunnel and tunnel['encapsulation'] != 'gre':
+        raise ConfigError('remote-ip is mandatory for tunnel')
 
-default_config_data = {
-    # interface definition
-    'vrf': '',
-    'addresses-add': [],
-    'addresses-del': [],
-    'state': 'up',
-    'dhcp-interface': '',
-    'link_detect': 1,
-    'ip': False,
-    'ipv6': False,
-    'nhrp': [],
-    'arp_filter': 1,
-    'arp_accept': 0,
-    'arp_announce': 0,
-    'arp_ignore': 0,
-    'ipv6_accept_ra': 1,
-    'ipv6_autoconf': 0,
-    'ipv6_forwarding': 1,
-    'ipv6_dad_transmits': 1,
-    # internal
-    'interfaces': [],
-    'tunnel': {},
-    'bridge': '',
-    # the following names are exactly matching the name
-    # for the ip command and must not be changed
-    'ifname': '',
-    'type': '',
-    'alias': '',
-    'mtu': '1476',
-    'local': '',
-    'remote': '',
-    'dev': '',
-    'multicast': 'disable',
-    'allmulticast': 'disable',
-    'ttl': '255',
-    'tos': 'inherit',
-    'key': '',
-    'encaplimit': '4',
-    'flowlabel': 'inherit',
-    'hoplimit': '64',
-    'tclass': 'inherit',
-    '6rd-prefix': '',
-    '6rd-relay-prefix': '',
-}
+    if {'local_ip', 'dhcp_interface'} <= set(tunnel):
+        raise ConfigError('Can not use both local-ip and dhcp-interface')
 
+    if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
+        error_ipv6 = 'Encapsulation mode requires IPv6'
+        if 'local_ip' in tunnel and not is_ipv6(tunnel['local_ip']):
+            raise ConfigError(f'{error_ipv6} local-ip')
 
-# dict name -> config name, multiple values, default
-mapping = {
-    'type':                ('encapsulation', False, None),
-    'alias':               ('description', False, None),
-    'mtu':                 ('mtu', False, None),
-    'local':               ('local-ip', False, None),
-    'remote':              ('remote-ip', False, None),
-    'multicast':           ('multicast', False, None),
-    'dev':                 ('source-interface', False, None),
-    'ttl':                 ('parameters ip ttl', False, None),
-    'tos':                 ('parameters ip tos', False, None),
-    'key':                 ('parameters ip key', False, None),
-    'encaplimit':          ('parameters ipv6 encaplimit', False, None),
-    'flowlabel':           ('parameters ipv6 flowlabel', False, None),
-    'hoplimit':            ('parameters ipv6 hoplimit', False, None),
-    'tclass':              ('parameters ipv6 tclass', False, None),
-    '6rd-prefix':          ('6rd-prefix', False, None),
-    '6rd-relay-prefix':    ('6rd-relay-prefix', False, None),
-    'dhcp-interface':      ('dhcp-interface', False, None),
-    'state':               ('disable', False, 'down'),
-    'link_detect':         ('disable-link-detect', False, 2),
-    'vrf':                 ('vrf', False, None),
-    'addresses':           ('address', True, None),
-    'arp_filter':          ('ip disable-arp-filter', False, 0),
-    'arp_accept':          ('ip enable-arp-accept', False, 1),
-    'arp_announce':        ('ip enable-arp-announce', False, 1),
-    'arp_ignore':          ('ip enable-arp-ignore', False, 1),
-    'ipv6_autoconf':       ('ipv6 address autoconf', False, 1),
-    'ipv6_forwarding':     ('ipv6 disable-forwarding', False, 0),
-    'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None)
-}
+        if 'remote_ip' in tunnel and not is_ipv6(tunnel['remote_ip']):
+            raise ConfigError(f'{error_ipv6} remote-ip')
+    else:
+        error_ipv4 = 'Encapsulation mode requires IPv4'
+        if 'local_ip' in tunnel and not is_ipv4(tunnel['local_ip']):
+            raise ConfigError(f'{error_ipv4} local-ip')
+
+        if 'remote_ip' in tunnel and not is_ipv4(tunnel['remote_ip']):
+            raise ConfigError(f'{error_ipv4} remote-ip')
+
+    if tunnel['encapsulation'] in ['sit', 'gre-bridge']:
+        if 'source_interface' in tunnel:
+            raise ConfigError('Option source-interface can not be used with ' \
+                              'encapsulation "sit" or "gre-bridge"')
+    elif tunnel['encapsulation'] == 'gre':
+        if is_ipv6(tunnel['local_ip']):
+            raise ConfigError('Can not use local IPv6 address is for mGRE tunnels')
+
+def generate(tunnel):
+    return None
 
+def apply(tunnel):
+    if 'deleted' in tunnel:
+        tmp = Interface(tunnel['ifname'])
+        tmp.remove()
+        return None
 
-def get_class (options):
     dispatch = {
         'gre': GREIf,
         'gre-bridge': GRETapIf,
@@ -373,353 +129,52 @@ def get_class (options):
         'sit': SitIf,
     }
 
-    kls = dispatch[options['type']]
-    if options['type'] == 'gre' and not options['remote'] \
-        and not options['key'] and not options['multicast']:
-        # will use GreTapIf on GreIf deletion but it does not matter
-        return GRETapIf
-    elif options['type'] == 'sit' and options['6rd-prefix']:
-        # will use SitIf on Sit6RDIf deletion but it does not matter
-        return Sit6RDIf
-    return kls
-
-def get_interface_ip (ifname):
-    if not ifname:
-        return ''
-    try:
-        addrs = Interface(ifname).get_addr()
-        if addrs:
-            return addrs[0].split('/')[0]
-    except Exception:
-        return ''
-
-def get_afi (ip):
-    return IP6 if is_ipv6(ip) else IP4
-
-def ip_proto (afi):
-    return 6 if afi == IP6 else 4
-
-
-def get_config(config=None):
-    ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
-    if not ifname:
-        raise ConfigError('Interface not specified')
-
-    if config:
-        config = config
-    else:
-        config = Config()
-
-    conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data)
-    options = conf.options
-    changes = conf.changes
-    options['ifname'] = ifname
-
-    if changes['section'] == 'delete':
-        conf.get_effective('type', mapping['type'][0])
-        config.set_level(['protocols', 'nhrp', 'tunnel'])
-        options['nhrp'] = config.list_nodes('')
-        return conf.to_api()
-
-    # load all the configuration option according to the mapping
-    conf.load(mapping)
-
-    # remove default value if not set and not required
-    afi_local = get_afi(options['local'])
-    if afi_local == IP6:
-        conf.remove_default('ttl', 'tos', 'key')
-    if afi_local == IP4:
-        conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass')
-
-    # if the local-ip is not set, pick one from the interface !
-    # hopefully there is only one, otherwise it will not be very deterministic
-    # at time of writing the code currently returns ipv4 before ipv6 in the list
-
-    # XXX: There is no way to trigger an update of the interface source IP if
-    # XXX: the underlying interface IP address does change, I believe this
-    # XXX: limit/issue is present in vyatta too
-
-    if not options['local'] and options['dhcp-interface']:
-        # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if
-        # XXX: the interface was not DHCP. As there is no easy way to find if an
-        # XXX: interface is using DHCP, and using this feature to get 127.0.0.1
-        # XXX: makes little sense, I feel the change in behaviour is acceptable
-        picked = get_interface_ip(options['dhcp-interface'])
-        if picked == '':
-            picked = '127.0.0.1'
-            print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead')
-        options['local'] = picked
-        options['dhcp-interface'] = ''
-
-    # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
-    # accept_ra must be 2
-    if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
-        options['ipv6_accept_ra'] = 2
-
-    # allmulticast fate is linked to multicast
-    options['allmulticast'] = options['multicast']
-
-    # check that per encapsulation all local-remote pairs are unique
-    ct = conf.as_dict(['interfaces', 'tunnel'])
-    options['tunnel'] = {}
-
-    # check for bridges
-    tmp = is_member(config, ifname, 'bridge')
-    if tmp: options['bridge'] = next(iter(tmp))
-    options['interfaces'] = interfaces()
-
-    for name in ct:
-        tunnel = ct[name]
-        encap = tunnel.get('encapsulation', '')
-        local = tunnel.get('local-ip', '')
-        if not local:
-            local = get_interface_ip(tunnel.get('dhcp-interface', ''))
-        remote = tunnel.get('remote-ip', '<unset>')
-        pair = f'{local}-{remote}'
-        options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1
-
-    return conf.to_api()
-
-
-def verify(conf):
-    options = conf['options']
-    changes = conf['changes']
-    actions = conf['actions']
-
-    ifname = options['ifname']
-    iftype = options['type']
-
-    if changes['section'] == 'delete':
-        if ifname in options['nhrp']:
-            raise ConfigError((
-                f'Cannot delete interface tunnel {iftype} {ifname}, '
-                'it is used by NHRP'))
-
-        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
-
-    # tunnel encapsulation checks
-
-    if not iftype:
-        raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}')
-
-    if changes['type'] in ('modify', 'delete'):
-        # TODO: we could now deal with encapsulation modification by deleting / recreating
-        raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}')
-
-    if iftype != 'sit' and options['6rd-prefix']:
-        # XXX: should be able to remove this and let the definition catch it
-        print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}')
-
-    # what are the tunnel options we can set / modified / deleted
-
-    kls = get_class(options)
-    valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
-    valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore']
-    valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits']
-
-    if changes['section'] == 'create':
-        valid.extend(['type',])
-        valid.extend([o for o in kls.options if o not in kls.updates])
-
-    for create in actions['create']:
-        if create not in valid:
-            raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation')
-
-    for modify in actions['modify']:
-        if modify not in valid:
-            raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation')
-
-    for delete in actions['delete']:
-        if delete in kls.required:
-            raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}')
-
-    # tunnel information
-
-    tun_local = options['local']
-    afi_local = get_afi(tun_local)
-    tun_remote = options['remote'] or tun_local
-    afi_remote = get_afi(tun_remote)
-    tun_ismgre = iftype == 'gre' and not options['remote']
-    tun_is6rd = iftype == 'sit' and options['6rd-prefix']
-    tun_dev = options['dev']
-
-    # incompatible options
-
-    if not tun_local and not options['dhcp-interface'] and not tun_is6rd:
-        raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}')
-
-    if tun_local and options['dhcp-interface']:
-        raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
-
-    if tun_dev and iftype in ('gre-bridge', 'sit'):
-        raise ConfigError(f'source interface can not be used with {iftype} {ifname}')
-
-    # tunnel endpoint
-
-    if afi_local != afi_remote:
-        raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}')
-
-    if afi_local != kls.tunnel:
-        version = 4 if tun_local == IP4 else 6
-        raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}')
-
-    ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)])
-    ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)])
-
-    if tun_ismgre and afi_local == IP6:
-        raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}')
-
-    # check address family use
-    # checks are not enforced (but ip command failing) for backward compatibility
-
-    if ipv4_count and not IP4 in kls.ip:
-        print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}')
-
-    if ipv6_count and not IP6 in kls.ip:
-        print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
-
-    # vrf check
-    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
-
-    if tun_dev and tun_dev not in options['interfaces']:
-        raise ConfigError(f'device "{tun_dev}" does not exist')
-
-    # tunnel encapsulation check
-
-    convert = {
-        (6, 4, 'gre'):  'ip6gre',
-        (6, 6, 'gre'):  'ip6gre',
-        (4, 6, 'ipip'): 'ipip6',
-        (6, 6, 'ipip'): 'ip6ip6',
+    # We need to re-map the tunnel encapsulation proto to a valid interface class
+    encap = tunnel['encapsulation']
+    klass = dispatch[encap]
+
+    # This is a special type of interface which needs additional parameters
+    # when created using iproute2. Instead of passing a ton of arguments,
+    # use a dictionary provided by the interface class which holds all the
+    # options necessary.
+    conf = klass.get_config()
+
+    # Copy/re-assign our dictionary values to values understood by the
+    # derived _Tunnel classes
+    mapping = {
+        # this                       :  get_config()
+        'local_ip'                   : 'local',
+        'remote_ip'                  : 'remote',
+        'source_interface'           : 'dev',
+        'parameters.ip.ttl'          : 'ttl',
+        'parameters.ip.tos'          : 'tos',
+        'parameters.ip.key'          : 'key',
+        'parameters.ipv6.encaplimit' : 'encaplimit'
     }
 
-    iprotos = []
-    if ipv4_count:
-        iprotos.append(4)
-    if ipv6_count:
-        iprotos.append(6)
-
-    for iproto in iprotos:
-        replace  = convert.get((kls.tunnel, iproto, iftype), '')
-        if replace:
-            raise ConfigError(
-                f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' +
-                f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.'
-            )
-
-    # tunnel options
-
-    incompatible = []
-    if afi_local == IP6:
-        incompatible.extend(['ttl', 'tos', 'key',])
-    if afi_local == IP4:
-        incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass'])
-
-    for option in incompatible:
-        if option in options:
-            # TODO: raise converted to print as not enforced by vyatta
-            # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}')
-            print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}')
-
-    # duplicate tunnel pairs
-
-    pair = '{}-{}'.format(options['local'], options['remote'])
-    if options['tunnel'].get(iftype, {}).get(pair, 0) > 1:
-        raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}')
+    # Add additional IPv6 options if tunnel is IPv6 aware
+    if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
+        mappingv6 = {
+            # this                       :  get_config()
+            'parameters.ipv6.encaplimit' : 'encaplimit'
+        }
+        mapping.update(mappingv6)
 
-    return None
+    for our_key, their_key in mapping.items():
+        if dict_search(our_key, tunnel) and their_key in conf:
+            conf[their_key] = dict_search(our_key, tunnel)
 
+    # TODO: support encapsulation change per tunnel
 
-def generate(gre):
+    tun = klass(tunnel['ifname'], **conf)
+    tun.update(tunnel)
     return None
 
-def apply(conf):
-    options = conf['options']
-    changes = conf['changes']
-    actions = conf['actions']
-    kls = get_class(options)
-
-    # extract ifname as otherwise it is duplicated on the interface creation
-    ifname = options.pop('ifname')
-
-    # only the valid keys for creation of a Interface
-    config = dict((k, options[k]) for k in kls.options if options[k])
-
-    # setup or create the tunnel interface if it does not exist
-    tunnel = kls(ifname, **config)
-
-    if changes['section'] == 'delete':
-        tunnel.remove()
-        # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup
-        # which identified tunnels type which were not used anymore to remove them
-        # (ie: gre0, gretap0, etc.) The perl code did however nothing
-        # This feature is also not implemented yet
-        return
-
-    # A GRE interface without remote will be mGRE
-    # if the interface does not suppor the option, it skips the change
-    for option in tunnel.updates:
-        if changes['section'] in 'create' and option in tunnel.options:
-            # it was setup at creation
-            continue
-        if not options[option]:
-            # remote can be set to '' and it would generate an invalide command
-            continue
-        tunnel.set_interface(option, options[option])
-
-    # set other interface properties
-    for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
-                   'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore',
-                   'ipv6_accept_ra', '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)
-    for addr in options['addresses-add']:
-        tunnel.add_addr(addr)
-
-    # now bring it up (or not)
-    tunnel.set_admin_state(options['state'])
-
-
 if __name__ == '__main__':
     try:
         c = get_config()
-        verify(c)
         generate(c)
+        verify(c)
         apply(c)
     except ConfigError as e:
         print(e)
-- 
cgit v1.2.3