diff options
-rw-r--r-- | data/templates/vrf/vrf.conf.tmpl | 9 | ||||
-rw-r--r-- | interface-definitions/vrf.xml.in | 1 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_vrf.py | 47 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 241 |
4 files changed, 145 insertions, 153 deletions
diff --git a/data/templates/vrf/vrf.conf.tmpl b/data/templates/vrf/vrf.conf.tmpl index 6d01d2b89..29c0ba08d 100644 --- a/data/templates/vrf/vrf.conf.tmpl +++ b/data/templates/vrf/vrf.conf.tmpl @@ -1,8 +1,9 @@ ### Autogenerated by vrf.py ### # # Routing table ID to name mapping reference - # id vrf name comment -{% for vrf in vrf_add %} -{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} -{% endfor %} +{% if name is defined and name is not none %} +{% for vrf, vrf_config in name.items() %} +{{ "%-10s" | format(vrf_config.table) }} {{ "%-16s" | format(vrf) }} {{ '# ' + vrf_config.description if vrf_config.description is defined and vrf_config.description is not none }} +{% endfor %} +{% endif %} diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 06923971d..81c89d94b 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -40,6 +40,7 @@ </properties> </leafNode> #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> </children> </tagNode> </children> diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 7bcfea861..c0f610323 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -16,10 +16,13 @@ import os import unittest +from netifaces import interfaces -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.util import read_file from vyos.validate import is_intf_addr_assigned +from vyos.ifconfig import Interface class VRFTest(unittest.TestCase): def setUp(self): @@ -36,9 +39,12 @@ class VRFTest(unittest.TestCase): table = 1000 for vrf in self._vrfs: base = ['vrf', 'name', vrf] - description = "VyOS-VRF-" + vrf + description = f'VyOS-VRF-{vrf}' self.session.set(base + ['description', description]) + if vrf == 'green': + self.session.set(base + ['disable']) + # check validate() - a table ID is mandatory with self.assertRaises(ConfigSessionError): self.session.commit() @@ -49,6 +55,19 @@ class VRFTest(unittest.TestCase): # commit changes self.session.commit() + # Verify VRF configuration + for vrf in self._vrfs: + description = f'VyOS-VRF-{vrf}' + self.assertTrue(vrf in interfaces()) + vrf_if = Interface(vrf) + # validate proper interface description + self.assertEqual(vrf_if.get_alias(), description) + # validate admin up/down state of VRF + state = 'up' + if vrf == 'green': + state = 'down' + self.assertEqual(vrf_if.get_admin_state(), state) + def test_vrf_loopback_ips(self): table = 1000 for vrf in self._vrfs: @@ -58,9 +77,33 @@ class VRFTest(unittest.TestCase): # commit changes self.session.commit() + + # Verify VRF configuration for vrf in self._vrfs: + self.assertTrue(vrf in interfaces()) self.assertTrue(is_intf_addr_assigned(vrf, '127.0.0.1')) self.assertTrue(is_intf_addr_assigned(vrf, '::1')) + def test_vrf_table_id_is_unalterable(self): + # Linux Kernel prohibits the change of a VRF table on the fly. + # VRF must be deleted and recreated! + table = 666 + vrf = self._vrfs[0] + base = ['vrf', 'name', vrf] + self.session.set(base + ['table', str(table)]) + + # commit changes + self.session.commit() + + # Check if VRF has been created + self.assertTrue(vrf in interfaces()) + + table += 1 + self.session.set(base + ['table', str(table)]) + # check validate() - table ID can not be altered! + with self.assertRaises(ConfigSessionError): + self.session.commit() + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c4ba859b7..ebad7e246 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -17,29 +17,20 @@ import os from sys import exit -from copy import deepcopy from json import loads from vyos.config import Config -from vyos.configdict import list_diff +from vyos.configdict import node_changed from vyos.ifconfig import Interface -from vyos.util import read_file, cmd -from vyos import ConfigError from vyos.template import render - +from vyos.util import cmd +from vyos.util import dict_search +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' -default_config_data = { - 'bind_to_all': '0', - 'deleted': False, - 'vrf_add': [], - 'vrf_existing': [], - 'vrf_remove': [] -} - def _cmd(command): cmd(command, raising=ConfigError, message='Error changing VRF') @@ -81,112 +72,60 @@ def get_config(config=None): conf = config else: conf = Config() - vrf_config = deepcopy(default_config_data) - cfg_base = ['vrf'] - if not conf.exists(cfg_base): - # get all currently effetive VRFs and mark them for deletion - vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) - else: - # set configuration level base - conf.set_level(cfg_base) - - # Should services be allowed to bind to all VRFs? - if conf.exists(['bind-to-all']): - vrf_config['bind_to_all'] = '1' - - # Determine vrf interfaces (currently effective) - to determine which - # vrf interface is no longer present and needs to be removed - eff_vrf = conf.list_effective_nodes(['name']) - act_vrf = conf.list_nodes(['name']) - vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) - - # read in individual VRF definition and build up - # configuration - for name in conf.list_nodes(['name']): - vrf_inst = { - 'description' : '', - 'members': [], - 'name' : name, - 'table' : '', - 'table_mod': False - } - conf.set_level(cfg_base + ['name', name]) - - if conf.exists(['table']): - # VRF table can't be changed on demand, thus we need to read in the - # current and the effective routing table number - act_table = conf.return_value(['table']) - eff_table = conf.return_effective_value(['table']) - vrf_inst['table'] = act_table - if eff_table and eff_table != act_table: - vrf_inst['table_mod'] = True - - if conf.exists(['description']): - vrf_inst['description'] = conf.return_value(['description']) - - # append individual VRF configuration to global configuration list - vrf_config['vrf_add'].append(vrf_inst) - - # set configuration level base - conf.set_level(cfg_base) - - # check VRFs which need to be removed as they are not allowed to have - # interfaces attached - tmp = [] - for name in vrf_config['vrf_remove']: - vrf_inst = { - 'interfaces': [], - 'name': name, - 'routes': [] - } - - # find member interfaces of this particulat VRF - vrf_inst['interfaces'] = vrf_interfaces(conf, name) - - # find routing protocols used by this VRF - vrf_inst['routes'] = vrf_routing(conf, name) - - # append individual VRF configuration to temporary configuration list - tmp.append(vrf_inst) - - # replace values in vrf_remove with list of dictionaries - # as we need it in verify() - we can't delete a VRF with members attached - vrf_config['vrf_remove'] = tmp - return vrf_config - -def verify(vrf_config): - # ensure VRF is not assigned to any interface - for vrf in vrf_config['vrf_remove']: - if len(vrf['interfaces']) > 0: - raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!") - - if len(vrf['routes']) > 0: - raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + base = ['vrf'] + vrf = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - table_ids = [] - for vrf in vrf_config['vrf_add']: - # table id is mandatory - if not vrf['table']: - raise ConfigError(f"VRF {vrf['name']} table id is mandatory!") + # determine which VRF has been removed + tmp = node_changed(conf, base + ['name']) + for name in tmp: + vrf.update({'vrf_remove' : { name : ''}}) + # get VRF bound interfaces + interfaces = vrf_interfaces(conf, name) + if interfaces: vrf.update({'vrf_remove' : { name : {'interfaces' : ''}}}) + # get VRF bound routing instances + routes = vrf_routing(conf, name) + if routes: vrf.update({'vrf_remove' : { name : {'routes' : routes}}}) - # routing table id can't be changed - OS restriction - if vrf['table_mod']: - raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!") + return vrf - # VRf routing table ID must be unique on the system - if vrf['table'] in table_ids: - raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!") - - table_ids.append(vrf['table']) +def verify(vrf): + # ensure VRF is not assigned to any interface + if 'vrf_remove' in vrf: + for name, config in vrf['vrf_remove'].items(): + if 'interfaces' in config: + raise ConfigError(f'Can not remove VRF "{name}", it still has '\ + f'member interfaces!') + if 'routes' in config: + raise ConfigError(f'Can not remove VRF "{name}", it still has '\ + f'static routes installed!') + + if 'name' in vrf: + table_ids = [] + for name, config in vrf['name'].items(): + # table id is mandatory + if 'table' not in config: + raise ConfigError(f'VRF "{name}" table id is mandatory!') + + # routing table id can't be changed - OS restriction + if os.path.isdir(f'/sys/class/net/{name}'): + tmp = loads(cmd(f'ip -j -d link show {name}'))[0] + tmp = str(dict_search('linkinfo.info_data.table', tmp)) + if tmp and tmp != config['table']: + raise ConfigError(f'VRF "{name}" table id modification not possible!') + + # VRf routing table ID must be unique on the system + if config['table'] in table_ids: + raise ConfigError(f'VRF "{name}" table id is not unique!') + table_ids.append(config['table']) return None -def generate(vrf_config): - render(config_file, 'vrf/vrf.conf.tmpl', vrf_config) +def generate(vrf): + render(config_file, 'vrf/vrf.conf.tmpl', vrf) return None -def apply(vrf_config): +def apply(vrf): # Documentation # # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt @@ -196,40 +135,48 @@ def apply(vrf_config): # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf # set the default VRF global behaviour - bind_all = vrf_config['bind_to_all'] - if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all: - _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') - _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') - - for vrf in vrf_config['vrf_remove']: - name = vrf['name'] - if os.path.isdir(f'/sys/class/net/{name}'): - _cmd(f'ip -4 route del vrf {name} unreachable default metric 4278198272') - _cmd(f'ip -6 route del vrf {name} unreachable default metric 4278198272') - _cmd(f'ip link delete dev {name}') - - for vrf in vrf_config['vrf_add']: - name = vrf['name'] - table = vrf['table'] - - if not os.path.isdir(f'/sys/class/net/{name}'): - # For each VRF apart from your default context create a VRF - # interface with a separate routing table - _cmd(f'ip link add {name} type vrf table {table}') - # Start VRf - _cmd(f'ip link set dev {name} up') - # The kernel Documentation/networking/vrf.txt also recommends - # adding unreachable routes to the VRF routing tables so that routes - # afterwards are taken. - _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') - _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') - # We also should add proper loopback IP addresses to the newly - # created VRFs for services bound to the loopback address (SNMP, NTP) - _cmd(f'ip -4 addr add 127.0.0.1/8 dev {name}') - _cmd(f'ip -6 addr add ::1/128 dev {name}') - - # set VRF description for e.g. SNMP monitoring - Interface(name).set_alias(vrf['description']) + bind_all = '0' + if 'bind_to_all' in vrf: + bind_all = '1' + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + + for tmp in (dict_search('vrf_remove', vrf) or []): + if os.path.isdir(f'/sys/class/net/{tmp}'): + _cmd(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272') + _cmd(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272') + _cmd(f'ip link delete dev {tmp}') + + if 'name' in vrf: + for name, config in vrf['name'].items(): + table = config['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table + _cmd(f'ip link add {name} type vrf table {table}') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') + # We also should add proper loopback IP addresses to the newly + # created VRFs for services bound to the loopback address (SNMP, NTP) + _cmd(f'ip -4 addr add 127.0.0.1/8 dev {name}') + _cmd(f'ip -6 addr add ::1/128 dev {name}') + + # set VRF description for e.g. SNMP monitoring + vrf_if = Interface(name) + vrf_if.set_alias(config.get('description', '')) + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + vrf_if.set_admin_state(state) # Linux routing uses rules to find tables - routing targets are then # looked up in those tables. If the lookup got a matching route, the @@ -248,13 +195,13 @@ def apply(vrf_config): local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] # change preference when VRFs are enabled and local lookup table is default - if not local_pref and vrf_config['vrf_add']: + if not local_pref and 'name' in vrf: for af in ['-4', '-6']: _cmd(f'ip {af} rule add pref 32765 table local') _cmd(f'ip {af} rule del pref 0') # return to default lookup preference when no VRF is configured - if not vrf_config['vrf_add']: + if 'name' not in vrf: for af in ['-4', '-6']: _cmd(f'ip {af} rule add pref 0 table local') _cmd(f'ip {af} rule del pref 32765') |