From ce3b9dda7d832e5316657460518503ce72745285 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 18 Dec 2024 19:15:10 +0100 Subject: [PATCH 1/5] frrender: T6746: support dynamic enable/disable of debug output Always evaluate if the debug file exsits and not once during module init. Thus we can always eanble/disable FRR command debugging during runtime even under vyos-configd. --- python/vyos/frrender.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index 0c9dde315f..f8d18f207c 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -28,10 +28,8 @@ from vyos.template import render_to_string from vyos import ConfigError -DEBUG_ON = os.path.exists(frr_debug_enable) - def debug(message): - if not DEBUG_ON: + if not os.path.exists(frr_debug_enable): return print(message) @@ -124,7 +122,7 @@ def inline_helper(config_dict) -> str: output += '\n' return output - debug('======< RENDERING CONFIG >======') + debug('FRR: START CONFIGURATION RENDERING') # we can not reload an empty file, thus we always embed the marker output = '!\n' # Enable SNMP agentx support @@ -155,23 +153,25 @@ def inline_helper(config_dict) -> str: raise ConfigError('FRR configuration contains "!!" which is not allowed') debug(output) - debug('======< RENDERING CONFIG COMPLETE >======') write_file(self._frr_conf, output) + debug('FRR: RENDERING CONFIG COMPLETE') + return None def apply(self, count_max=5): count = 0 emsg = '' while count < count_max: count += 1 - debug(f'FRR: Reloading configuration - tries: {count} | Python class ID: {id(self)}') + debug(f'FRR: reloading configuration - tries: {count} | Python class ID: {id(self)}') cmdline = '/usr/lib/frr/frr-reload.py --reload' - if DEBUG_ON: cmdline += ' --debug' + if os.path.exists(frr_debug_enable): + cmdline += ' --debug' rc, emsg = rc_cmd(f'{cmdline} {self._frr_conf}') if rc != 0: sleep(2) continue debug(emsg) - debug('======< DONE APPLYING CONFIG >======') + debug('FRR: configuration reload complete') break if count >= count_max: From 0b46e61379d2aa74f8b6e87dc94e801d0863e1ab Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 18 Dec 2024 19:16:57 +0100 Subject: [PATCH 2/5] frrender: T6746: rename config -> config_dict --- python/vyos/frrender.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index f8d18f207c..ccadb3d1ec 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -56,9 +56,9 @@ class FRRender: def __init__(self): self._frr_conf = '/run/frr/config/vyos.frr.conf' - def generate(self, config): - if not isinstance(config, dict): - tmp = type(config) + def generate(self, config_dict) -> None: + if not isinstance(config_dict, dict): + tmp = type(config_dict) raise ValueError(f'Config must be of type "dict" and not "{tmp}"!') def inline_helper(config_dict) -> str: @@ -127,18 +127,18 @@ def inline_helper(config_dict) -> str: output = '!\n' # Enable SNMP agentx support # SNMP AgentX support cannot be disabled once enabled - if 'snmp' in config: + if 'snmp' in config_dict: output += 'agentx\n' # Add routing protocols in global VRF - output += inline_helper(config) + output += inline_helper(config_dict) # Interface configuration for EVPN is not VRF related - if 'interfaces' in config: - output += render_to_string('frr/evpn.mh.frr.j2', {'interfaces' : config['interfaces']}) + if 'interfaces' in config_dict: + output += render_to_string('frr/evpn.mh.frr.j2', {'interfaces' : config_dict['interfaces']}) output += '\n' - if 'vrf' in config and 'name' in config['vrf']: - output += render_to_string('frr/zebra.vrf.route-map.frr.j2', config['vrf']) - for vrf, vrf_config in config['vrf']['name'].items(): + if 'vrf' in config_dict and 'name' in config_dict['vrf']: + output += render_to_string('frr/zebra.vrf.route-map.frr.j2', config_dict['vrf']) + for vrf, vrf_config in config_dict['vrf']['name'].items(): if 'protocols' not in vrf_config: continue for protocol in vrf_config['protocols']: From 75de2fa91c7d727eef9d38d045e0e04fba218695 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 18 Dec 2024 19:17:33 +0100 Subject: [PATCH 3/5] vyos-configd: T6746: render resulting FRR configuration only once Previously the FRR configuration was rendered via Jinja2 on every call to get_frrendet_dict() - but it was used/applied only once. We save the CPU cycles and render the FRR configuration only once prior to applying it in FRR. --- python/vyos/configdict.py | 13 ------------- src/services/vyos-configd | 3 +++ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index cbcbf9f721..17a5fd7bb7 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -19,7 +19,6 @@ import os import json -from vyos.defaults import frr_debug_enable from vyos.utils.dict import dict_search from vyos.utils.process import cmd @@ -1146,16 +1145,4 @@ def dict_helper_pim_defaults(pim, path): dict.update({'vrf' : vrf}) - if os.path.exists(frr_debug_enable): - print('======== < BEGIN > ==========') - import pprint - pprint.pprint(dict) - print('========= < END > ===========') - - # Use singleton instance of the FRR render class - if hasattr(conf, 'frrender_cls'): - frrender = getattr(conf, 'frrender_cls') - dict.update({'frrender_cls' : frrender}) - frrender.generate(dict) - return dict diff --git a/src/services/vyos-configd b/src/services/vyos-configd index ecad85801b..500d5425ff 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -33,6 +33,7 @@ import zmq from vyos.defaults import directories from vyos.utils.boot import boot_configuration_complete +from vyos.configdict import get_frrender_dict from vyos.configsource import ConfigSourceString from vyos.configsource import ConfigSourceError from vyos.configdiff import get_commit_scripts @@ -333,6 +334,8 @@ if __name__ == '__main__': if hasattr(config, 'frrender_cls') and res == R_SUCCESS: frrender_cls = getattr(config, 'frrender_cls') + tmp = get_frrender_dict(config) + frrender_cls.generate(tmp) frrender_cls.apply() else: logger.critical(f'Unexpected message: {message}') From 77628e49379066e07b4046a00007187263e7206b Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 18 Dec 2024 19:25:20 +0100 Subject: [PATCH 4/5] frrender: T6746: move get_frrender_dict from vyos.configdict to this module Keep all FRRender stuff in one place. --- python/vyos/configdict.py | 482 -------------------- python/vyos/frrender.py | 484 +++++++++++++++++++++ src/conf_mode/interfaces_bonding.py | 2 +- src/conf_mode/interfaces_ethernet.py | 2 +- src/conf_mode/policy.py | 2 +- src/conf_mode/protocols_babel.py | 2 +- src/conf_mode/protocols_bfd.py | 2 +- src/conf_mode/protocols_bgp.py | 2 +- src/conf_mode/protocols_eigrp.py | 2 +- src/conf_mode/protocols_isis.py | 2 +- src/conf_mode/protocols_mpls.py | 2 +- src/conf_mode/protocols_openfabric.py | 2 +- src/conf_mode/protocols_ospf.py | 2 +- src/conf_mode/protocols_ospfv3.py | 2 +- src/conf_mode/protocols_pim.py | 2 +- src/conf_mode/protocols_pim6.py | 2 +- src/conf_mode/protocols_rip.py | 2 +- src/conf_mode/protocols_ripng.py | 2 +- src/conf_mode/protocols_rpki.py | 2 +- src/conf_mode/protocols_segment-routing.py | 2 +- src/conf_mode/protocols_static.py | 2 +- src/conf_mode/system_ip.py | 2 +- src/conf_mode/system_ipv6.py | 2 +- src/conf_mode/vrf.py | 2 +- src/services/vyos-configd | 2 +- 25 files changed, 507 insertions(+), 505 deletions(-) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 17a5fd7bb7..5a353b1106 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -664,485 +664,3 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False): dict['authentication']['radius']['server'][server]['acct_port'] = '0' return dict - -def get_frrender_dict(conf, argv=None) -> dict: - from copy import deepcopy - from vyos.config import config_dict_merge - from vyos.frrender import frr_protocols - - # Create an empty dictionary which will be filled down the code path and - # returned to the caller - dict = {} - - if argv and len(argv) > 1: - dict['vrf_context'] = argv[1] - - def dict_helper_ospf_defaults(ospf, path): - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), - get_first_key=True, recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: default-information - # originate comes with a default metric-type of 2, which will enable the - # entire default-information originate tree, even when not set via CLI so we - # need to check this first and probably drop that key. - if dict_search('default_information.originate', ospf) is None: - del default_values['default_information'] - if 'mpls_te' not in ospf: - del default_values['mpls_te'] - if 'graceful_restart' not in ospf: - del default_values['graceful_restart'] - for area_num in default_values.get('area', []): - if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: - del default_values['area'][area_num]['area_type']['nssa'] - - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: - if dict_search(f'redistribute.{protocol}', ospf) is None: - del default_values['redistribute'][protocol] - if not bool(default_values['redistribute']): - del default_values['redistribute'] - - for interface in ospf.get('interface', []): - # We need to reload the defaults on every pass b/c of - # hello-multiplier dependency on dead-interval - # If hello-multiplier is set, we need to remove the default from - # dead-interval. - if 'hello_multiplier' in ospf['interface'][interface]: - del default_values['interface'][interface]['dead_interval'] - - ospf = config_dict_merge(default_values, ospf) - return ospf - - def dict_helper_ospfv3_defaults(ospfv3, path): - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), - get_first_key=True, recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: default-information - # originate comes with a default metric-type of 2, which will enable the - # entire default-information originate tree, even when not set via CLI so we - # need to check this first and probably drop that key. - if dict_search('default_information.originate', ospfv3) is None: - del default_values['default_information'] - if 'graceful_restart' not in ospfv3: - del default_values['graceful_restart'] - - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']: - if dict_search(f'redistribute.{protocol}', ospfv3) is None: - del default_values['redistribute'][protocol] - if not bool(default_values['redistribute']): - del default_values['redistribute'] - - default_values.pop('interface', {}) - - # merge in remaining default values - ospfv3 = config_dict_merge(default_values, ospfv3) - return ospfv3 - - def dict_helper_pim_defaults(pim, path): - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), - get_first_key=True, recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. - for interface in pim.get('interface', []): - if 'igmp' not in pim['interface'][interface]: - del default_values['interface'][interface]['igmp'] - - pim = config_dict_merge(default_values, pim) - return pim - - # Ethernet and bonding interfaces can participate in EVPN which is configured via FRR - tmp = {} - for if_type in ['ethernet', 'bonding']: - interface_path = ['interfaces', if_type] - if not conf.exists(interface_path): - continue - for interface in conf.list_nodes(interface_path): - evpn_path = interface_path + [interface, 'evpn'] - if not conf.exists(evpn_path): - continue - - evpn = conf.get_config_dict(evpn_path, key_mangling=('-', '_')) - tmp.update({interface : evpn}) - # At least one participating EVPN interface found, add to result dict - if tmp: dict['interfaces'] = tmp - - # Zebra prefix exchange for Kernel IP/IPv6 and routing protocols - for ip_version in ['ip', 'ipv6']: - ip_cli_path = ['system', ip_version] - ip_dict = conf.get_config_dict(ip_cli_path, key_mangling=('-', '_'), - get_first_key=True, with_recursive_defaults=True) - if ip_dict: - ip_dict['afi'] = ip_version - dict.update({ip_version : ip_dict}) - - # Enable SNMP agentx support - # SNMP AgentX support cannot be disabled once enabled - if conf.exists(['service', 'snmp']): - dict['snmp'] = {} - - # We will always need the policy key - dict['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # We need to check the CLI if the BABEL node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - babel_cli_path = ['protocols', 'babel'] - if conf.exists(babel_cli_path): - babel = conf.get_config_dict(babel_cli_path, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - dict.update({'babel' : babel}) - - # We need to check the CLI if the BFD node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - bfd_cli_path = ['protocols', 'bfd'] - if conf.exists(bfd_cli_path): - bfd = conf.get_config_dict(bfd_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - dict.update({'bfd' : bfd}) - - # We need to check the CLI if the BGP node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - bgp_cli_path = ['protocols', 'bgp'] - if conf.exists(bgp_cli_path): - bgp = conf.get_config_dict(bgp_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - bgp['dependent_vrfs'] = {} - dict.update({'bgp' : bgp}) - elif conf.exists_effective(bgp_cli_path): - dict.update({'bgp' : {'deleted' : '', 'dependent_vrfs' : {}}}) - - # We need to check the CLI if the EIGRP node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - eigrp_cli_path = ['protocols', 'eigrp'] - if conf.exists(eigrp_cli_path): - isis = conf.get_config_dict(eigrp_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - dict.update({'eigrp' : isis}) - elif conf.exists_effective(eigrp_cli_path): - dict.update({'eigrp' : {'deleted' : ''}}) - - # We need to check the CLI if the ISIS node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - isis_cli_path = ['protocols', 'isis'] - if conf.exists(isis_cli_path): - isis = conf.get_config_dict(isis_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - dict.update({'isis' : isis}) - elif conf.exists_effective(isis_cli_path): - dict.update({'isis' : {'deleted' : ''}}) - - # We need to check the CLI if the MPLS node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - mpls_cli_path = ['protocols', 'mpls'] - if conf.exists(mpls_cli_path): - mpls = conf.get_config_dict(mpls_cli_path, key_mangling=('-', '_'), - get_first_key=True) - dict.update({'mpls' : mpls}) - elif conf.exists_effective(mpls_cli_path): - dict.update({'mpls' : {'deleted' : ''}}) - - # We need to check the CLI if the OPENFABRIC node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - openfabric_cli_path = ['protocols', 'openfabric'] - if conf.exists(openfabric_cli_path): - openfabric = conf.get_config_dict(openfabric_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - dict.update({'openfabric' : openfabric}) - elif conf.exists_effective(openfabric_cli_path): - dict.update({'openfabric' : {'deleted' : ''}}) - - # We need to check the CLI if the OSPF node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - ospf_cli_path = ['protocols', 'ospf'] - if conf.exists(ospf_cli_path): - ospf = conf.get_config_dict(ospf_cli_path, key_mangling=('-', '_'), - get_first_key=True) - ospf = dict_helper_ospf_defaults(ospf, ospf_cli_path) - dict.update({'ospf' : ospf}) - elif conf.exists_effective(ospf_cli_path): - dict.update({'ospf' : {'deleted' : ''}}) - - # We need to check the CLI if the OSPFv3 node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - ospfv3_cli_path = ['protocols', 'ospfv3'] - if conf.exists(ospfv3_cli_path): - ospfv3 = conf.get_config_dict(ospfv3_cli_path, key_mangling=('-', '_'), - get_first_key=True) - ospfv3 = dict_helper_ospfv3_defaults(ospfv3, ospfv3_cli_path) - dict.update({'ospfv3' : ospfv3}) - elif conf.exists_effective(ospfv3_cli_path): - dict.update({'ospfv3' : {'deleted' : ''}}) - - # We need to check the CLI if the PIM node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - pim_cli_path = ['protocols', 'pim'] - if conf.exists(pim_cli_path): - pim = conf.get_config_dict(pim_cli_path, key_mangling=('-', '_'), - get_first_key=True) - pim = dict_helper_pim_defaults(pim, pim_cli_path) - dict.update({'pim' : pim}) - elif conf.exists_effective(pim_cli_path): - dict.update({'pim' : {'deleted' : ''}}) - - # We need to check the CLI if the PIM6 node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - pim6_cli_path = ['protocols', 'pim6'] - if conf.exists(pim6_cli_path): - pim6 = conf.get_config_dict(pim6_cli_path, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - dict.update({'pim6' : pim6}) - elif conf.exists_effective(pim6_cli_path): - dict.update({'pim6' : {'deleted' : ''}}) - - # We need to check the CLI if the RIP node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - rip_cli_path = ['protocols', 'rip'] - if conf.exists(rip_cli_path): - rip = conf.get_config_dict(rip_cli_path, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - dict.update({'rip' : rip}) - elif conf.exists_effective(rip_cli_path): - dict.update({'rip' : {'deleted' : ''}}) - - # We need to check the CLI if the RIPng node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - ripng_cli_path = ['protocols', 'ripng'] - if conf.exists(ripng_cli_path): - ripng = conf.get_config_dict(ripng_cli_path, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - dict.update({'ripng' : ripng}) - elif conf.exists_effective(ripng_cli_path): - dict.update({'ripng' : {'deleted' : ''}}) - - # We need to check the CLI if the RPKI node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - rpki_cli_path = ['protocols', 'rpki'] - if conf.exists(rpki_cli_path): - rpki = conf.get_config_dict(rpki_cli_path, key_mangling=('-', '_'), - get_first_key=True, with_pki=True, - with_recursive_defaults=True) - rpki_ssh_key_base = '/run/frr/id_rpki' - for cache, cache_config in rpki.get('cache',{}).items(): - if 'ssh' in cache_config: - cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub' - cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}' - dict.update({'rpki' : rpki}) - elif conf.exists_effective(rpki_cli_path): - dict.update({'rpki' : {'deleted' : ''}}) - - # We need to check the CLI if the Segment Routing node is present and thus load in - # all the default values present on the CLI - that's why we have if conf.exists() - sr_cli_path = ['protocols', 'segment-routing'] - if conf.exists(sr_cli_path): - sr = conf.get_config_dict(sr_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) - dict.update({'segment_routing' : sr}) - elif conf.exists_effective(sr_cli_path): - dict.update({'segment_routing' : {'deleted' : ''}}) - - # We need to check the CLI if the static node is present and thus load in - # all the default values present on the CLI - that's why we have if conf.exists() - static_cli_path = ['protocols', 'static'] - if conf.exists(static_cli_path): - static = conf.get_config_dict(static_cli_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # T3680 - get a list of all interfaces currently configured to use DHCP - tmp = get_dhcp_interfaces(conf) - if tmp: static.update({'dhcp' : tmp}) - tmp = get_pppoe_interfaces(conf) - if tmp: static.update({'pppoe' : tmp}) - - dict.update({'static' : static}) - elif conf.exists_effective(static_cli_path): - dict.update({'static' : {'deleted' : ''}}) - - # keep a re-usable list of dependent VRFs - dependent_vrfs_default = {} - if 'bgp' in dict: - dependent_vrfs_default = deepcopy(dict['bgp']) - # we do not need to nest the 'dependent_vrfs' key - simply remove it - if 'dependent_vrfs' in dependent_vrfs_default: - del dependent_vrfs_default['dependent_vrfs'] - - vrf_cli_path = ['vrf', 'name'] - if conf.exists(vrf_cli_path): - vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'), - get_first_key=False, - no_tag_node_value_mangle=True) - # We do not have any VRF related default values on the CLI. The defaults will only - # come into place under the protocols tree, thus we can safely merge them with the - # appropriate routing protocols - for vrf_name, vrf_config in vrf['name'].items(): - bgp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'bgp'] - if 'bgp' in vrf_config.get('protocols', []): - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(bgp_vrf_path, key_mangling=('-', '_'), - get_first_key=True, recursive=True) - - # merge in remaining default values - vrf_config['protocols']['bgp'] = config_dict_merge(default_values, - vrf_config['protocols']['bgp']) - - # Add this BGP VRF instance as dependency into the default VRF - if 'bgp' in dict: - dict['bgp']['dependent_vrfs'].update({vrf_name : deepcopy(vrf_config)}) - - vrf_config['protocols']['bgp']['dependent_vrfs'] = conf.get_config_dict( - vrf_cli_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - # We can safely delete ourself from the dependent VRF list - if vrf_name in vrf_config['protocols']['bgp']['dependent_vrfs']: - del vrf_config['protocols']['bgp']['dependent_vrfs'][vrf_name] - - # Add dependency on possible existing default VRF to this VRF - if 'bgp' in dict: - vrf_config['protocols']['bgp']['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': dependent_vrfs_default}}}) - elif conf.exists_effective(bgp_vrf_path): - # Add this BGP VRF instance as dependency into the default VRF - tmp = {'deleted' : '', 'dependent_vrfs': deepcopy(vrf['name'])} - # We can safely delete ourself from the dependent VRF list - if vrf_name in tmp['dependent_vrfs']: - del tmp['dependent_vrfs'][vrf_name] - - # Add dependency on possible existing default VRF to this VRF - if 'bgp' in dict: - tmp['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': dependent_vrfs_default}}}) - - if 'bgp' in dict: - dict['bgp']['dependent_vrfs'].update({vrf_name : {'protocols': tmp} }) - - if 'protocols' not in vrf['name'][vrf_name]: - vrf['name'][vrf_name].update({'protocols': {'bgp' : tmp}}) - else: - vrf['name'][vrf_name]['protocols'].update({'bgp' : tmp}) - - # We need to check the CLI if the EIGRP node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - eigrp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'eigrp'] - if 'eigrp' in vrf_config.get('protocols', []): - eigrp = conf.get_config_dict(eigrp_vrf_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - vrf['name'][vrf_name]['protocols'].update({'eigrp' : isis}) - elif conf.exists_effective(eigrp_vrf_path): - vrf['name'][vrf_name]['protocols'].update({'eigrp' : {'deleted' : ''}}) - - # We need to check the CLI if the ISIS node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - isis_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'isis'] - if 'isis' in vrf_config.get('protocols', []): - isis = conf.get_config_dict(isis_vrf_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True, with_recursive_defaults=True) - vrf['name'][vrf_name]['protocols'].update({'isis' : isis}) - elif conf.exists_effective(isis_vrf_path): - vrf['name'][vrf_name]['protocols'].update({'isis' : {'deleted' : ''}}) - - # We need to check the CLI if the OSPF node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - ospf_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospf'] - if 'ospf' in vrf_config.get('protocols', []): - ospf = conf.get_config_dict(ospf_vrf_path, key_mangling=('-', '_'), get_first_key=True) - ospf = dict_helper_ospf_defaults(vrf_config['protocols']['ospf'], ospf_vrf_path) - vrf['name'][vrf_name]['protocols'].update({'ospf' : ospf}) - elif conf.exists_effective(ospf_vrf_path): - vrf['name'][vrf_name]['protocols'].update({'ospf' : {'deleted' : ''}}) - - # We need to check the CLI if the OSPFv3 node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - ospfv3_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospfv3'] - if 'ospfv3' in vrf_config.get('protocols', []): - ospfv3 = conf.get_config_dict(ospfv3_vrf_path, key_mangling=('-', '_'), get_first_key=True) - ospfv3 = dict_helper_ospfv3_defaults(vrf_config['protocols']['ospfv3'], ospfv3_vrf_path) - vrf['name'][vrf_name]['protocols'].update({'ospfv3' : ospfv3}) - elif conf.exists_effective(ospfv3_vrf_path): - vrf['name'][vrf_name]['protocols'].update({'ospfv3' : {'deleted' : ''}}) - - # We need to check the CLI if the static node is present and thus load in all the default - # values present on the CLI - that's why we have if conf.exists() - static_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'static'] - if 'static' in vrf_config.get('protocols', []): - static = conf.get_config_dict(static_vrf_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - # T3680 - get a list of all interfaces currently configured to use DHCP - tmp = get_dhcp_interfaces(conf, vrf_name) - if tmp: static.update({'dhcp' : tmp}) - tmp = get_pppoe_interfaces(conf, vrf_name) - if tmp: static.update({'pppoe' : tmp}) - - vrf['name'][vrf_name]['protocols'].update({'static': static}) - elif conf.exists_effective(static_vrf_path): - vrf['name'][vrf_name]['protocols'].update({'static': {'deleted' : ''}}) - - vrf_vni_path = ['vrf', 'name', vrf_name, 'vni'] - if conf.exists(vrf_vni_path): - vrf_config.update({'vni': conf.return_value(vrf_vni_path)}) - - dict.update({'vrf' : vrf}) - elif conf.exists_effective(vrf_cli_path): - effective_vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'), - get_first_key=False, - no_tag_node_value_mangle=True, - effective=True) - vrf = {'name' : {}} - for vrf_name, vrf_config in effective_vrf.get('name', {}).items(): - vrf['name'].update({vrf_name : {}}) - for protocol in frr_protocols: - if protocol in vrf_config.get('protocols', []): - # Create initial protocols key if not present - if 'protocols' not in vrf['name'][vrf_name]: - vrf['name'][vrf_name].update({'protocols' : {}}) - # All routing protocols are deleted when we pass this point - tmp = {'deleted' : ''} - - # Special treatment for BGP routing protocol - if protocol == 'bgp': - tmp['dependent_vrfs'] = {} - if 'name' in vrf: - tmp['dependent_vrfs'] = conf.get_config_dict( - vrf_cli_path, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True, - effective=True) - # Add dependency on possible existing default VRF to this VRF - if 'bgp' in dict: - tmp['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': dependent_vrfs_default}}}) - # We can safely delete ourself from the dependent VRF list - if vrf_name in tmp['dependent_vrfs']: - del tmp['dependent_vrfs'][vrf_name] - - # Update VRF related dict - vrf['name'][vrf_name]['protocols'].update({protocol : tmp}) - - dict.update({'vrf' : vrf}) - - return dict diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index ccadb3d1ec..badc5d59f8 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -22,6 +22,7 @@ from time import sleep from vyos.defaults import frr_debug_enable +from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.process import cmd from vyos.utils.process import rc_cmd @@ -52,6 +53,489 @@ def debug(message): ripng_daemon = 'ripngd' zebra_daemon = 'zebra' +def get_frrender_dict(conf, argv=None) -> dict: + from copy import deepcopy + from vyos.config import config_dict_merge + from vyos.configdict import get_dhcp_interfaces + from vyos.configdict import get_pppoe_interfaces + + # Create an empty dictionary which will be filled down the code path and + # returned to the caller + dict = {} + + if argv and len(argv) > 1: + dict['vrf_context'] = argv[1] + + def dict_helper_ospf_defaults(ospf, path): + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), + get_first_key=True, recursive=True) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: default-information + # originate comes with a default metric-type of 2, which will enable the + # entire default-information originate tree, even when not set via CLI so we + # need to check this first and probably drop that key. + if dict_search('default_information.originate', ospf) is None: + del default_values['default_information'] + if 'mpls_te' not in ospf: + del default_values['mpls_te'] + if 'graceful_restart' not in ospf: + del default_values['graceful_restart'] + for area_num in default_values.get('area', []): + if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: + del default_values['area'][area_num]['area_type']['nssa'] + + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: + if dict_search(f'redistribute.{protocol}', ospf) is None: + del default_values['redistribute'][protocol] + if not bool(default_values['redistribute']): + del default_values['redistribute'] + + for interface in ospf.get('interface', []): + # We need to reload the defaults on every pass b/c of + # hello-multiplier dependency on dead-interval + # If hello-multiplier is set, we need to remove the default from + # dead-interval. + if 'hello_multiplier' in ospf['interface'][interface]: + del default_values['interface'][interface]['dead_interval'] + + ospf = config_dict_merge(default_values, ospf) + return ospf + + def dict_helper_ospfv3_defaults(ospfv3, path): + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), + get_first_key=True, recursive=True) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: default-information + # originate comes with a default metric-type of 2, which will enable the + # entire default-information originate tree, even when not set via CLI so we + # need to check this first and probably drop that key. + if dict_search('default_information.originate', ospfv3) is None: + del default_values['default_information'] + if 'graceful_restart' not in ospfv3: + del default_values['graceful_restart'] + + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']: + if dict_search(f'redistribute.{protocol}', ospfv3) is None: + del default_values['redistribute'][protocol] + if not bool(default_values['redistribute']): + del default_values['redistribute'] + + default_values.pop('interface', {}) + + # merge in remaining default values + ospfv3 = config_dict_merge(default_values, ospfv3) + return ospfv3 + + def dict_helper_pim_defaults(pim, path): + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(path, key_mangling=('-', '_'), + get_first_key=True, recursive=True) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. + for interface in pim.get('interface', []): + if 'igmp' not in pim['interface'][interface]: + del default_values['interface'][interface]['igmp'] + + pim = config_dict_merge(default_values, pim) + return pim + + # Ethernet and bonding interfaces can participate in EVPN which is configured via FRR + tmp = {} + for if_type in ['ethernet', 'bonding']: + interface_path = ['interfaces', if_type] + if not conf.exists(interface_path): + continue + for interface in conf.list_nodes(interface_path): + evpn_path = interface_path + [interface, 'evpn'] + if not conf.exists(evpn_path): + continue + + evpn = conf.get_config_dict(evpn_path, key_mangling=('-', '_')) + tmp.update({interface : evpn}) + # At least one participating EVPN interface found, add to result dict + if tmp: dict['interfaces'] = tmp + + # Zebra prefix exchange for Kernel IP/IPv6 and routing protocols + for ip_version in ['ip', 'ipv6']: + ip_cli_path = ['system', ip_version] + ip_dict = conf.get_config_dict(ip_cli_path, key_mangling=('-', '_'), + get_first_key=True, with_recursive_defaults=True) + if ip_dict: + ip_dict['afi'] = ip_version + dict.update({ip_version : ip_dict}) + + # Enable SNMP agentx support + # SNMP AgentX support cannot be disabled once enabled + if conf.exists(['service', 'snmp']): + dict['snmp'] = {} + + # We will always need the policy key + dict['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # We need to check the CLI if the BABEL node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + babel_cli_path = ['protocols', 'babel'] + if conf.exists(babel_cli_path): + babel = conf.get_config_dict(babel_cli_path, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + dict.update({'babel' : babel}) + + # We need to check the CLI if the BFD node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + bfd_cli_path = ['protocols', 'bfd'] + if conf.exists(bfd_cli_path): + bfd = conf.get_config_dict(bfd_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + dict.update({'bfd' : bfd}) + + # We need to check the CLI if the BGP node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + bgp_cli_path = ['protocols', 'bgp'] + if conf.exists(bgp_cli_path): + bgp = conf.get_config_dict(bgp_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + bgp['dependent_vrfs'] = {} + dict.update({'bgp' : bgp}) + elif conf.exists_effective(bgp_cli_path): + dict.update({'bgp' : {'deleted' : '', 'dependent_vrfs' : {}}}) + + # We need to check the CLI if the EIGRP node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + eigrp_cli_path = ['protocols', 'eigrp'] + if conf.exists(eigrp_cli_path): + isis = conf.get_config_dict(eigrp_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + dict.update({'eigrp' : isis}) + elif conf.exists_effective(eigrp_cli_path): + dict.update({'eigrp' : {'deleted' : ''}}) + + # We need to check the CLI if the ISIS node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + isis_cli_path = ['protocols', 'isis'] + if conf.exists(isis_cli_path): + isis = conf.get_config_dict(isis_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + dict.update({'isis' : isis}) + elif conf.exists_effective(isis_cli_path): + dict.update({'isis' : {'deleted' : ''}}) + + # We need to check the CLI if the MPLS node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + mpls_cli_path = ['protocols', 'mpls'] + if conf.exists(mpls_cli_path): + mpls = conf.get_config_dict(mpls_cli_path, key_mangling=('-', '_'), + get_first_key=True) + dict.update({'mpls' : mpls}) + elif conf.exists_effective(mpls_cli_path): + dict.update({'mpls' : {'deleted' : ''}}) + + # We need to check the CLI if the OPENFABRIC node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + openfabric_cli_path = ['protocols', 'openfabric'] + if conf.exists(openfabric_cli_path): + openfabric = conf.get_config_dict(openfabric_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + dict.update({'openfabric' : openfabric}) + elif conf.exists_effective(openfabric_cli_path): + dict.update({'openfabric' : {'deleted' : ''}}) + + # We need to check the CLI if the OSPF node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + ospf_cli_path = ['protocols', 'ospf'] + if conf.exists(ospf_cli_path): + ospf = conf.get_config_dict(ospf_cli_path, key_mangling=('-', '_'), + get_first_key=True) + ospf = dict_helper_ospf_defaults(ospf, ospf_cli_path) + dict.update({'ospf' : ospf}) + elif conf.exists_effective(ospf_cli_path): + dict.update({'ospf' : {'deleted' : ''}}) + + # We need to check the CLI if the OSPFv3 node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + ospfv3_cli_path = ['protocols', 'ospfv3'] + if conf.exists(ospfv3_cli_path): + ospfv3 = conf.get_config_dict(ospfv3_cli_path, key_mangling=('-', '_'), + get_first_key=True) + ospfv3 = dict_helper_ospfv3_defaults(ospfv3, ospfv3_cli_path) + dict.update({'ospfv3' : ospfv3}) + elif conf.exists_effective(ospfv3_cli_path): + dict.update({'ospfv3' : {'deleted' : ''}}) + + # We need to check the CLI if the PIM node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + pim_cli_path = ['protocols', 'pim'] + if conf.exists(pim_cli_path): + pim = conf.get_config_dict(pim_cli_path, key_mangling=('-', '_'), + get_first_key=True) + pim = dict_helper_pim_defaults(pim, pim_cli_path) + dict.update({'pim' : pim}) + elif conf.exists_effective(pim_cli_path): + dict.update({'pim' : {'deleted' : ''}}) + + # We need to check the CLI if the PIM6 node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + pim6_cli_path = ['protocols', 'pim6'] + if conf.exists(pim6_cli_path): + pim6 = conf.get_config_dict(pim6_cli_path, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + dict.update({'pim6' : pim6}) + elif conf.exists_effective(pim6_cli_path): + dict.update({'pim6' : {'deleted' : ''}}) + + # We need to check the CLI if the RIP node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + rip_cli_path = ['protocols', 'rip'] + if conf.exists(rip_cli_path): + rip = conf.get_config_dict(rip_cli_path, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + dict.update({'rip' : rip}) + elif conf.exists_effective(rip_cli_path): + dict.update({'rip' : {'deleted' : ''}}) + + # We need to check the CLI if the RIPng node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + ripng_cli_path = ['protocols', 'ripng'] + if conf.exists(ripng_cli_path): + ripng = conf.get_config_dict(ripng_cli_path, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + dict.update({'ripng' : ripng}) + elif conf.exists_effective(ripng_cli_path): + dict.update({'ripng' : {'deleted' : ''}}) + + # We need to check the CLI if the RPKI node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + rpki_cli_path = ['protocols', 'rpki'] + if conf.exists(rpki_cli_path): + rpki = conf.get_config_dict(rpki_cli_path, key_mangling=('-', '_'), + get_first_key=True, with_pki=True, + with_recursive_defaults=True) + rpki_ssh_key_base = '/run/frr/id_rpki' + for cache, cache_config in rpki.get('cache',{}).items(): + if 'ssh' in cache_config: + cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub' + cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}' + dict.update({'rpki' : rpki}) + elif conf.exists_effective(rpki_cli_path): + dict.update({'rpki' : {'deleted' : ''}}) + + # We need to check the CLI if the Segment Routing node is present and thus load in + # all the default values present on the CLI - that's why we have if conf.exists() + sr_cli_path = ['protocols', 'segment-routing'] + if conf.exists(sr_cli_path): + sr = conf.get_config_dict(sr_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + dict.update({'segment_routing' : sr}) + elif conf.exists_effective(sr_cli_path): + dict.update({'segment_routing' : {'deleted' : ''}}) + + # We need to check the CLI if the static node is present and thus load in + # all the default values present on the CLI - that's why we have if conf.exists() + static_cli_path = ['protocols', 'static'] + if conf.exists(static_cli_path): + static = conf.get_config_dict(static_cli_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # T3680 - get a list of all interfaces currently configured to use DHCP + tmp = get_dhcp_interfaces(conf) + if tmp: static.update({'dhcp' : tmp}) + tmp = get_pppoe_interfaces(conf) + if tmp: static.update({'pppoe' : tmp}) + + dict.update({'static' : static}) + elif conf.exists_effective(static_cli_path): + dict.update({'static' : {'deleted' : ''}}) + + # keep a re-usable list of dependent VRFs + dependent_vrfs_default = {} + if 'bgp' in dict: + dependent_vrfs_default = deepcopy(dict['bgp']) + # we do not need to nest the 'dependent_vrfs' key - simply remove it + if 'dependent_vrfs' in dependent_vrfs_default: + del dependent_vrfs_default['dependent_vrfs'] + + vrf_cli_path = ['vrf', 'name'] + if conf.exists(vrf_cli_path): + vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'), + get_first_key=False, + no_tag_node_value_mangle=True) + # We do not have any VRF related default values on the CLI. The defaults will only + # come into place under the protocols tree, thus we can safely merge them with the + # appropriate routing protocols + for vrf_name, vrf_config in vrf['name'].items(): + bgp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'bgp'] + if 'bgp' in vrf_config.get('protocols', []): + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(bgp_vrf_path, key_mangling=('-', '_'), + get_first_key=True, recursive=True) + + # merge in remaining default values + vrf_config['protocols']['bgp'] = config_dict_merge(default_values, + vrf_config['protocols']['bgp']) + + # Add this BGP VRF instance as dependency into the default VRF + if 'bgp' in dict: + dict['bgp']['dependent_vrfs'].update({vrf_name : deepcopy(vrf_config)}) + + vrf_config['protocols']['bgp']['dependent_vrfs'] = conf.get_config_dict( + vrf_cli_path, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + # We can safely delete ourself from the dependent VRF list + if vrf_name in vrf_config['protocols']['bgp']['dependent_vrfs']: + del vrf_config['protocols']['bgp']['dependent_vrfs'][vrf_name] + + # Add dependency on possible existing default VRF to this VRF + if 'bgp' in dict: + vrf_config['protocols']['bgp']['dependent_vrfs'].update({'default': {'protocols': { + 'bgp': dependent_vrfs_default}}}) + elif conf.exists_effective(bgp_vrf_path): + # Add this BGP VRF instance as dependency into the default VRF + tmp = {'deleted' : '', 'dependent_vrfs': deepcopy(vrf['name'])} + # We can safely delete ourself from the dependent VRF list + if vrf_name in tmp['dependent_vrfs']: + del tmp['dependent_vrfs'][vrf_name] + + # Add dependency on possible existing default VRF to this VRF + if 'bgp' in dict: + tmp['dependent_vrfs'].update({'default': {'protocols': { + 'bgp': dependent_vrfs_default}}}) + + if 'bgp' in dict: + dict['bgp']['dependent_vrfs'].update({vrf_name : {'protocols': tmp} }) + + if 'protocols' not in vrf['name'][vrf_name]: + vrf['name'][vrf_name].update({'protocols': {'bgp' : tmp}}) + else: + vrf['name'][vrf_name]['protocols'].update({'bgp' : tmp}) + + # We need to check the CLI if the EIGRP node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + eigrp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'eigrp'] + if 'eigrp' in vrf_config.get('protocols', []): + eigrp = conf.get_config_dict(eigrp_vrf_path, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + vrf['name'][vrf_name]['protocols'].update({'eigrp' : isis}) + elif conf.exists_effective(eigrp_vrf_path): + vrf['name'][vrf_name]['protocols'].update({'eigrp' : {'deleted' : ''}}) + + # We need to check the CLI if the ISIS node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + isis_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'isis'] + if 'isis' in vrf_config.get('protocols', []): + isis = conf.get_config_dict(isis_vrf_path, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True, with_recursive_defaults=True) + vrf['name'][vrf_name]['protocols'].update({'isis' : isis}) + elif conf.exists_effective(isis_vrf_path): + vrf['name'][vrf_name]['protocols'].update({'isis' : {'deleted' : ''}}) + + # We need to check the CLI if the OSPF node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + ospf_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospf'] + if 'ospf' in vrf_config.get('protocols', []): + ospf = conf.get_config_dict(ospf_vrf_path, key_mangling=('-', '_'), get_first_key=True) + ospf = dict_helper_ospf_defaults(vrf_config['protocols']['ospf'], ospf_vrf_path) + vrf['name'][vrf_name]['protocols'].update({'ospf' : ospf}) + elif conf.exists_effective(ospf_vrf_path): + vrf['name'][vrf_name]['protocols'].update({'ospf' : {'deleted' : ''}}) + + # We need to check the CLI if the OSPFv3 node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + ospfv3_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospfv3'] + if 'ospfv3' in vrf_config.get('protocols', []): + ospfv3 = conf.get_config_dict(ospfv3_vrf_path, key_mangling=('-', '_'), get_first_key=True) + ospfv3 = dict_helper_ospfv3_defaults(vrf_config['protocols']['ospfv3'], ospfv3_vrf_path) + vrf['name'][vrf_name]['protocols'].update({'ospfv3' : ospfv3}) + elif conf.exists_effective(ospfv3_vrf_path): + vrf['name'][vrf_name]['protocols'].update({'ospfv3' : {'deleted' : ''}}) + + # We need to check the CLI if the static node is present and thus load in all the default + # values present on the CLI - that's why we have if conf.exists() + static_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'static'] + if 'static' in vrf_config.get('protocols', []): + static = conf.get_config_dict(static_vrf_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + # T3680 - get a list of all interfaces currently configured to use DHCP + tmp = get_dhcp_interfaces(conf, vrf_name) + if tmp: static.update({'dhcp' : tmp}) + tmp = get_pppoe_interfaces(conf, vrf_name) + if tmp: static.update({'pppoe' : tmp}) + + vrf['name'][vrf_name]['protocols'].update({'static': static}) + elif conf.exists_effective(static_vrf_path): + vrf['name'][vrf_name]['protocols'].update({'static': {'deleted' : ''}}) + + vrf_vni_path = ['vrf', 'name', vrf_name, 'vni'] + if conf.exists(vrf_vni_path): + vrf_config.update({'vni': conf.return_value(vrf_vni_path)}) + + dict.update({'vrf' : vrf}) + elif conf.exists_effective(vrf_cli_path): + effective_vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'), + get_first_key=False, + no_tag_node_value_mangle=True, + effective=True) + vrf = {'name' : {}} + for vrf_name, vrf_config in effective_vrf.get('name', {}).items(): + vrf['name'].update({vrf_name : {}}) + for protocol in frr_protocols: + if protocol in vrf_config.get('protocols', []): + # Create initial protocols key if not present + if 'protocols' not in vrf['name'][vrf_name]: + vrf['name'][vrf_name].update({'protocols' : {}}) + # All routing protocols are deleted when we pass this point + tmp = {'deleted' : ''} + + # Special treatment for BGP routing protocol + if protocol == 'bgp': + tmp['dependent_vrfs'] = {} + if 'name' in vrf: + tmp['dependent_vrfs'] = conf.get_config_dict( + vrf_cli_path, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True, + effective=True) + # Add dependency on possible existing default VRF to this VRF + if 'bgp' in dict: + tmp['dependent_vrfs'].update({'default': {'protocols': { + 'bgp': dependent_vrfs_default}}}) + # We can safely delete ourself from the dependent VRF list + if vrf_name in tmp['dependent_vrfs']: + del tmp['dependent_vrfs'][vrf_name] + + # Update VRF related dict + vrf['name'][vrf_name]['protocols'].update({protocol : tmp}) + + dict.update({'vrf' : vrf}) + + return dict + class FRRender: def __init__(self): self._frr_conf = '/run/frr/config/vyos.frr.conf' diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 0844d29136..4f1141dcba 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -17,7 +17,6 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configdict import leaf_node_changed @@ -32,6 +31,7 @@ from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import BondIf from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig import Section diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 5024e6982d..41c89fdf8c 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -20,7 +20,6 @@ from vyos.base import Warning from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_address @@ -35,6 +34,7 @@ from vyos.configverify import verify_eapol from vyos.ethtool import Ethtool from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf from vyos.utils.dict import dict_search diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 5e71a612d5..a90e33e81b 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -17,10 +17,10 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.frrender import frr_protocols +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running from vyos import ConfigError diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py index 48b7ae734e..80a847af87 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -17,11 +17,11 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running from vyos import ConfigError diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 2e7d406764..d3bc3e9619 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -15,10 +15,10 @@ # along with this program. If not, see . from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import verify_vrf from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.template import is_ipv6 from vyos.utils.network import is_ipv6_link_local from vyos.utils.process import is_systemd_service_running diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 60f3f2ad02..c4af717afa 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,12 +19,12 @@ from vyos.base import Warning from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_prefix_list from vyos.configverify import verify_route_map from vyos.configverify import verify_vrf from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.template import is_ip from vyos.template import is_interface from vyos.utils.dict import dict_search diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index 8f49bb151f..324ff883f2 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -18,11 +18,11 @@ from sys import argv from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_vrf from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError from vyos import airbag airbag.enable() diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 1e5f0d6e88..1c994492e0 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -18,11 +18,11 @@ from sys import argv from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_interface_exists from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index e8097b7ff5..33d9a6dae3 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -20,9 +20,9 @@ from glob import glob from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.process import is_systemd_service_running diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py index 41c5d95449..7df11fb20a 100644 --- a/src/conf_mode/protocols_openfabric.py +++ b/src/conf_mode/protocols_openfabric.py @@ -18,11 +18,11 @@ from vyos.base import Warning from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError from vyos import airbag airbag.enable() diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index f2c95a63c3..c06c0aafcb 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -18,13 +18,13 @@ from sys import argv from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import verify_access_list from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos.utils.process import is_systemd_service_running diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index ac189c3785..2563eb7d5c 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -18,12 +18,12 @@ from sys import argv from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 477895b0bb..6320999646 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -22,10 +22,10 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.frrender import pim_daemon from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running diff --git a/src/conf_mode/protocols_pim6.py b/src/conf_mode/protocols_pim6.py index 3a9b876cc1..03a79139ae 100755 --- a/src/conf_mode/protocols_pim6.py +++ b/src/conf_mode/protocols_pim6.py @@ -19,11 +19,11 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_interface_exists from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError from vyos import airbag airbag.enable() diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 39743f9654..ec9dfbb8b6 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -17,12 +17,12 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running from vyos import ConfigError diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index 14f0384441..9a9ac8ec86 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -17,12 +17,12 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running from vyos import ConfigError diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 5ad656586e..ef0250e3d9 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -20,9 +20,9 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.pki import wrap_openssh_public_key from vyos.pki import wrap_openssh_private_key from vyos.utils.dict import dict_search_args diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py index 99cf87556f..f2bd42a793 100755 --- a/src/conf_mode/protocols_segment-routing.py +++ b/src/conf_mode/protocols_segment-routing.py @@ -17,10 +17,10 @@ from sys import exit from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configdict import list_diff from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Section from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 9d02db6dd7..1b9e51167a 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -19,11 +19,11 @@ from sys import argv from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.process import is_systemd_service_running from vyos.template import render from vyos import ConfigError diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py index 86843eb783..7f3796168a 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -19,10 +19,10 @@ from vyos.config import Config from vyos.configdep import set_dependents from vyos.configdep import call_dependents -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_active from vyos.utils.process import is_systemd_service_running diff --git a/src/conf_mode/system_ipv6.py b/src/conf_mode/system_ipv6.py index 593b8f7f37..309869b2f2 100755 --- a/src/conf_mode/system_ipv6.py +++ b/src/conf_mode/system_ipv6.py @@ -20,10 +20,10 @@ from vyos.config import Config from vyos.configdep import set_dependents from vyos.configdep import call_dependents -from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.process import is_systemd_service_active diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 6533f493fc..74780b6011 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -19,11 +19,11 @@ from json import loads from vyos.config import Config -from vyos.configdict import get_frrender_dict from vyos.configdict import node_changed from vyos.configverify import verify_route_map from vyos.firewall import conntrack_required from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.template import render from vyos.utils.dict import dict_search diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 500d5425ff..d558e8c268 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -33,12 +33,12 @@ import zmq from vyos.defaults import directories from vyos.utils.boot import boot_configuration_complete -from vyos.configdict import get_frrender_dict from vyos.configsource import ConfigSourceString from vyos.configsource import ConfigSourceError from vyos.configdiff import get_commit_scripts from vyos.config import Config from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError CFG_GROUP = 'vyattacfg' From d2a7584a6d5cc3fcd01471b9cfd45735b5458724 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Wed, 18 Dec 2024 23:01:43 +0100 Subject: [PATCH 5/5] ifconfig: fix AttributeError caused by internal race condition File "/usr/lib/python3/dist-packages/vyos/ifconfig/interface.py", line 342 if not self.iftype: ^^^^^^^^^^^ AttributeError: 'Interface' object has no attribute 'iftype' --- python/vyos/ifconfig/interface.py | 11 +++++------ python/vyos/ifconfig/macvlan.py | 5 ++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index cd562e1fea..eac9f61f51 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -26,8 +26,9 @@ # this is not the same as socket.AF_INET/INET6 from netifaces import AF_INET from netifaces import AF_INET6 +from netaddr import EUI +from netaddr import mac_unix_expanded -from vyos import ConfigError from vyos.configdict import list_diff from vyos.configdict import dict_merge from vyos.configdict import get_vlan_ids @@ -61,9 +62,7 @@ from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational from vyos.ifconfig import Section - -from netaddr import EUI -from netaddr import mac_unix_expanded +from vyos import ConfigError link_local_prefix = 'fe80::/64' @@ -339,8 +338,8 @@ def __init__(self, ifname, **kargs): # Any instance of Interface, such as Interface('eth0') can be used # safely to access the generic function in this class as 'type' is # unset, the class can not be created - if not self.iftype: - raise Exception(f'interface "{ifname}" not found') + if not hasattr(self, 'iftype'): + raise ConfigError(f'Interface "{ifname}" has no "iftype" attribute defined!') self.config['type'] = self.iftype # Should an Instance of a child class (EthernetIf, DummyIf, ..) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 2266879ec3..fb7f1d2980 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 VyOS maintainers and contributors +# Copyright 2019-2024 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -42,6 +42,5 @@ def _create(self): self.set_admin_state('down') def set_mode(self, mode): - ifname = self.config['ifname'] - cmd = f'ip link set dev {ifname} type macvlan mode {mode}' + cmd = f'ip link set dev {self.ifname} type {self.iftype} mode {mode}' return self._cmd(cmd)