diff options
41 files changed, 810 insertions, 881 deletions
diff --git a/data/templates/frr_exporter/frr_exporter.service.j2 b/data/templates/prometheus/frr_exporter.service.j2 index c3892e42b..c3892e42b 100644 --- a/data/templates/frr_exporter/frr_exporter.service.j2 +++ b/data/templates/prometheus/frr_exporter.service.j2 diff --git a/data/templates/node_exporter/node_exporter.service.j2 b/data/templates/prometheus/node_exporter.service.j2 index 62e7e6774..62e7e6774 100644 --- a/data/templates/node_exporter/node_exporter.service.j2 +++ b/data/templates/prometheus/node_exporter.service.j2 diff --git a/debian/control b/debian/control index 76ca83dcd..08b86356a 100644 --- a/debian/control +++ b/debian/control @@ -235,12 +235,12 @@ Depends: squidclient, squidguard, # End "service webproxy" -# For "service monitoring node-exporter" +# For "service monitoring prometheus node-exporter" node-exporter, -# End "service monitoring node-exporter" -# For "service monitoring frr-exporter" +# End "service monitoring prometheus node-exporter" +# For "service monitoring prometheus frr-exporter" frr-exporter, -# End "service monitoring frr-exporter" +# End "service monitoring prometheus frr-exporter" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" diff --git a/interface-definitions/include/version/monitoring-version.xml.i b/interface-definitions/include/version/monitoring-version.xml.i index 6a275a5d8..2e2e0116e 100644 --- a/interface-definitions/include/version/monitoring-version.xml.i +++ b/interface-definitions/include/version/monitoring-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/monitoring-version.xml.i --> -<syntaxVersion component='monitoring' version='1'></syntaxVersion> +<syntaxVersion component='monitoring' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/service_monitoring_frr_exporter.xml.in b/interface-definitions/service_monitoring_frr_exporter.xml.in deleted file mode 100644 index 96aee3ab4..000000000 --- a/interface-definitions/service_monitoring_frr_exporter.xml.in +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="monitoring"> - <children> - <node name="frr-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_frr-exporter.py"> - <properties> - <help>Prometheus exporter for FRR metrics</help> - <priority>1280</priority> - </properties> - <children> - #include <include/listen-address.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>9342</defaultValue> - </leafNode> - #include <include/interface/vrf.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/service_monitoring_node_exporter.xml.in b/interface-definitions/service_monitoring_node_exporter.xml.in deleted file mode 100644 index a11d2304f..000000000 --- a/interface-definitions/service_monitoring_node_exporter.xml.in +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="monitoring"> - <children> - <node name="node-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_node-exporter.py"> - <properties> - <help>Prometheus exporter for hardware and operating system metrics</help> - <priority>1280</priority> - </properties> - <children> - #include <include/listen-address.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>9100</defaultValue> - </leafNode> - #include <include/interface/vrf.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/service_monitoring_prometheus.xml.in b/interface-definitions/service_monitoring_prometheus.xml.in new file mode 100644 index 000000000..24f31e15c --- /dev/null +++ b/interface-definitions/service_monitoring_prometheus.xml.in @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="monitoring"> + <children> + <node name="prometheus" owner="${vyos_conf_scripts_dir}/service_monitoring_prometheus.py"> + <properties> + <help>Prometheus metric exporter</help> + <priority>1280</priority> + </properties> + <children> + <node name="node-exporter"> + <properties> + <help>Prometheus exporter for hardware and operating system metrics</help> + </properties> + <children> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>9100</defaultValue> + </leafNode> + #include <include/interface/vrf.xml.i> + </children> + </node> + <node name="frr-exporter"> + <properties> + <help>Prometheus exporter for FRR metrics</help> + </properties> + <children> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>9342</defaultValue> + </leafNode> + #include <include/interface/vrf.xml.i> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index cbcbf9f72..5a353b110 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -19,7 +19,6 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. import os import json -from vyos.defaults import frr_debug_enable from vyos.utils.dict import dict_search from vyos.utils.process import cmd @@ -665,497 +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}) - - 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/python/vyos/frrender.py b/python/vyos/frrender.py index 0c9dde315..badc5d59f 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -22,16 +22,15 @@ import os 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 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) @@ -54,13 +53,496 @@ rip_daemon = 'ripd' 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' - 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: @@ -124,23 +606,23 @@ class FRRender: 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 # 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']: @@ -155,23 +637,25 @@ class FRRender: 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: diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index cd562e1fe..eac9f61f5 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -26,8 +26,9 @@ from netifaces import ifaddresses # 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.control import Control 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 @@ class Interface(Control): # 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 2266879ec..fb7f1d298 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ class MACVLANIf(Interface): 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) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index c39d4467a..3e14976fc 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -22,11 +22,10 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd -from vyos.utils.process import process_running +from vyos.utils.process import process_named_running DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' -DDCLIENT_PID = '/run/ddclient/ddclient.pid' DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] @@ -46,14 +45,14 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process - self.assertTrue(process_running(DDCLIENT_PID)) + self.assertTrue(process_named_running(DDCLIENT_PNAME)) # Delete DDNS configuration self.cli_delete(base_path) self.cli_commit() - # PID file must no londer exist after process exited - self.assertFalse(os.path.exists(DDCLIENT_PID)) + # Check for process not running anymore + self.assertFalse(process_named_running(DDCLIENT_PNAME)) # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): diff --git a/smoketest/scripts/cli/test_service_monitoring_node-exporter.py b/smoketest/scripts/cli/test_service_monitoring_node-exporter.py deleted file mode 100755 index e18a3f7a2..000000000 --- a/smoketest/scripts/cli/test_service_monitoring_node-exporter.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 unittest - -from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'node_exporter' -base_path = ['service', 'monitoring', 'node-exporter'] -service_file = '/etc/systemd/system/node_exporter.service' -listen_if = 'dum3421' -listen_ip = '192.0.2.1' - - -class TestMonitoringNodeExporter(VyOSUnitTestSHIM.TestCase): - @classmethod - def setUpClass(cls): - # call base-classes classmethod - super(TestMonitoringNodeExporter, cls).setUpClass() - # create a test interfaces - cls.cli_set( - cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] - ) - - @classmethod - def tearDownClass(cls): - cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) - super(TestMonitoringNodeExporter, cls).tearDownClass() - - def tearDown(self): - self.cli_delete(base_path) - self.cli_commit() - self.assertFalse(process_named_running(PROCESS_NAME)) - - def test_01_basic_config(self): - self.cli_set(base_path + ['listen-address', listen_ip]) - - # commit changes - self.cli_commit() - - file_content = read_file(service_file) - self.assertIn(f'{listen_ip}:9100', file_content) - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py b/smoketest/scripts/cli/test_service_monitoring_prometheus.py index 230171c11..dae103e4b 100755 --- a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py +++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py @@ -20,18 +20,21 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.process import process_named_running from vyos.utils.file import read_file -PROCESS_NAME = 'frr_exporter' -base_path = ['service', 'monitoring', 'frr-exporter'] -service_file = '/etc/systemd/system/frr_exporter.service' +NODE_EXPORTER_PROCESS_NAME = 'node_exporter' +FRR_EXPORTER_PROCESS_NAME = 'frr_exporter' + +base_path = ['service', 'monitoring', 'prometheus'] listen_if = 'dum3421' listen_ip = '192.0.2.1' +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' -class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase): +class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): # call base-classes classmethod - super(TestMonitoringFrrExporter, cls).setUpClass() + super(TestMonitoringPrometheus, cls).setUpClass() # create a test interfaces cls.cli_set( cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] @@ -40,24 +43,37 @@ class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) - super(TestMonitoringFrrExporter, cls).tearDownClass() + super(TestMonitoringPrometheus, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) self.cli_commit() - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(process_named_running(NODE_EXPORTER_PROCESS_NAME)) + self.assertFalse(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + + def test_01_node_exporter(self): + self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(node_exporter_service_file) + self.assertIn(f'{listen_ip}:9100', file_content) + + # Check for running process + self.assertTrue(process_named_running(NODE_EXPORTER_PROCESS_NAME)) - def test_01_basic_config(self): - self.cli_set(base_path + ['listen-address', listen_ip]) + def test_02_frr_exporter(self): + self.cli_set(base_path + ['frr-exporter', 'listen-address', listen_ip]) # commit changes self.cli_commit() - file_content = read_file(service_file) + file_content = read_file(frr_exporter_service_file) self.assertIn(f'{listen_ip}:9342', file_content) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(process_named_running(FRR_EXPORTER_PROCESS_NAME)) if __name__ == '__main__': diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 0844d2913..4f1141dcb 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_mtu_ipv6 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 5024e6982..41c89fdf8 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -20,7 +20,6 @@ from sys import exit 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_bond_bridge_member 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 5e71a612d..a90e33e81 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 48b7ae734..80a847af8 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 2e7d40676..d3bc3e961 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 <http://www.gnu.org/licenses/>. 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 60f3f2ad0..c4af717af 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,12 +19,12 @@ from sys import argv 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 8f49bb151..324ff883f 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -18,11 +18,11 @@ from sys import exit 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 1e5f0d6e8..1c994492e 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -18,11 +18,11 @@ from sys import exit 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 e8097b7ff..33d9a6dae 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -20,9 +20,9 @@ from sys import exit 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 41c5d9544..7df11fb20 100644 --- a/src/conf_mode/protocols_openfabric.py +++ b/src/conf_mode/protocols_openfabric.py @@ -18,11 +18,11 @@ from sys import exit 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 f2c95a63c..c06c0aafc 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -18,13 +18,13 @@ from sys import exit 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 ac189c378..2563eb7d5 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -18,12 +18,12 @@ from sys import exit 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 477895b0b..632099964 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -22,10 +22,10 @@ from signal import SIGTERM 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 3a9b876cc..03a79139a 100755 --- a/src/conf_mode/protocols_pim6.py +++ b/src/conf_mode/protocols_pim6.py @@ -19,11 +19,11 @@ from ipaddress import IPv6Network 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 39743f965..ec9dfbb8b 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 14f038444..9a9ac8ec8 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 5ad656586..ef0250e3d 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -20,9 +20,9 @@ from glob import glob 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 99cf87556..f2bd42a79 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 9d02db6dd..1b9e51167 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -19,11 +19,11 @@ from sys import exit 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/service_monitoring_frr-exporter.py b/src/conf_mode/service_monitoring_frr-exporter.py deleted file mode 100755 index 01527d579..000000000 --- a/src/conf_mode/service_monitoring_frr-exporter.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 is_node_changed -from vyos.configverify import verify_vrf -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag - - -airbag.enable() - -service_file = '/etc/systemd/system/frr_exporter.service' -systemd_service = 'frr_exporter.service' - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'monitoring', 'frr-exporter'] - if not conf.exists(base): - return None - - config_data = conf.get_config_dict( - base, key_mangling=('-', '_'), get_first_key=True - ) - config_data = conf.merge_defaults(config_data, recursive=True) - - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: - config_data.update({'restart_required': {}}) - - return config_data - - -def verify(config_data): - # bail out early - looks like removal from running config - if not config_data: - return None - - verify_vrf(config_data) - return None - - -def generate(config_data): - if not config_data: - # Delete systemd files - if os.path.isfile(service_file): - os.unlink(service_file) - return None - - # Render frr_exporter service_file - render(service_file, 'frr_exporter/frr_exporter.service.j2', config_data) - return None - - -def apply(config_data): - # Reload systemd manager configuration - call('systemctl daemon-reload') - if not config_data: - call(f'systemctl stop {systemd_service}') - return - - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in config_data: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_service}') - - -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/service_monitoring_node-exporter.py b/src/conf_mode/service_monitoring_node-exporter.py deleted file mode 100755 index db34bb5d0..000000000 --- a/src/conf_mode/service_monitoring_node-exporter.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 is_node_changed -from vyos.configverify import verify_vrf -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag - - -airbag.enable() - -service_file = '/etc/systemd/system/node_exporter.service' -systemd_service = 'node_exporter.service' - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'monitoring', 'node-exporter'] - if not conf.exists(base): - return None - - config_data = conf.get_config_dict( - base, key_mangling=('-', '_'), get_first_key=True - ) - config_data = conf.merge_defaults(config_data, recursive=True) - - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: - config_data.update({'restart_required': {}}) - - return config_data - - -def verify(config_data): - # bail out early - looks like removal from running config - if not config_data: - return None - - verify_vrf(config_data) - return None - - -def generate(config_data): - if not config_data: - # Delete systemd files - if os.path.isfile(service_file): - os.unlink(service_file) - return None - - # Render node_exporter service_file - render(service_file, 'node_exporter/node_exporter.service.j2', config_data) - return None - - -def apply(config_data): - # Reload systemd manager configuration - call('systemctl daemon-reload') - if not config_data: - call(f'systemctl stop {systemd_service}') - return - - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in config_data: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_service}') - - -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/service_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py new file mode 100755 index 000000000..e0a9fc4ef --- /dev/null +++ b/src/conf_mode/service_monitoring_prometheus.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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 is_node_changed +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag + + +airbag.enable() + +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +node_exporter_systemd_service = 'node_exporter.service' + +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' +frr_exporter_systemd_service = 'frr_exporter.service' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'prometheus'] + if not conf.exists(base): + return None + + monitoring = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True + ) + + tmp = is_node_changed(conf, base + ['node-exporter', 'vrf']) + if tmp: + monitoring.update({'node_exporter_restart_required': {}}) + + tmp = is_node_changed(conf, base + ['frr-exporter', 'vrf']) + if tmp: + monitoring.update({'frr_exporter_restart_required': {}}) + + return monitoring + + +def verify(monitoring): + if not monitoring: + return None + + if 'node_exporter' in monitoring: + verify_vrf(monitoring['node_exporter']) + + if 'frr_exporter' in monitoring: + verify_vrf(monitoring['frr_exporter']) + + return None + + +def generate(monitoring): + if not monitoring or 'node_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(node_exporter_service_file): + os.unlink(node_exporter_service_file) + + if not monitoring or 'frr_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(frr_exporter_service_file): + os.unlink(frr_exporter_service_file) + + if not monitoring: + return None + + if 'node_exporter' in monitoring: + # Render node_exporter node_exporter_service_file + render( + node_exporter_service_file, + 'prometheus/node_exporter.service.j2', + monitoring['node_exporter'], + ) + + if 'frr_exporter' in monitoring: + # Render frr_exporter service_file + render( + frr_exporter_service_file, + 'prometheus/frr_exporter.service.j2', + monitoring['frr_exporter'], + ) + + return None + + +def apply(monitoring): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not monitoring or 'node_exporter' not in monitoring: + call(f'systemctl stop {node_exporter_systemd_service}') + if not monitoring or 'frr_exporter' not in monitoring: + call(f'systemctl stop {frr_exporter_systemd_service}') + + if not monitoring: + return + + if 'node_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'node_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {node_exporter_systemd_service}') + + if 'frr_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'frr_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {frr_exporter_systemd_service}') + + +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/system_ip.py b/src/conf_mode/system_ip.py index 86843eb78..7f3796168 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -19,10 +19,10 @@ from sys import exit 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 593b8f7f3..309869b2f 100755 --- a/src/conf_mode/system_ipv6.py +++ b/src/conf_mode/system_ipv6.py @@ -20,10 +20,10 @@ from sys import exit 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 6533f493f..74780b601 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -19,11 +19,11 @@ from jmespath import search 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/migration-scripts/monitoring/1-to-2 b/src/migration-scripts/monitoring/1-to-2 new file mode 100644 index 000000000..8bdaebae9 --- /dev/null +++ b/src/migration-scripts/monitoring/1-to-2 @@ -0,0 +1,50 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T6953: merge node and frr exporter under prometheus section + +from vyos.configtree import ConfigTree + +old_base = ['service', 'monitoring'] +new_base = ['service', 'monitoring', 'prometheus'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(old_base): + # Nothing to do + return + + if config.exists(old_base + ['node-exporter']): + if config.exists(old_base + ['node-exporter', 'listen-address']): + tmp = config.return_value(old_base + ['node-exporter', 'listen-address']) + config.set(new_base + ['node-exporter', 'listen-address'], value=tmp) + if config.exists(old_base + ['node-exporter', 'port']): + tmp = config.return_value(old_base + ['node-exporter', 'port']) + config.set(new_base + ['node-exporter', 'port'], value=tmp) + if config.exists(old_base + ['node-exporter', 'vrf']): + tmp = config.return_value(old_base + ['node-exporter', 'vrf']) + config.set(new_base + ['node-exporter', 'vrf'], value=tmp) + config.delete(old_base + ['node-exporter']) + + if config.exists(old_base + ['frr-exporter']): + if config.exists(old_base + ['frr-exporter', 'listen-address']): + tmp = config.return_value(old_base + ['frr-exporter', 'listen-address']) + config.set(new_base + ['frr-exporter', 'listen-address'], value=tmp) + if config.exists(old_base + ['frr-exporter', 'port']): + tmp = config.return_value(old_base + ['frr-exporter', 'port']) + config.set(new_base + ['frr-exporter', 'port'], value=tmp) + if config.exists(old_base + ['frr-exporter', 'vrf']): + tmp = config.return_value(old_base + ['frr-exporter', 'vrf']) + config.set(new_base + ['frr-exporter', 'vrf'], value=tmp) + config.delete(old_base + ['frr-exporter']) diff --git a/src/services/vyos-configd b/src/services/vyos-configd index ecad85801..d558e8c26 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -38,6 +38,7 @@ 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' @@ -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}') |