diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 92 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/nat.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/ntp.py | 11 | ||||
-rwxr-xr-x | src/conf_mode/policy-local-route.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 42 | ||||
-rw-r--r-- | src/conf_mode/protocols_ospf.py | 103 | ||||
-rwxr-xr-x | src/conf_mode/ssh.py | 20 | ||||
-rwxr-xr-x | src/conf_mode/system-option.py | 14 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 241 |
11 files changed, 299 insertions, 249 deletions
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 7af3e3d7c..fd4ffed9a 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -41,26 +41,6 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -def helper_check_removed_vlan(conf,bridge,key,key_mangling): - key_update = re.sub(key_mangling[0], key_mangling[1], key) - if dict_search('member.interface', bridge): - for interface in bridge['member']['interface']: - tmp = leaf_node_changed(conf, ['member', 'interface',interface,key]) - if tmp: - if 'member' in bridge: - if 'interface' in bridge['member']: - if interface in bridge['member']['interface']: - bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp }) - else: - bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }}) - else: - bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}}) - else: - bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}}) - - return bridge - - def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -74,18 +54,12 @@ def get_config(config=None): bridge = get_interface_dict(conf, base) # determine which members have been removed - tmp = node_changed(conf, ['member', 'interface']) + tmp = node_changed(conf, ['member', 'interface'], key_mangling=('-', '_')) if tmp: if 'member' in bridge: bridge['member'].update({'interface_remove': tmp }) else: bridge.update({'member': {'interface_remove': tmp }}) - - - # determine which members vlan have been removed - - bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_')) - bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_')) if dict_search('member.interface', bridge): # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: @@ -99,7 +73,6 @@ def get_config(config=None): # the default dictionary is not properly paged into the dict (see T2665) # thus we will ammend it ourself default_member_values = defaults(base + ['member', 'interface']) - vlan_aware = False for interface,interface_config in bridge['member']['interface'].items(): bridge['member']['interface'][interface] = dict_merge( default_member_values, bridge['member']['interface'][interface]) @@ -120,19 +93,11 @@ def get_config(config=None): # Bridge members must not have an assigned address tmp = has_address_configured(conf, interface) if tmp: bridge['member']['interface'][interface].update({'has_address' : ''}) - + # VLAN-aware bridge members must not have VLAN interface configuration - if 'native_vlan' in interface_config: - vlan_aware = True - - if 'allowed_vlan' in interface_config: - vlan_aware = True - - - if vlan_aware: - tmp = has_vlan_subinterface_configured(conf,interface) - if tmp: - if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) + tmp = has_vlan_subinterface_configured(conf,interface) + if 'enable_vlan' in bridge and tmp: + bridge['member']['interface'][interface].update({'has_vlan' : ''}) return bridge @@ -142,8 +107,8 @@ def verify(bridge): verify_dhcpv6(bridge) verify_vrf(bridge) - - vlan_aware = False + + ifname = bridge['ifname'] if dict_search('member.interface', bridge): for interface, interface_config in bridge['member']['interface'].items(): @@ -166,31 +131,24 @@ def verify(bridge): if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') - - if 'has_vlan' in interface_config: - raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') - - # VLAN-aware bridge members must not have VLAN interface configuration - if 'native_vlan' in interface_config: - vlan_aware = True - - if 'allowed_vlan' in interface_config: - vlan_aware = True - - if vlan_aware and 'wlan' in interface: - raise ConfigError(error_msg + 'VLAN aware cannot be set!') - - if 'allowed_vlan' in interface_config: - for vlan in interface_config['allowed_vlan']: - if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan): - vlan_range = vlan.split('-') - if int(vlan_range[0]) <1 and int(vlan_range[0])>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') - if int(vlan_range[1]) <1 and int(vlan_range[1])>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') - else: - if int(vlan) <1 and int(vlan)>4094: - raise ConfigError('VLAN ID must be between 1 and 4094') + + if 'enable_vlan' in bridge: + if 'has_vlan' in interface_config: + raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + + if 'wlan' in interface: + raise ConfigError(error_msg + 'VLAN aware cannot be set!') + else: + for option in ['allowed_vlan', 'native_vlan']: + if option in interface_config: + raise ConfigError('Can not use VLAN options on non VLAN aware bridge') + + if 'enable_vlan' in bridge: + if dict_search('vif.1', bridge): + raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface') + else: + if dict_search('vif', bridge): + raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"') return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index e4a6a5ec1..ee6f05fcd 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -17,6 +17,7 @@ import os import re +from glob import glob from sys import exit from ipaddress import IPv4Address from ipaddress import IPv4Network @@ -488,14 +489,9 @@ def apply(openvpn): # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: - # cleanup old configuration files - cleanup = [] - cleanup.append(cfg_file.format(**openvpn)) - cleanup.append(openvpn['auth_user_pass_file']) - - for file in cleanup: - if os.path.isfile(file): - os.unlink(file) + for cleanup_file in glob(f'/run/openvpn/{interface}.*'): + if os.path.isfile(cleanup_file): + os.unlink(cleanup_file) if interface in interfaces(): VTunIf(interface).remove() diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 7cfc76aa0..3e6320f02 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -58,7 +58,7 @@ def get_config(config=None): # Determine which Wireguard peer has been removed. # Peers can only be removed with their public key! dict = {} - tmp = node_changed(conf, ['peer']) + tmp = node_changed(conf, ['peer'], key_mangling=('-', '_')) for peer in (tmp or []): pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey']) if pubkey: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 2d98cb11b..dae958774 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.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 @@ -26,6 +26,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render +from vyos.template import is_ip_network from vyos.util import cmd from vyos.util import check_kmod from vyos.util import dict_search @@ -68,9 +69,9 @@ def verify_rule(config, err_msg): 'ports can only be specified when protocol is '\ 'either tcp, udp or tcp_udp!') - if '/' in (dict_search('translation.address', config) or []): + if is_ip_network(dict_search('translation.address', config)): raise ConfigError(f'{err_msg}\n' \ - 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'Cannot use ports with an IPv4 network as translation address as it\n' \ 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') @@ -147,7 +148,7 @@ def verify(nat): addr = dict_search('translation.address', config) if addr != None: - if addr != 'masquerade': + if addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): if not is_addr_assigned(ip): print(f'WARNING: IP address {ip} does not exist on the system!') diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index b102b3e9e..52070aabc 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -24,7 +24,7 @@ from vyos.template import render from vyos import airbag airbag.enable() -config_file = r'/etc/ntp.conf' +config_file = r'/run/ntpd/ntpd.conf' systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf' def get_config(config=None): @@ -33,8 +33,11 @@ def get_config(config=None): else: conf = Config() base = ['system', 'ntp'] + if not conf.exists(base): + return None ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + ntp['config_file'] = config_file return ntp def verify(ntp): @@ -42,7 +45,7 @@ def verify(ntp): if not ntp: return None - if len(ntp.get('allow_clients', {})) and not (len(ntp.get('server', {})) > 0): + if 'allow_clients' in ntp and 'server' not in ntp: raise ConfigError('NTP server not configured') verify_vrf(ntp) @@ -53,7 +56,7 @@ def generate(ntp): if not ntp: return None - render(config_file, 'ntp/ntp.conf.tmpl', ntp) + render(config_file, 'ntp/ntpd.conf.tmpl', ntp) render(systemd_override, 'ntp/override.conf.tmpl', ntp) return None diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py index c4024dce4..013f22665 100755 --- a/src/conf_mode/policy-local-route.py +++ b/src/conf_mode/policy-local-route.py @@ -40,7 +40,7 @@ def get_config(config=None): # delete policy local-route dict = {} - tmp = node_changed(conf, ['policy', 'local-route', 'rule']) + tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_')) if tmp: for rule in (tmp or []): src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index f8e34285e..39d367b97 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os + from sys import exit from vyos.config import Config @@ -29,6 +31,14 @@ airbag.enable() config_file = r'/tmp/bgp.frr' +DEBUG = os.path.exists('/tmp/bgp.debug') +if DEBUG: + import logging + lg = logging.getLogger("vyos.frr") + lg.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + lg.addHandler(ch) + def get_config(): conf = Config() base = ['protocols', 'bgp'] @@ -78,8 +88,13 @@ def verify(bgp): if neighbor == 'neighbor': # remote-as must be either set explicitly for the neighbor # or for the entire peer-group - if 'remote_as' not in peer_config: - if 'peer_group' not in peer_config or 'remote_as' not in asn_config['peer_group'][peer_config['peer_group']]: + if 'interface' in peer_config: + if 'remote_as' not in peer_config['interface']: + if 'peer_group' not in peer_config['interface'] or 'remote_as' not in asn_config['peer_group'][ peer_config['interface']['peer_group'] ]: + raise ConfigError('Remote AS must be set for neighbor or peer-group!') + + elif 'remote_as' not in peer_config: + if 'peer_group' not in peer_config or 'remote_as' not in asn_config['peer_group'][ peer_config['peer_group'] ]: raise ConfigError('Remote AS must be set for neighbor or peer-group!') for afi in ['ipv4_unicast', 'ipv6_unicast']: @@ -144,6 +159,21 @@ def apply(bgp): frr_cfg.load_configuration(daemon='bgpd') frr_cfg.modify_section(f'router bgp \S+', '') frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['new_frr_config']) + + # Debugging + if DEBUG: + from pprint import pprint + print('') + print('--------- DEBUGGING ----------') + pprint(dir(frr_cfg)) + print('Existing config:\n') + for line in frr_cfg.original_config: + print(line) + print(f'Replacement config:\n') + print(f'{bgp["new_frr_config"]}') + print(f'Modified config:\n') + print(f'{frr_cfg}') + frr_cfg.commit_configuration(daemon='bgpd') # If FRR config is blank, rerun the blank commit x times due to frr-reload @@ -152,14 +182,6 @@ def apply(bgp): for a in range(5): frr_cfg.commit_configuration(daemon='bgpd') - # Debugging - ''' - print('') - print('--------- DEBUGGING ----------') - print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') - print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n') - print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') - ''' return None diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py new file mode 100644 index 000000000..73c244571 --- /dev/null +++ b/src/conf_mode/protocols_ospf.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.template import render_to_string +from vyos.util import call +from vyos.util import dict_search +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ospf.frr' + +DEBUG = os.path.exists('/tmp/ospf.debug') +if DEBUG: + import logging + lg = logging.getLogger("vyos.frr") + lg.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + lg.addHandler(ch) + +def get_config(): + conf = Config() + base = ['protocols', 'ospf'] + ospf = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return ospf + +def verify(ospf): + if not ospf: + return None + + return None + +def generate(ospf): + if not ospf: + ospf['new_frr_config'] = '' + return None + + # render(config) not needed, its only for debug + render(config_file, 'frr/ospf.frr.tmpl', ospf) + ospf['new_frr_config'] = render_to_string('frr/ospf.frr.tmpl', ospf) + + return None + +def apply(ospf): + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(daemon='ospfd') + frr_cfg.modify_section(f'router ospf', '') + + # Debugging + if DEBUG: + from pprint import pprint + print('') + print('--------- DEBUGGING ----------') + pprint(dir(frr_cfg)) + print('Existing config:\n') + for line in frr_cfg.original_config: + print(line) + print(f'Replacement config:\n') + print(f'{ospf["new_frr_config"]}') + print(f'Modified config:\n') + print(f'{frr_cfg}') + + frr_cfg.commit_configuration(daemon='ospfd') + + # If FRR config is blank, rerun the blank commit x times due to frr-reload + # behavior/bug not properly clearing out on one commit. + if ospf['new_frr_config'] == '': + for a in range(5): + frr_cfg.commit_configuration(daemon='ospfd') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 8eeb0a7c1..67724b043 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-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,6 +17,8 @@ import os from sys import exit +from syslog import syslog +from syslog import LOG_INFO from vyos.config import Config from vyos.configdict import dict_merge @@ -31,6 +33,10 @@ airbag.enable() config_file = r'/run/sshd/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' +key_rsa = '/etc/ssh/ssh_host_rsa_key' +key_dsa = '/etc/ssh/ssh_host_dsa_key' +key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' + def get_config(config=None): if config: conf = config @@ -66,6 +72,18 @@ def generate(ssh): return None + # This usually happens only once on a fresh system, SSH keys need to be + # freshly generted, one per every system! + if not os.path.isfile(key_rsa): + syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}') + if not os.path.isfile(key_dsa): + syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}') + if not os.path.isfile(key_ed25519): + syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!') + call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') + render(config_file, 'ssh/sshd_config.tmpl', ssh) render(systemd_override, 'ssh/override.conf.tmpl', ssh) # Reload systemd manager configuration diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 910c14474..454611c55 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -87,10 +87,10 @@ def apply(options): # Ctrl-Alt-Delete action if os.path.exists(systemd_action_file): os.unlink(systemd_action_file) - if 'ctrl_alt_del' in options: - if options['ctrl_alt_del'] == 'reboot': + if 'ctrl_alt_delete' in options: + if options['ctrl_alt_delete'] == 'reboot': os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) - elif options['ctrl_alt_del'] == 'poweroff': + elif options['ctrl_alt_delete'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) # Configure HTTP client @@ -104,11 +104,11 @@ def apply(options): os.unlink(ssh_config) # Reboot system on kernel panic + timeout = '0' + if 'reboot_on_panic' in options: + timeout = '60' with open('/proc/sys/kernel/panic', 'w') as f: - if 'reboot_on_panic' in options: - f.write('60') - else: - f.write('0') + f.write(timeout) # tuned - performance tuning if 'performance' in options: diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c4ba859b7..6c6e219a5 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,62 @@ 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!") + base = ['vrf'] + vrf = conf.get_config_dict(base, get_first_key=True) - if len(vrf['routes']) > 0: - raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + # determine which VRF has been removed + for name in node_changed(conf, base + ['name']): + if 'vrf_remove' not in vrf: + vrf.update({'vrf_remove' : {}}) - 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!") + vrf['vrf_remove'][name] = {} + # get VRF bound interfaces + interfaces = vrf_interfaces(conf, name) + if interfaces: vrf['vrf_remove'][name]['interface'] = interfaces + # get VRF bound routing instances + routes = vrf_routing(conf, name) + if routes: vrf['vrf_remove'][name]['route'] = 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 'interface' in config: + raise ConfigError(f'Can not remove VRF "{name}", it still has '\ + f'member interfaces!') + if 'route' 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 +137,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 +197,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') |