diff options
author | Christian Breunig <christian@breunig.cc> | 2024-12-08 08:34:41 +0100 |
---|---|---|
committer | Christian Breunig <christian@breunig.cc> | 2024-12-16 22:24:36 +0100 |
commit | 3c79477adf3cd4f4efb302b58542ddd668b562ac (patch) | |
tree | 316d03a81f1c9c37b48b658af7826eaf11413130 | |
parent | 328f354677a4fd2f60b95473698816e99aaa20a7 (diff) | |
download | vyos-1x-3c79477adf3cd4f4efb302b58542ddd668b562ac.tar.gz vyos-1x-3c79477adf3cd4f4efb302b58542ddd668b562ac.zip |
frr: T6747: migrate protocols to unified FRRender class
With FRR 10.0 daemons started to be migrated to integrated FRR mgmtd and a
northbound interface. This led to some drawbacks in the current state how
changes to FRR are handled. The current implementation will use frr-reload.py
and specifies excatly WHICH daemon needs a config update and will only replace
this part inside FRR.
With FRR10 and mgmtd when a partial configuration is sent to mgmtd, it will
remove configuration parts from other daemons like bgpd or ospfd which have
not yet been migrated to mgmtd.
It's not possible to call frr-reload.py with daemon mgmtd - it will error out.
This commit will also change the CLI for static routes:
CLI command "set protocols static route 10.0.0.0/8 next-hop 1.2.3.4 bfd multi-hop
source 1.1.1.1" will be split into:
* set protocols static route 10.0.0.0/8 next-hop 1.2.3.4 bfd source-address 1.1.1.1
* set protocols static route 10.0.0.0/8 next-hop 1.2.3.4 bfd multi-hop
To make the XML blocks reusable, and comply with the FRR CLI - this was actually
a wrong implementation from the beginning as you can not have multiple BFD
source addresses.
CLI command "set protocols static route 10.0.0.0/8 next-hop 1.2.3.4 bfd multi-hop
source 1.1.1.1 profile bar" is changed to:
* set protocols static route 10.0.0.0/8 next-hop 1.2.3.4 bfd profile bar
CLI commands "set protocols static multicast interface-route" is moved to:
* set protocols static multicast route <x.x.x.x/x> interface
To have an identical look and feel with regular static routes.
42 files changed, 1378 insertions, 1424 deletions
diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 71716250d..51a3f2564 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -1,13 +1,19 @@ {### MACRO definition for recurring peer patter, this can be either fed by a ###} {### peer-group or an individual BGP neighbor ###} {% macro bgp_neighbor(neighbor, config, peer_group=false) %} +{# BGP order of peer-group and remote-as placement must be honored #} {% if peer_group == true %} neighbor {{ neighbor }} peer-group -{% elif config.peer_group is vyos_defined %} - neighbor {{ neighbor }} peer-group {{ config.peer_group }} -{% endif %} -{% if config.remote_as is vyos_defined %} +{% if config.remote_as is vyos_defined %} + neighbor {{ neighbor }} remote-as {{ config.remote_as }} +{% endif %} +{% else %} +{% if config.remote_as is vyos_defined %} neighbor {{ neighbor }} remote-as {{ config.remote_as }} +{% endif %} +{% if config.peer_group is vyos_defined %} + neighbor {{ neighbor }} peer-group {{ config.peer_group }} +{% endif %} {% endif %} {% if config.local_role is vyos_defined %} {% for role, strict in config.local_role.items() %} diff --git a/data/templates/frr/evpn.mh.frr.j2 b/data/templates/frr/evpn.mh.frr.j2 index 03aaac44b..2fd7b7c09 100644 --- a/data/templates/frr/evpn.mh.frr.j2 +++ b/data/templates/frr/evpn.mh.frr.j2 @@ -1,16 +1,20 @@ ! -interface {{ ifname }} -{% if evpn.es_df_pref is vyos_defined %} - evpn mh es-df-pref {{ evpn.es_df_pref }} -{% endif %} -{% if evpn.es_id is vyos_defined %} - evpn mh es-id {{ evpn.es_id }} -{% endif %} -{% if evpn.es_sys_mac is vyos_defined %} - evpn mh es-sys-mac {{ evpn.es_sys_mac }} -{% endif %} -{% if evpn.uplink is vyos_defined %} +{% if interfaces is vyos_defined %} +{% for if_name, if_config in interfaces.items() %} +interface {{ if_name }} +{% if if_config.evpn.es_df_pref is vyos_defined %} + evpn mh es-df-pref {{ if_config.evpn.es_df_pref }} +{% endif %} +{% if if_config.evpn.es_id is vyos_defined %} + evpn mh es-id {{ if_config.evpn.es_id }} +{% endif %} +{% if if_config.evpn.es_sys_mac is vyos_defined %} + evpn mh es-sys-mac {{ if_config.evpn.es_sys_mac }} +{% endif %} +{% if if_config.evpn.uplink is vyos_defined %} evpn mh uplink -{% endif %} +{% endif %} exit ! +{% endfor %} +{% endif %} diff --git a/data/templates/frr/fabricd.frr.j2 b/data/templates/frr/fabricd.frr.j2 index 8f2ae6466..3a0646eb8 100644 --- a/data/templates/frr/fabricd.frr.j2 +++ b/data/templates/frr/fabricd.frr.j2 @@ -70,3 +70,4 @@ router openfabric {{ name }} exit ! {% endfor %} +! diff --git a/data/templates/frr/static_mcast.frr.j2 b/data/templates/frr/static_mcast.frr.j2 deleted file mode 100644 index 54b2790b0..000000000 --- a/data/templates/frr/static_mcast.frr.j2 +++ /dev/null @@ -1,11 +0,0 @@ -! -{% for route_gr in mroute %} -{% for nh in mroute[route_gr] %} -{% if mroute[route_gr][nh] %} -ip mroute {{ route_gr }} {{ nh }} {{ mroute[route_gr][nh] }} -{% else %} -ip mroute {{ route_gr }} {{ nh }} -{% endif %} -{% endfor %} -{% endfor %} -! diff --git a/data/templates/frr/static_routes_macro.j2 b/data/templates/frr/static_routes_macro.j2 deleted file mode 100644 index db31700c5..000000000 --- a/data/templates/frr/static_routes_macro.j2 +++ /dev/null @@ -1,31 +0,0 @@ -{% macro static_routes(ip_ipv6, prefix, prefix_config, table=None) %} -{% if prefix_config.blackhole is vyos_defined %} -{{ ip_ipv6 }} route {{ prefix }} blackhole {{ prefix_config.blackhole.distance if prefix_config.blackhole.distance is vyos_defined }} {{ 'tag ' ~ prefix_config.blackhole.tag if prefix_config.blackhole.tag is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined and table is not none }} -{% endif %} -{% if prefix_config.reject is vyos_defined %} -{{ ip_ipv6 }} route {{ prefix }} reject {{ prefix_config.reject.distance if prefix_config.reject.distance is vyos_defined }} {{ 'tag ' ~ prefix_config.reject.tag if prefix_config.reject.tag is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }} -{% endif %} -{% if prefix_config.dhcp_interface is vyos_defined %} -{% for dhcp_interface in prefix_config.dhcp_interface %} -{% set next_hop = dhcp_interface | get_dhcp_router %} -{% if next_hop is vyos_defined %} -{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ dhcp_interface }} {{ 'table ' ~ table if table is vyos_defined }} -{% endif %} -{% endfor %} -{% endif %} -{% if prefix_config.interface is vyos_defined %} -{% for interface, interface_config in prefix_config.interface.items() if interface_config.disable is not defined %} -{{ ip_ipv6 }} route {{ prefix }} {{ interface }} {{ interface_config.distance if interface_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ interface_config.vrf if interface_config.vrf is vyos_defined }} {{ 'segments ' ~ interface_config.segments if interface_config.segments is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }} -{% endfor %} -{% endif %} -{% if prefix_config.next_hop is vyos_defined and prefix_config.next_hop is not none %} -{% for next_hop, next_hop_config in prefix_config.next_hop.items() if next_hop_config.disable is not defined %} -{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ next_hop_config.interface if next_hop_config.interface is vyos_defined }} {{ next_hop_config.distance if next_hop_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ next_hop_config.vrf if next_hop_config.vrf is vyos_defined }} {{ 'bfd profile ' ~ next_hop_config.bfd.profile if next_hop_config.bfd.profile is vyos_defined }} {{ 'segments ' ~ next_hop_config.segments if next_hop_config.segments is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }} -{% if next_hop_config.bfd.multi_hop.source is vyos_defined %} -{% for source, source_config in next_hop_config.bfd.multi_hop.source.items() %} -{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} bfd multi-hop source {{ source }} profile {{ source_config.profile }} -{% endfor %} -{% endif %} -{% endfor %} -{% endif %} -{% endmacro %} diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2 index 68e3f21f9..c662b7650 100644 --- a/data/templates/frr/staticd.frr.j2 +++ b/data/templates/frr/staticd.frr.j2 @@ -1,12 +1,77 @@ -{% from 'frr/static_routes_macro.j2' import static_routes %} +{# Common macro for recurroiing options for a static route #} +{% macro route_options(route, interface_or_next_hop, config, table) %} +{# j2lint: disable=jinja-statements-delimeter #} +{% set ip_route = route ~ ' ' ~ interface_or_next_hop %} +{% if config.interface is vyos_defined %} +{% set ip_route = ip_route ~ ' ' ~ config.interface %} +{% endif %} +{% if config.tag is vyos_defined %} +{% set ip_route = ip_route ~ ' tag ' ~ config.tag %} +{% endif %} +{% if config.distance is vyos_defined %} +{% set ip_route = ip_route ~ ' ' ~ config.distance %} +{% endif %} +{% if config.bfd is vyos_defined %} +{% set ip_route = ip_route ~ ' bfd' %} +{% if config.bfd.multi_hop is vyos_defined %} +{% set ip_route = ip_route ~ ' multi-hop' %} +{% if config.bfd.source_address is vyos_defined %} +{% set ip_route = ip_route ~ ' source ' ~ config.bfd.source_address %} +{% endif %} +{% endif %} +{% if config.bfd.profile is vyos_defined %} +{% set ip_route = ip_route ~ ' profile ' ~ config.bfd.profile %} +{% endif %} +{% endif %} +{% if config.vrf is vyos_defined %} +{% set ip_route = ip_route ~ ' nexthop-vrf ' ~ config.vrf %} +{% endif %} +{% if config.segments is vyos_defined %} +{# Segments used in/for SRv6 #} +{% set ip_route = ip_route ~ ' segments ' ~ config.segments %} +{% endif %} +{# Routing table to configure #} +{% if table is vyos_defined %} +{% set ip_route = ip_route ~ ' table ' ~ table %} +{% endif %} +{{ ip_route }} +{%- endmacro -%} +{# Build static IPv4/IPv6 route #} +{% macro static_routes(ip_ipv6, prefix, prefix_config, table=None) %} +{% set route = ip_ipv6 ~ 'route ' ~ prefix %} +{% if prefix_config.interface is vyos_defined %} +{% for interface, interface_config in prefix_config.interface.items() if interface_config.disable is not defined %} +{{ route_options(route, interface, interface_config, table) }} +{% endfor %} +{% endif %} +{% if prefix_config.next_hop is vyos_defined and prefix_config.next_hop is not none %} +{% for next_hop, next_hop_config in prefix_config.next_hop.items() if next_hop_config.disable is not defined %} +{{ route_options(route, next_hop, next_hop_config, table) }} +{% endfor %} +{% endif %} +{% if prefix_config.dhcp_interface is vyos_defined %} +{% for dhcp_interface in prefix_config.dhcp_interface %} +{% set next_hop = dhcp_interface | get_dhcp_router %} +{% if next_hop is vyos_defined %} +{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ dhcp_interface }} {{ 'table ' ~ table if table is vyos_defined }} +{% endif %} +{% endfor %} +{% endif %} +{% if prefix_config.blackhole is vyos_defined %} +{{ route_options(route, 'blackhole', prefix_config.blackhole, table) }} +{% elif prefix_config.reject is vyos_defined %} +{{ route_options(route, 'reject', prefix_config.reject, table) }} +{% endif %} +{# j2lint: disable=jinja-statements-delimeter #} +{%- endmacro -%} ! -{% set ip_prefix = 'ip' %} -{% set ipv6_prefix = 'ipv6' %} +{% set ip_prefix = 'ip ' %} +{% set ipv6_prefix = 'ipv6 ' %} {% if vrf is vyos_defined %} {# We need to add an additional whitespace in front of the prefix #} {# when VRFs are in use, thus we use a variable for prefix handling #} -{% set ip_prefix = ' ip' %} -{% set ipv6_prefix = ' ipv6' %} +{% set ip_prefix = ' ip ' %} +{% set ipv6_prefix = ' ipv6 ' %} vrf {{ vrf }} {% endif %} {# IPv4 routing #} @@ -47,19 +112,33 @@ exit-vrf {% for table_id, table_config in table.items() %} {% if table_config.route is vyos_defined %} {% for prefix, prefix_config in table_config.route.items() %} -{{ static_routes('ip', prefix, prefix_config, table_id) }} -{% endfor %} +{{ static_routes('ip ', prefix, prefix_config, table_id) }} +{# j2lint: disable=jinja-statements-delimeter #} +{%- endfor %} {% endif %} ! {% if table_config.route6 is vyos_defined %} {% for prefix, prefix_config in table_config.route6.items() %} -{{ static_routes('ipv6', prefix, prefix_config, table_id) }} -{% endfor %} +{{ static_routes('ipv6 ', prefix, prefix_config, table_id) }} +{# j2lint: disable=jinja-statements-delimeter #} +{%- endfor %} {% endif %} ! {% endfor %} {% endif %} ! +{# Multicast route #} +{% if multicast is vyos_defined %} +{% set ip_prefix = 'ip m' %} +{# IPv4 multicast routing #} +{% if multicast.route is vyos_defined %} +{% for prefix, prefix_config in multicast.route.items() %} +{{ static_routes(ip_prefix, prefix, prefix_config) }} +{# j2lint: disable=jinja-statements-delimeter #} +{%- endfor %} +{% endif %} +{% endif %} +! {% if route_map is vyos_defined %} ip protocol static route-map {{ route_map }} ! diff --git a/data/templates/frr/zebra.vrf.route-map.frr.j2 b/data/templates/frr/zebra.vrf.route-map.frr.j2 index 8ebb82511..656b31deb 100644 --- a/data/templates/frr/zebra.vrf.route-map.frr.j2 +++ b/data/templates/frr/zebra.vrf.route-map.frr.j2 @@ -25,6 +25,6 @@ vrf {{ vrf }} vni {{ vrf_config.vni }} {% endif %} exit-vrf -{% endfor %} ! +{% endfor %} {% endif %} diff --git a/interface-definitions/include/source-address-ipv4.xml.i b/interface-definitions/include/source-address-ipv4.xml.i index 052678113..aa0b083c7 100644 --- a/interface-definitions/include/source-address-ipv4.xml.i +++ b/interface-definitions/include/source-address-ipv4.xml.i @@ -1,7 +1,7 @@ <!-- include start from source-address-ipv4.xml.i --> <leafNode name="source-address"> <properties> - <help>IPv4 source address used to initiate connection</help> + <help>IPv4 address used to initiate connection</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> </completionHelp> diff --git a/interface-definitions/include/source-address-ipv6.xml.i b/interface-definitions/include/source-address-ipv6.xml.i new file mode 100644 index 000000000..a27955b0c --- /dev/null +++ b/interface-definitions/include/source-address-ipv6.xml.i @@ -0,0 +1,17 @@ +<!-- include start from source-address-ipv6.xml.i --> +<leafNode name="source-address"> + <properties> + <help>IPv6 address used to initiate connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script> + </completionHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/static/bfd-multi-hop.xml.i b/interface-definitions/include/static/bfd-multi-hop.xml.i new file mode 100644 index 000000000..e53994191 --- /dev/null +++ b/interface-definitions/include/static/bfd-multi-hop.xml.i @@ -0,0 +1,8 @@ +<!-- include start from static/bfd-multi-hop.xml.i --> +<leafNode name="multi-hop"> + <properties> + <help>Enable BFD multi-hop session (requires source-address)</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/static/static-route-bfd.xml.i b/interface-definitions/include/static/static-route-bfd.xml.i deleted file mode 100644 index d588b369f..000000000 --- a/interface-definitions/include/static/static-route-bfd.xml.i +++ /dev/null @@ -1,36 +0,0 @@ -<!-- include start from static/static-route-bfd.xml.i --> -<node name="bfd"> - <properties> - <help>BFD monitoring</help> - </properties> - <children> - #include <include/bfd/profile.xml.i> - <node name="multi-hop"> - <properties> - <help>Use BFD multi hop session</help> - </properties> - <children> - <tagNode name="source"> - <properties> - <help>Use source for BFD session</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 source address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 source address</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - </constraint> - </properties> - <children> - #include <include/bfd/profile.xml.i> - </children> - </tagNode> - </children> - </node> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i index 51e45c6f7..fa1131118 100644 --- a/interface-definitions/include/static/static-route.xml.i +++ b/interface-definitions/include/static/static-route.xml.i @@ -51,10 +51,18 @@ #include <include/static/static-route-distance.xml.i> #include <include/static/static-route-interface.xml.i> #include <include/static/static-route-vrf.xml.i> - #include <include/static/static-route-bfd.xml.i> + <node name="bfd"> + <properties> + <help>BFD monitoring</help> + </properties> + <children> + #include <include/bfd/profile.xml.i> + #include <include/static/bfd-multi-hop.xml.i> + #include <include/source-address-ipv4.xml.i> + </children> + </node> </children> </tagNode> </children> </tagNode> <!-- include end --> - diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i index 4468c8025..e75385dc7 100644 --- a/interface-definitions/include/static/static-route6.xml.i +++ b/interface-definitions/include/static/static-route6.xml.i @@ -48,11 +48,20 @@ </properties> <children> #include <include/generic-disable-node.xml.i> - #include <include/static/static-route-bfd.xml.i> #include <include/static/static-route-distance.xml.i> #include <include/static/static-route-interface.xml.i> #include <include/static/static-route-segments.xml.i> #include <include/static/static-route-vrf.xml.i> + <node name="bfd"> + <properties> + <help>BFD monitoring</help> + </properties> + <children> + #include <include/bfd/profile.xml.i> + #include <include/static/bfd-multi-hop.xml.i> + #include <include/source-address-ipv6.xml.i> + </children> + </node> </children> </tagNode> </children> diff --git a/interface-definitions/protocols_static.xml.in b/interface-definitions/protocols_static.xml.in index ca4ca2d74..407e56553 100644 --- a/interface-definitions/protocols_static.xml.in +++ b/interface-definitions/protocols_static.xml.in @@ -11,6 +11,62 @@ <priority>480</priority> </properties> <children> + <node name="multicast"> + <properties> + <help>Multicast static route</help> + </properties> + <children> + <tagNode name="route"> + <properties> + <help>Configure static unicast route into MRIB for multicast RPF lookup</help> + <valueHelp> + <format>ipv4net</format> + <description>Network</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop"> + <properties> + <help>Next-hop IPv4 router address</help> + <valueHelp> + <format>ipv4</format> + <description>Next-hop router address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + #include <include/static/static-route-distance.xml.i> + </children> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Next-hop IPv4 router interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Gateway interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + #include <include/static/static-route-distance.xml.i> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> #include <include/route-map.xml.i> #include <include/static/static-route.xml.i> #include <include/static/static-route6.xml.i> diff --git a/interface-definitions/protocols_static_multicast.xml.in b/interface-definitions/protocols_static_multicast.xml.in deleted file mode 100644 index caf95ed7c..000000000 --- a/interface-definitions/protocols_static_multicast.xml.in +++ /dev/null @@ -1,95 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="protocols"> - <children> - <node name="static"> - <children> - <node name="multicast" owner="${vyos_conf_scripts_dir}/protocols_static_multicast.py"> - <properties> - <help>Multicast static route</help> - <priority>481</priority> - </properties> - <children> - <tagNode name="route"> - <properties> - <help>Configure static unicast route into MRIB for multicast RPF lookup</help> - <valueHelp> - <format>ipv4net</format> - <description>Network</description> - </valueHelp> - <constraint> - <validator name="ip-prefix"/> - </constraint> - </properties> - <children> - <tagNode name="next-hop"> - <properties> - <help>Nexthop IPv4 address</help> - <valueHelp> - <format>ipv4</format> - <description>Nexthop IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - <children> - <leafNode name="distance"> - <properties> - <help>Distance value for this route</help> - <valueHelp> - <format>u32:1-255</format> - <description>Distance for this route</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-255"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <tagNode name="interface-route"> - <properties> - <help>Multicast interface based route</help> - <valueHelp> - <format>ipv4net</format> - <description>Network</description> - </valueHelp> - <constraint> - <validator name="ip-prefix"/> - </constraint> - </properties> - <children> - <tagNode name="next-hop-interface"> - <properties> - <help>Next-hop interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - <children> - <leafNode name="distance"> - <properties> - <help>Distance value for this route</help> - <valueHelp> - <format>u32:1-255</format> - <description>Distance for this route</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-255"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 5a353b110..88d131573 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -664,3 +664,468 @@ 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) -> 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 = {} + + 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 + + # 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) + 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} }) + 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_effective(vrf_vni_path): + vrf_config.update({'vni': conf.return_effective_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}) + + print('======== < > ==========') + import pprint + pprint.pprint(dict) + print('======== < > ==========') + return dict diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 92996f2ee..4450dc16b 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -420,7 +420,7 @@ def verify_common_route_maps(config): continue tmp = config[route_map] # Check if the specified route-map exists, if not error out - if dict_search(f'policy.route-map.{tmp}', config) == None: + if dict_search(f'policy.route_map.{tmp}', config) == None: raise ConfigError(f'Specified route-map "{tmp}" does not exist!') if 'redistribute' in config: @@ -434,7 +434,7 @@ def verify_route_map(route_map_name, config): recurring validation if a specified route-map exists! """ # Check if the specified route-map exists, if not error out - if dict_search(f'policy.route-map.{route_map_name}', config) == None: + if dict_search(f'policy.route_map.{route_map_name}', config) == None: raise ConfigError(f'Specified route-map "{route_map_name}" does not exist!') def verify_prefix_list(prefix_list, config, version=''): @@ -443,7 +443,7 @@ def verify_prefix_list(prefix_list, config, version=''): recurring validation if a specified prefix-list exists! """ # Check if the specified prefix-list exists, if not error out - if dict_search(f'policy.prefix-list{version}.{prefix_list}', config) == None: + if dict_search(f'policy.prefix_list{version}.{prefix_list}', config) == None: raise ConfigError(f'Specified prefix-list{version} "{prefix_list}" does not exist!') def verify_access_list(access_list, config, version=''): @@ -452,7 +452,7 @@ def verify_access_list(access_list, config, version=''): recurring validation if a specified prefix-list exists! """ # Check if the specified ACL exists, if not error out - if dict_search(f'policy.access-list{version}.{access_list}', config) == None: + if dict_search(f'policy.access_list{version}.{access_list}', config) == None: raise ConfigError(f'Specified access-list{version} "{access_list}" does not exist!') def verify_pki_certificate(config: dict, cert_name: str, no_password_protected: bool=False): @@ -537,3 +537,10 @@ def verify_eapol(config: dict): if 'ca_certificate' in config['eapol']: for ca_cert in config['eapol']['ca_certificate']: verify_pki_ca_certificate(config, ca_cert) + +def has_frr_protocol_in_dict(config_dict: dict, protocol: str, vrf: str=None) -> bool: + if vrf and protocol in (dict_search(f'vrf.name.{vrf}.protocols', config_dict) or []): + return True + if config_dict and protocol in config_dict: + return True + return False diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py new file mode 100644 index 000000000..015596a8f --- /dev/null +++ b/python/vyos/frrender.py @@ -0,0 +1,156 @@ +# 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/>. + +""" +Library used to interface with FRRs mgmtd introduced in version 10.0 +""" + +import os + +from vyos.utils.file import write_file +from vyos.utils.process import rc_cmd +from vyos.template import render_to_string +from vyos import ConfigError + +DEBUG_ON = os.path.exists('/tmp/vyos.frr.debug') +DEBUG_ON = True + +def debug(message): + if not DEBUG_ON: + return + print(message) + +pim_daemon = 'pimd' + +frr_protocols = ['babel', 'bfd', 'bgp', 'eigrp', 'isis', 'mpls', 'nhrp', + 'openfabric', 'ospf', 'ospfv3', 'pim', 'pim6', 'rip', + 'ripng', 'rpki', 'segment_routing', 'static'] + +class FRRender: + def __init__(self): + self._frr_conf = '/run/frr/config/frr.conf' + + def generate(self, config): + if not isinstance(config, dict): + raise ValueError('config must be of type dict') + + def inline_helper(config_dict) -> str: + output = '!\n' + if 'babel' in config_dict and 'deleted' not in config_dict['babel']: + output += render_to_string('frr/babeld.frr.j2', config_dict['babel']) + output += '\n' + if 'bfd' in config_dict and 'deleted' not in config_dict['bfd']: + output += render_to_string('frr/bfdd.frr.j2', config_dict['bfd']) + output += '\n' + if 'bgp' in config_dict and 'deleted' not in config_dict['bgp']: + output += render_to_string('frr/bgpd.frr.j2', config_dict['bgp']) + output += '\n' + if 'eigrp' in config_dict and 'deleted' not in config_dict['eigrp']: + output += render_to_string('frr/eigrpd.frr.j2', config_dict['eigrp']) + output += '\n' + if 'isis' in config_dict and 'deleted' not in config_dict['isis']: + output += render_to_string('frr/isisd.frr.j2', config_dict['isis']) + output += '\n' + if 'mpls' in config_dict and 'deleted' not in config_dict['mpls']: + output += render_to_string('frr/ldpd.frr.j2', config_dict['mpls']) + output += '\n' + if 'openfabric' in config_dict and 'deleted' not in config_dict['openfabric']: + output += render_to_string('frr/fabricd.frr.j2', config_dict['openfabric']) + output += '\n' + if 'ospf' in config_dict and 'deleted' not in config_dict['ospf']: + output += render_to_string('frr/ospfd.frr.j2', config_dict['ospf']) + output += '\n' + if 'ospfv3' in config_dict and 'deleted' not in config_dict['ospfv3']: + output += render_to_string('frr/ospf6d.frr.j2', config_dict['ospfv3']) + output += '\n' + if 'pim' in config_dict and 'deleted' not in config_dict['pim']: + output += render_to_string('frr/pimd.frr.j2', config_dict['pim']) + output += '\n' + if 'pim6' in config_dict and 'deleted' not in config_dict['pim6']: + output += render_to_string('frr/pim6d.frr.j2', config_dict['pim6']) + output += '\n' + if 'policy' in config_dict and len(config_dict['policy']) > 0: + output += render_to_string('frr/policy.frr.j2', config_dict['policy']) + output += '\n' + if 'rip' in config_dict and 'deleted' not in config_dict['rip']: + output += render_to_string('frr/ripd.frr.j2', config_dict['rip']) + output += '\n' + if 'ripng' in config_dict and 'deleted' not in config_dict['ripng']: + output += render_to_string('frr/ripngd.frr.j2', config_dict['ripng']) + output += '\n' + if 'rpki' in config_dict and 'deleted' not in config_dict['rpki']: + output += render_to_string('frr/rpki.frr.j2', config_dict['rpki']) + output += '\n' + if 'segment_routing' in config_dict and 'deleted' not in config_dict['segment_routing']: + output += render_to_string('frr/zebra.segment_routing.frr.j2', config_dict['segment_routing']) + output += '\n' + if 'static' in config_dict and 'deleted' not in config_dict['static']: + output += render_to_string('frr/staticd.frr.j2', config_dict['static']) + output += '\n' + return output + + debug('======< RENDERING CONFIG >======') + # 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: + output += 'agentx\n' + # Add routing protocols in global VRF + output += inline_helper(config) + # Interface configuration for EVPN is not VRF related + if 'interfaces' in config: + output += render_to_string('frr/evpn.mh.frr.j2', {'interfaces' : config['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']) + '\n' + for vrf, vrf_config in config['vrf']['name'].items(): + if 'protocols' not in vrf_config: + continue + for protocol in vrf_config['protocols']: + vrf_config['protocols'][protocol]['vrf'] = vrf + + output += inline_helper(vrf_config['protocols']) + + debug(output) + debug('======< RENDERING CONFIG COMPLETE >======') + write_file(self._frr_conf, output) + if DEBUG_ON: write_file('/tmp/frr.conf.debug', output) + + def apply(self): + count = 0 + count_max = 5 + emsg = '' + while count < count_max: + count += 1 + print('FRR: Reloading configuration', count) + + cmdline = '/usr/lib/frr/frr-reload.py --reload' + if DEBUG_ON: + cmdline += ' --debug' + rc, emsg = rc_cmd(f'{cmdline} {self._frr_conf}') + if rc != 0: + debug('FRR configuration reload failed, retrying') + continue + debug(emsg) + debug('======< DONE APPLYING CONFIG >======') + break + if count >= count_max: + raise ConfigError(emsg) + + def save_configuration(): + """ T3217: Save FRR configuration to /run/frr/config/frr.conf """ + return cmd('/usr/bin/vtysh -n --writeconfig') diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index a0c6ab055..7ea1b610e 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1945,7 +1945,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): local_preference = base_local_preference table = base_table for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set local-preference {local_preference}', config) self.assertIn(f' set table {table}', config) local_preference += 20 @@ -1958,7 +1958,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): local_preference = base_local_preference for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set local-preference {local_preference}', config) local_preference += 20 @@ -1972,7 +1972,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertIn(f' set as-path prepend {prepend}', config) for route_map in route_maps: @@ -1981,7 +1981,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() for route_map in route_maps: - config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') + config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='', endsection='^exit') self.assertNotIn(f' set', config) def sort_ip(output): diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 716d0a806..9f178c821 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -22,6 +22,7 @@ from vyos.utils.process import process_named_running PROCESS_NAME = 'bfdd' base_path = ['protocols', 'bfd'] +frr_endsection = '^ exit' dum_if = 'dum1001' vrf_name = 'red' @@ -130,7 +131,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('bfd', daemon=PROCESS_NAME) + frrconfig = self.getFRRconfig('bfd', endsection='^exit', daemon=PROCESS_NAME) for peer, peer_config in peers.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: @@ -143,8 +144,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): tmp += f' vrf {peer_config["vrf"]}' self.assertIn(tmp, frrconfig) - peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) - + peerconfig = self.getFRRconfig(f' peer {peer}', end='', endsection=frr_endsection, + daemon=PROCESS_NAME) if 'echo_mode' in peer_config: self.assertIn(f'echo-mode', peerconfig) if 'intv_echo' in peer_config: @@ -206,7 +207,7 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): # Verify FRR bgpd configuration for profile, profile_config in profiles.items(): - config = self.getFRRconfig(f' profile {profile}', endsection='^ !') + config = self.getFRRconfig(f' profile {profile}', endsection=frr_endsection) if 'echo_mode' in profile_config: self.assertIn(f' echo-mode', config) if 'intv_echo' in profile_config: @@ -228,7 +229,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertNotIn(f'shutdown', config) for peer, peer_config in peers.items(): - peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) + peerconfig = self.getFRRconfig(f' peer {peer}', end='', + endsection=frr_endsection, daemon=PROCESS_NAME) if 'profile' in peer_config: self.assertIn(f' profile {peer_config["profile"]}', peerconfig) diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index f676e2a52..980bc6e7f 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -33,7 +33,11 @@ routes = { '192.0.2.110' : { 'distance' : '110', 'interface' : 'eth0' }, '192.0.2.120' : { 'distance' : '120', 'disable' : '' }, '192.0.2.130' : { 'bfd' : '' }, - '192.0.2.140' : { 'bfd_source' : '192.0.2.10' }, + '192.0.2.131' : { 'bfd' : '', + 'bfd_profile' : 'vyos1' }, + '192.0.2.140' : { 'bfd' : '', + 'bfd_source' : '192.0.2.10', + 'bfd_profile' : 'vyos2' }, }, 'interface' : { 'eth0' : { 'distance' : '130' }, @@ -114,6 +118,45 @@ routes = { }, } +multicast_routes = { + '224.0.0.0/24' : { + 'next_hop' : { + '224.203.0.1' : { }, + '224.203.0.2' : { 'distance' : '110'}, + }, + }, + '224.1.0.0/24' : { + 'next_hop' : { + '224.205.0.1' : { 'disable' : {} }, + '224.205.0.2' : { 'distance' : '110'}, + }, + }, + '224.2.0.0/24' : { + 'next_hop' : { + '1.2.3.0' : { }, + '1.2.3.1' : { 'distance' : '110'}, + }, + }, + '224.10.0.0/24' : { + 'interface' : { + 'eth1' : { 'disable' : {} }, + 'eth2' : { 'distance' : '110'}, + }, + }, + '224.11.0.0/24' : { + 'interface' : { + 'eth0' : { }, + 'eth1' : { 'distance' : '10'}, + }, + }, + '224.12.0.0/24' : { + 'interface' : { + 'eth0' : { }, + 'eth1' : { 'distance' : '200'}, + }, + }, +} + tables = ['80', '81', '82'] class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): @@ -138,7 +181,6 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertFalse(v6route) def test_01_static(self): - bfd_profile = 'vyos-test' for route, route_config in routes.items(): route_type = 'route' if is_ipv6(route): @@ -156,9 +198,12 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): if 'vrf' in next_hop_config: self.cli_set(base + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']]) if 'bfd' in next_hop_config: - self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', bfd_profile ]) - if 'bfd_source' in next_hop_config: - self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source', next_hop_config['bfd_source'], 'profile', bfd_profile]) + self.cli_set(base + ['next-hop', next_hop, 'bfd']) + if 'bfd_profile' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', next_hop_config['bfd_profile']]) + if 'bfd_source' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop']) + self.cli_set(base + ['next-hop', next_hop, 'bfd', 'source-address', next_hop_config['bfd_source']]) if 'segments' in next_hop_config: self.cli_set(base + ['next-hop', next_hop, 'segments', next_hop_config['segments']]) @@ -217,9 +262,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): if 'vrf' in next_hop_config: tmp += ' nexthop-vrf ' + next_hop_config['vrf'] if 'bfd' in next_hop_config: - tmp += ' bfd profile ' + bfd_profile - if 'bfd_source' in next_hop_config: - tmp += ' bfd multi-hop source ' + next_hop_config['bfd_source'] + ' profile ' + bfd_profile + tmp += ' bfd' + if 'bfd_source' in next_hop_config: + tmp += ' multi-hop source ' + next_hop_config['bfd_source'] + if 'bfd_profile' in next_hop_config: + tmp += ' profile ' + next_hop_config['bfd_profile'] if 'segments' in next_hop_config: tmp += ' segments ' + next_hop_config['segments'] @@ -426,7 +473,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf') # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig(f'vrf {vrf}') + frrconfig = self.getFRRconfig(f'vrf {vrf}', endsection='^exit-vrf') self.assertIn(f'vrf {vrf}', frrconfig) # Verify routes @@ -478,5 +525,48 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) + def test_04_static_multicast(self): + for route, route_config in multicast_routes.items(): + if 'next_hop' in route_config: + base = base_path + ['multicast', 'route', route] + for next_hop, next_hop_config in route_config['next_hop'].items(): + self.cli_set(base + ['next-hop', next_hop]) + if 'distance' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'distance', next_hop_config['distance']]) + if 'disable' in next_hop_config: + self.cli_set(base + ['next-hop', next_hop, 'disable']) + + if 'interface' in route_config: + base = base_path + ['multicast', 'route', route] + for next_hop, next_hop_config in route_config['interface'].items(): + self.cli_set(base + ['interface', next_hop]) + if 'distance' in next_hop_config: + self.cli_set(base + ['interface', next_hop, 'distance', next_hop_config['distance']]) + + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('ip mroute', end='') + for route, route_config in multicast_routes.items(): + if 'next_hop' in route_config: + for next_hop, next_hop_config in route_config['next_hop'].items(): + tmp = f'ip mroute {route} {next_hop}' + if 'distance' in next_hop_config: + tmp += ' ' + next_hop_config['distance'] + if 'disable' in next_hop_config: + self.assertNotIn(tmp, frrconfig) + else: + self.assertIn(tmp, frrconfig) + + if 'next_hop_interface' in route_config: + for next_hop, next_hop_config in route_config['next_hop_interface'].items(): + tmp = f'ip mroute {route} {next_hop}' + if 'distance' in next_hop_config: + tmp += ' ' + next_hop_config['distance'] + if 'disable' in next_hop_config: + self.assertNotIn(tmp, frrconfig) + else: + self.assertIn(tmp, frrconfig) + if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_static_multicast.py b/smoketest/scripts/cli/test_protocols_static_multicast.py deleted file mode 100755 index 9fdda236f..000000000 --- a/smoketest/scripts/cli/test_protocols_static_multicast.py +++ /dev/null @@ -1,49 +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 - - -base_path = ['protocols', 'static', 'multicast'] - - -class TestProtocolsStaticMulticast(VyOSUnitTestSHIM.TestCase): - - def tearDown(self): - self.cli_delete(base_path) - self.cli_commit() - - mroute = self.getFRRconfig('ip mroute', end='') - self.assertFalse(mroute) - - def test_01_static_multicast(self): - - self.cli_set(base_path + ['route', '224.202.0.0/24', 'next-hop', '224.203.0.1']) - self.cli_set(base_path + ['interface-route', '224.203.0.0/24', 'next-hop-interface', 'eth0']) - - self.cli_commit() - - # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('ip mroute', end='') - - self.assertIn('ip mroute 224.202.0.0/24 224.203.0.1', frrconfig) - self.assertIn('ip mroute 224.203.0.0/24 eth0', frrconfig) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 633fb797c..adea8fc63 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -17,6 +17,7 @@ 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 @@ -30,10 +31,10 @@ from vyos.configverify import verify_mirror_redirect 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.ifconfig import BondIf from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig import Section -from vyos.template import render_to_string from vyos.utils.assertion import assert_mac from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values @@ -42,9 +43,9 @@ from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured from vyos.configdep import set_dependents, call_dependents from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() def get_bond_mode(mode): if mode == 'round-robin': @@ -87,10 +88,13 @@ def get_config(config=None): bond['mode'] = get_bond_mode(bond['mode']) tmp = is_node_changed(conf, base + [ifname, 'mode']) - if tmp: bond['shutdown_required'] = {} + if tmp: bond.update({'shutdown_required' : {}}) tmp = is_node_changed(conf, base + [ifname, 'lacp-rate']) - if tmp: bond['shutdown_required'] = {} + if tmp: bond.update({'shutdown_required' : {}}) + + tmp = is_node_changed(conf, base + [ifname, 'evpn']) + if tmp: bond.update({'frrender' : get_frrender_dict(conf)}) # determine which members have been removed interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) @@ -260,16 +264,16 @@ def verify(bond): return None def generate(bond): - bond['frr_zebra_config'] = '' - if 'deleted' not in bond: - bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond) + if 'frrender' in bond: + frrender.generate(bond['frrender']) return None def apply(bond): - ifname = bond['ifname'] - b = BondIf(ifname) + if 'frrender' in bond: + frrender.apply() + + b = BondIf(bond['ifname']) if 'deleted' in bond: - # delete interface b.remove() else: b.update(bond) @@ -281,16 +285,6 @@ def apply(bond): raise ConfigError('Error in updating ethernet interface ' 'after deleting it from bond') - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(frr.mgmt_daemon) - frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_zebra_config' in bond: - frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config']) - frr_cfg.commit_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index edbbb00c9..6a035e9e9 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -20,6 +20,7 @@ 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 @@ -33,17 +34,17 @@ from vyos.configverify import verify_vrf 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.ifconfig import EthernetIf from vyos.ifconfig import BondIf -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() def update_bond_options(conf: Config, eth_conf: dict) -> list: """ @@ -164,6 +165,9 @@ def get_config(config=None): tmp = is_node_changed(conf, base + [ifname, 'duplex']) if tmp: ethernet.update({'speed_duplex_changed': {}}) + tmp = is_node_changed(conf, base + [ifname, 'evpn']) + if tmp: ethernet.update({'frrender' : get_frrender_dict(conf)}) + return ethernet def verify_speed_duplex(ethernet: dict, ethtool: Ethtool): @@ -318,32 +322,20 @@ def verify_ethernet(ethernet): return None def generate(ethernet): - if 'deleted' in ethernet: - return None - - ethernet['frr_zebra_config'] = '' - if 'deleted' not in ethernet: - ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet) - + if 'frrender' in ethernet: + frrender.generate(ethernet['frrender']) return None def apply(ethernet): - ifname = ethernet['ifname'] + if 'frrender' in ethernet: + frrender.apply() - e = EthernetIf(ifname) + e = EthernetIf(ethernet['ifname']) if 'deleted' in ethernet: - # delete interface e.remove() else: e.update(ethernet) - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.mgmt_daemon) - frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_zebra_config' in ethernet: - frr_cfg.add_before(frr.default_add_before, ethernet['frr_zebra_config']) - frr_cfg.commit_configuration() + return None if __name__ == '__main__': try: diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index aef9b96c4..e6b6d474a 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -17,15 +17,16 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.template import render_to_string +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.utils.dict import dict_search from vyos import ConfigError -from vyos import frr from vyos import airbag - airbag.enable() +frrender = FRRender() def community_action_compatibility(actions: dict) -> bool: """ @@ -87,31 +88,27 @@ def get_config(config=None): else: conf = Config() - base = ['policy'] - policy = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['protocols'], key_mangling=('-', '_'), - no_tag_node_value_mangle=True) - # Merge policy dict into "regular" config dict - policy = dict_merge(tmp, policy) - return policy - - -def verify(policy): - if not policy: + return get_frrender_dict(conf) + + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'policy'): return None - for policy_type in ['access_list', 'access_list6', 'as_path_list', - 'community_list', 'extcommunity_list', - 'large_community_list', - 'prefix_list', 'prefix_list6', 'route_map']: + policy_types = ['access_list', 'access_list6', 'as_path_list', + 'community_list', 'extcommunity_list', + 'large_community_list', 'prefix_list', + 'prefix_list6', 'route_map'] + + policy = config_dict['policy'] + for protocol in frr_protocols: + if protocol not in config_dict: + continue + if 'protocol' not in policy: + policy.update({'protocol': {}}) + policy['protocol'].update({protocol : config_dict[protocol]}) + + for policy_type in policy_types: # Bail out early and continue with next policy type if policy_type not in policy: continue @@ -246,69 +243,33 @@ def verify(policy): # When the "routing policy" changes and policies, route-maps etc. are deleted, # it is our responsibility to verify that the policy can not be deleted if it # is used by any routing protocol - if 'protocols' in policy: - for policy_type in ['access_list', 'access_list6', 'as_path_list', - 'community_list', - 'extcommunity_list', 'large_community_list', - 'prefix_list', 'route_map']: - if policy_type in policy: - for policy_name in list(set(routing_policy_find(policy_type, - policy[ - 'protocols']))): - found = False - if policy_name in policy[policy_type]: - found = True - # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related - # list - we need to go the extra mile here and check both prefix-lists - if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \ - policy['prefix_list6']: - found = True - if not found: - tmp = policy_type.replace('_', '-') - raise ConfigError( - f'Can not delete {tmp} "{policy_name}", still in use!') - - return None - + # Check if any routing protocol is activated + if 'protocol' in policy: + for policy_type in policy_types: + for policy_name in list(set(routing_policy_find(policy_type, policy['protocol']))): + found = False + if policy_type in policy and policy_name in policy[policy_type]: + found = True + # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related + # list - we need to go the extra mile here and check both prefix-lists + if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \ + policy['prefix_list6']: + found = True + if not found: + tmp = policy_type.replace('_', '-') + raise ConfigError( + f'Can not delete {tmp} "{policy_name}", still in use!') -def generate(policy): - if not policy: - return None - policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy) return None -def apply(policy): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(frr.bgp_daemon) - frr_cfg.modify_section(r'^bgp as-path access-list .*') - frr_cfg.modify_section(r'^bgp community-list .*') - frr_cfg.modify_section(r'^bgp extcommunity-list .*') - frr_cfg.modify_section(r'^bgp large-community-list .*') - frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', - remove_stop_mark=True) - if 'new_frr_config' in policy: - frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) - frr_cfg.commit_configuration(frr.bgp_daemon) - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(frr.zebra_daemon) - frr_cfg.modify_section(r'^access-list .*') - frr_cfg.modify_section(r'^ipv6 access-list .*') - frr_cfg.modify_section(r'^ip prefix-list .*') - frr_cfg.modify_section(r'^ipv6 prefix-list .*') - frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', - remove_stop_mark=True) - if 'new_frr_config' in policy: - frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) - frr_cfg.commit_configuration(frr.zebra_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py index 06fd9b9b6..f9d5e45a0 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -17,63 +17,33 @@ from sys import exit from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.utils.dict import dict_search -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'babel'] - babel = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - babel['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does not exist - if not conf.exists(base): - babel.update({'deleted' : ''}) - return babel - - # We have gathered the dict representation of the CLI, but there are default - # values which we need to update into the dictionary retrieved. - default_values = conf.get_config_defaults(base, key_mangling=('-', '_'), - get_first_key=True, - recursive=True) - # merge in default values - babel = config_dict_merge(default_values, babel) + return get_frrender_dict(conf) - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - babel = dict_merge(tmp, babel) - return babel - -def verify(babel): - if not babel: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'babel'): return None + babel = config_dict['babel'] + babel['policy'] = config_dict['policy'] + # verify distribute_list if "distribute_list" in babel: acl_keys = { @@ -120,30 +90,11 @@ def verify(babel): verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '') -def generate(babel): - if not babel or 'deleted' in babel: - return None - - babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel) - return None - -def apply(babel): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(frr.babel_daemon) - frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in babel: - continue - for interface in babel[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in babel: - frr_cfg.add_before(frr.default_add_before, babel['new_frr_config']) - frr_cfg.commit_configuration(frr.babel_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index d94ec6a0d..a0d7fdfb5 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -15,36 +15,31 @@ # 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.template import is_ipv6 -from vyos.template import render_to_string from vyos.utils.network import is_ipv6_link_local from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'bfd'] - bfd = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - return bfd - - bfd = conf.merge_defaults(bfd, recursive=True) - return bfd + return get_frrender_dict(conf) -def verify(bfd): - if not bfd: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'bfd'): return None + bfd = config_dict['bfd'] if 'peer' in bfd: for peer, peer_config in bfd['peer'].items(): # IPv6 link local peers require an explicit local address/interface @@ -83,20 +78,11 @@ def verify(bfd): return None -def generate(bfd): - if not bfd: - return None - bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.j2', bfd) - -def apply(bfd): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.bfd_daemon) - frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in bfd: - frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config']) - frr_cfg.commit_configuration(frr.bfd_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index e5c46aee6..989cf9b5c 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,92 +19,35 @@ from sys import argv from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.template import is_ip from vyos.template import is_interface -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_vrf from vyos.utils.network import is_addr_assigned from vyos.utils.process import process_named_running -from vyos.utils.process import call from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + +vrf = None +if len(argv) > 1: + vrf = argv[1] + def get_config(config=None): if config: conf = config else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] - - base_path = ['protocols', 'bgp'] - - # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path - bgp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Remove per interface MPLS configuration - get a list if changed - # nodes under the interface tagNode - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - bgp['interface_removed'] = list(interfaces_removed) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: - bgp.update({'vrf' : vrf}) - # We can not delete the BGP VRF instance if there is a L3VNI configured - # FRR L3VNI must be deleted first otherwise we will see error: - # "FRR error: Please unconfigure l3vni 3000" - tmp = ['vrf', 'name', vrf, 'vni'] - if conf.exists_effective(tmp): - bgp.update({'vni' : conf.return_effective_value(tmp)}) - # We can safely delete ourself from the dependent vrf list - if vrf in bgp['dependent_vrfs']: - del bgp['dependent_vrfs'][vrf] - - bgp['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True)}}}) - - if not conf.exists(base): - # If bgp instance is deleted then mark it - bgp.update({'deleted' : ''}) - return bgp - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - bgp = conf.merge_defaults(bgp, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - bgp = dict_merge(tmp, bgp) - - return bgp - + return get_frrender_dict(conf) def verify_vrf_as_import(search_vrf_name: str, afi_name: str, vrfs_config: dict) -> bool: """ @@ -237,7 +180,18 @@ def verify_afi(peer_config, bgp_config): if tmp: return True return False -def verify(bgp): +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'bgp', vrf): + return None + + # eqivalent of the C foo ? 'a' : 'b' statement + bgp = vrf and config_dict['vrf']['name'][vrf]['protocols']['bgp'] or config_dict['bgp'] + bgp['policy'] = config_dict['policy'] + + if vrf: + bgp['vrf'] = vrf + if 'deleted' in bgp: if 'vrf' in bgp: # Cannot delete vrf if it exists in import vrf list in other vrfs @@ -252,8 +206,9 @@ def verify(bgp): for vrf, vrf_options in bgp['dependent_vrfs'].items(): if vrf != 'default': if dict_search('protocols.bgp', vrf_options): - raise ConfigError('Cannot delete default BGP instance, ' \ - 'dependent VRF instance(s) exist(s)!') + dependent_vrfs = ', '.join(bgp['dependent_vrfs'].keys()) + raise ConfigError(f'Cannot delete default BGP instance, ' \ + f'dependent VRF instance(s): {dependent_vrfs}') if 'vni' in vrf_options: raise ConfigError('Cannot delete default BGP instance, ' \ 'dependent L3VNI exists!') @@ -602,44 +557,11 @@ def verify(bgp): return None -def generate(bgp): - if not bgp or 'deleted' in bgp: - return None - - bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp) - return None - -def apply(bgp): - if 'deleted' in bgp: - # We need to ensure that the L3VNI is deleted first. - # This is not possible with old config backend - # priority bug - if {'vrf', 'vni'} <= set(bgp): - call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp)) - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in bgp: - vrf = ' vrf ' + bgp['vrf'] - - frr_cfg.load_configuration(frr.bgp_daemon) - - # Remove interface specific config - for key in ['interface', 'interface_removed']: - if key not in bgp: - continue - for interface in bgp[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_bgpd_config' in bgp: - frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config']) - frr_cfg.commit_configuration(frr.bgp_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index a948b47da..5d60e6bfd 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -18,14 +18,18 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdict import get_frrender_dict +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_vrf -from vyos.template import render_to_string +from vyos.frrender import FRRender from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() +vrf = None +if len(argv) > 1: + vrf = argv[1] def get_config(config=None): if config: @@ -33,48 +37,16 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf) - base_path = ['protocols', 'eigrp'] +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'eigrp', vrf): + return None # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path - eigrp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: eigrp.update({'vrf' : vrf}) - - if not conf.exists(base): - eigrp.update({'deleted' : ''}) - if not vrf: - # We are running in the default VRF context, thus we can not delete - # our main EIGRP instance if there are dependent EIGRP VRF instances. - eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - return eigrp - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - eigrp = dict_merge(tmp, eigrp) - - return eigrp - -def verify(eigrp): - if not eigrp or 'deleted' in eigrp: - return + eigrp = vrf and config_dict['vrf']['name'][vrf]['protocols']['eigrp'] or config_dict['eigrp'] + eigrp['policy'] = config_dict['policy'] if 'system_as' not in eigrp: raise ConfigError('EIGRP system-as must be defined!') @@ -82,28 +54,11 @@ def verify(eigrp): if 'vrf' in eigrp: verify_vrf(eigrp) -def generate(eigrp): - if not eigrp or 'deleted' in eigrp: - return None - - eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp) - -def apply(eigrp): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in eigrp: - vrf = ' vrf ' + eigrp['vrf'] - - frr_cfg.load_configuration(frr.eigrp_daemon) - frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_eigrpd_config' in eigrp: - frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config']) - frr_cfg.commit_configuration(frr.eigrp_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 9b70c7329..e812770bc 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -18,75 +18,89 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + +vrf = None +if len(argv) > 1: + vrf = argv[1] + def get_config(config=None): if config: conf = config else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf) - base_path = ['protocols', 'isis'] - # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path - isis = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: isis['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - isis['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - isis.update({'deleted' : ''}) - return isis - - # merge in default values - isis = conf.merge_defaults(isis, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - isis = dict_merge(tmp, isis) + # base_path = ['protocols', 'isis'] + + # # eqivalent of the C foo ? 'a' : 'b' statement + # base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path + # isis = conf.get_config_dict(base, key_mangling=('-', '_'), + # get_first_key=True, + # no_tag_node_value_mangle=True) + + # # Assign the name of our VRF context. This MUST be done before the return + # # statement below, else on deletion we will delete the default instance + # # instead of the VRF instance. + # if vrf: isis['vrf'] = vrf + + # # FRR has VRF support for different routing daemons. As interfaces belong + # # to VRFs - or the global VRF, we need to check for changed interfaces so + # # that they will be properly rendered for the FRR config. Also this eases + # # removal of interfaces from the running configuration. + # interfaces_removed = node_changed(conf, base + ['interface']) + # if interfaces_removed: + # isis['interface_removed'] = list(interfaces_removed) + + # # Bail out early if configuration tree does no longer exist. this must + # # be done after retrieving the list of interfaces to be removed. + # if not conf.exists(base): + # isis.update({'deleted' : ''}) + # return isis + + # # merge in default values + # isis = conf.merge_defaults(isis, recursive=True) + + # # We also need some additional information from the config, prefix-lists + # # and route-maps for instance. They will be used in verify(). + # # + # # XXX: one MUST always call this without the key_mangling() option! See + # # vyos.configverify.verify_common_route_maps() for more information. + # tmp = conf.get_config_dict(['policy']) + # # Merge policy dict into "regular" config dict + # isis = dict_merge(tmp, isis) return isis -def verify(isis): - # bail out early - looks like removal from running config - if not isis or 'deleted' in isis: +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'isis', vrf): return None + # eqivalent of the C foo ? 'a' : 'b' statement + isis = vrf and config_dict['vrf']['name'][vrf]['protocols']['isis'] or config_dict['isis'] + isis['policy'] = config_dict['policy'] + + if 'deleted' in isis: + return None + + if vrf: + isis['vrf'] = vrf + if 'net' not in isis: raise ConfigError('Network entity is mandatory!') @@ -266,37 +280,11 @@ def verify(isis): return None -def generate(isis): - if not isis or 'deleted' in isis: - return None - - isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis) - return None - -def apply(isis): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in isis: - vrf = ' vrf ' + isis['vrf'] - - frr_cfg.load_configuration(frr.isis_daemon) - frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in isis: - continue - for interface in isis[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_isisd_config' in isis: - frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config']) - - frr_cfg.commit_configuration(frr.isis_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 4a05b8044..2a691f4a4 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -20,33 +20,33 @@ from sys import exit from glob import glob from vyos.config import Config -from vyos.template import render_to_string +from vyos.configdict import get_frrender_dict +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.system import sysctl_write from vyos.configverify import verify_interface_exists from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() -config_file = r'/tmp/ldpd.frr' +frrender = FRRender() def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'mpls'] - mpls = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - return mpls + return get_frrender_dict(conf) -def verify(mpls): - # If no config, then just bail out early. - if not mpls: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'mpls'): return None + mpls = config_dict['mpls'] + if 'interface' in mpls: for interface in mpls['interface']: verify_interface_exists(mpls, interface) @@ -68,24 +68,16 @@ def verify(mpls): return None -def generate(mpls): - # If there's no MPLS config generated, create dictionary key with no value. - if not mpls or 'deleted' in mpls: - return None +def generate(config_dict): + frrender.generate(config_dict) - mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.j2', mpls) - return None +def apply(config_dict): + frrender.apply() -def apply(mpls): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(frr.ldpd_daemon) - frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True) + if not has_frr_protocol_in_dict(config_dict, 'mpls'): + return None - if 'frr_ldpd_config' in mpls: - frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config']) - frr_cfg.commit_configuration(frr.ldpd_daemon) + mpls = config_dict['mpls'] # Set number of entries in the platform label tables labels = '0' diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py index b60117a02..3dc06ee68 100644 --- a/src/conf_mode/protocols_openfabric.py +++ b/src/conf_mode/protocols_openfabric.py @@ -18,14 +18,15 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import node_changed +from vyos.configdict import get_frrender_dict from vyos.configverify import verify_interface_exists -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() def get_config(config=None): if config: @@ -33,32 +34,14 @@ def get_config(config=None): else: conf = Config() - base_path = ['protocols', 'openfabric'] + return get_frrender_dict(conf) - openfabric = conf.get_config_dict(base_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Remove per domain MPLS configuration - get a list of all changed Openfabric domains - # (removed and added) so that they will be properly rendered for the FRR config. - openfabric['domains_all'] = list(conf.list_nodes(' '.join(base_path) + f' domain') + - node_changed(conf, base_path + ['domain'])) - - # Get a list of all interfaces - openfabric['interfaces_all'] = [] - for domain in openfabric['domains_all']: - interfaces_modified = list(node_changed(conf, base_path + ['domain', domain, 'interface']) + - conf.list_nodes(' '.join(base_path) + f' domain {domain} interface')) - openfabric['interfaces_all'].extend(interfaces_modified) - - if not conf.exists(base_path): - openfabric.update({'deleted': ''}) - - return openfabric +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'openfabric'): + return None -def verify(openfabric): - # bail out early - looks like removal from running config - if not openfabric or 'deleted' in openfabric: + openfabric = config_dict['openfabric'] + if 'deleted' in openfabric: return None if 'net' not in openfabric: @@ -107,29 +90,11 @@ def verify(openfabric): return None -def generate(openfabric): - if not openfabric or 'deleted' in openfabric: - return None - - openfabric['frr_fabricd_config'] = render_to_string('frr/fabricd.frr.j2', openfabric) - return None - -def apply(openfabric): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(frr.openfabric_daemon) - for domain in openfabric['domains_all']: - frr_cfg.modify_section(f'^router openfabric {domain}', stop_pattern='^exit', remove_stop_mark=True) - - for interface in openfabric['interfaces_all']: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_fabricd_config' in openfabric: - frr_cfg.add_before(frr.default_add_before, openfabric['frr_fabricd_config']) - - frr_cfg.commit_configuration(frr.openfabric_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 44817b9b7..32ad5e497 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -18,20 +18,23 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + +vrf = None +if len(argv) > 1: + vrf = argv[1] def get_config(config=None): if config: @@ -39,85 +42,16 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf) - base_path = ['protocols', 'ospf'] +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'ospf', vrf): + return None # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path - ospf = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: ospf['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - ospf['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - ospf.update({'deleted' : ''}) - return ospf - - # 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(**ospf.kwargs, 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) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ospf = dict_merge(tmp, ospf) - - return ospf - -def verify(ospf): - if not ospf: - return None + ospf = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospf'] or config_dict['ospf'] + ospf['policy'] = config_dict['policy'] verify_common_route_maps(ospf) @@ -164,8 +98,7 @@ def verify(ospf): # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. - if 'vrf' in ospf: - vrf = ospf['vrf'] + if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') @@ -244,37 +177,11 @@ def verify(ospf): return None -def generate(ospf): - if not ospf or 'deleted' in ospf: - return None - - ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) - return None - -def apply(ospf): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in ospf: - vrf = ' vrf ' + ospf['vrf'] - - frr_cfg.load_configuration(frr.ospf_daemon) - frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in ospf: - continue - for interface in ospf[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_ospfd_config' in ospf: - frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) - - frr_cfg.commit_configuration(frr.ospf_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 7bdab3017..038fcd2b4 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -18,96 +18,41 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + +vrf = None +if len(argv) > 1: + vrf = argv[1] + def get_config(config=None): if config: conf = config else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf) - base_path = ['protocols', 'ospfv3'] +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'ospfv3', vrf): + return None # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path - ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: ospfv3['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - ospfv3['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - ospfv3.update({'deleted' : ''}) - return ospfv3 - - # 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(**ospfv3.kwargs, - 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) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ospfv3 = dict_merge(tmp, ospfv3) - - return ospfv3 - -def verify(ospfv3): - if not ospfv3: - return None + ospfv3 = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospfv3'] or config_dict['ospfv3'] + ospfv3['policy'] = config_dict['policy'] verify_common_route_maps(ospfv3) @@ -137,45 +82,18 @@ def verify(ospfv3): # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. - if 'vrf' in ospfv3: - vrf = ospfv3['vrf'] + if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') return None -def generate(ospfv3): - if not ospfv3 or 'deleted' in ospfv3: - return None - - ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3) - return None - -def apply(ospfv3): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in ospfv3: - vrf = ' vrf ' + ospfv3['vrf'] - - frr_cfg.load_configuration(frr.ospf6_daemon) - frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in ospfv3: - continue - for interface in ospfv3[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in ospfv3: - frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config']) - - frr_cfg.commit_configuration(frr.ospf6_daemon) +def generate(config_dict): + frrender.generate(config_dict) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 994007137..3862530a2 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -17,58 +17,34 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +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.utils.dict import dict_search -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'rip'] - rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - rip['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does not exist - if not conf.exists(base): - rip.update({'deleted' : ''}) - return rip - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - rip = conf.merge_defaults(rip, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - rip = dict_merge(tmp, rip) - - return rip - -def verify(rip): - if not rip: + + return get_frrender_dict(conf) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'rip'): return None + rip = config_dict['rip'] + rip['policy'] = config_dict['policy'] + verify_common_route_maps(rip) acl_in = dict_search('distribute_list.access_list.in', rip) @@ -93,33 +69,11 @@ def verify(rip): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') -def generate(rip): - if not rip or 'deleted' in rip: - return None - - rip['new_frr_config'] = render_to_string('frr/ripd.frr.j2', rip) - return None +def generate(frr_dict): + frrender.generate(frr_dict) def apply(rip): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the mgmt daemon as of FRR 10.1 - frr_cfg.load_configuration(frr.mgmt_daemon) - frr_cfg.modify_section('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in rip: - continue - for interface in rip[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in rip: - frr_cfg.add_before(frr.default_add_before, rip['new_frr_config']) - frr_cfg.commit_configuration() - + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index 9d4447d1f..327a0d239 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -17,48 +17,34 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +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.utils.dict import dict_search -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'ripng'] - ripng = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # Bail out early if configuration tree does not exist - if not conf.exists(base): - return ripng - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - ripng = conf.merge_defaults(ripng, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ripng = dict_merge(tmp, ripng) - - return ripng - -def verify(ripng): - if not ripng: + + return get_frrender_dict(conf) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ripng'): return None + ripng = config_dict['ripng'] + ripng['policy'] = config_dict['policy'] + verify_common_route_maps(ripng) acl_in = dict_search('distribute_list.access_list.in', ripng) @@ -83,25 +69,12 @@ def verify(ripng): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') -def generate(ripng): - if not ripng: - return None - ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.j2', ripng) - -def apply(ripng): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(frr.mgmt_daemon) - frr_cfg.modify_section('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in ripng: - frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config']) - frr_cfg.commit_configuration() +def generate(config_dict): + frrender.generate(config_dict) + return None +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index bec0cda91..c75f95860 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -20,15 +20,17 @@ 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.pki import wrap_openssh_public_key from vyos.pki import wrap_openssh_private_key -from vyos.template import render_to_string from vyos.utils.dict import dict_search_args from vyos.utils.file import write_file from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() rpki_ssh_key_base = '/run/frr/id_rpki' @@ -37,25 +39,14 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'rpki'] + return get_frrender_dict(conf) - rpki = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, with_pki=True) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - rpki.update({'deleted' : ''}) - return rpki - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - rpki = conf.merge_defaults(rpki, recursive=True) - - return rpki - -def verify(rpki): - if not rpki: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'rpki'): return None + rpki = config_dict['rpki'] + if 'cache' in rpki: preferences = [] for peer, peer_config in rpki['cache'].items(): @@ -81,12 +72,14 @@ def verify(rpki): return None -def generate(rpki): +def generate(config_dict): for key in glob(f'{rpki_ssh_key_base}*'): os.unlink(key) - if not rpki: - return + if not has_frr_protocol_in_dict(config_dict, 'rpki'): + return None + + rpki = config_dict['rpki'] if 'cache' in rpki: for cache, cache_config in rpki['cache'].items(): @@ -102,19 +95,11 @@ def generate(rpki): write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type)) write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data)) - rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki) - + frrender.generate(config_dict) return None -def apply(rpki): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.bgp_daemon) - frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in rpki: - frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config']) - - frr_cfg.commit_configuration(frr.bgp_daemon) +def apply(config_dict): + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py index 67f8005ef..0fad968e1 100755 --- a/src/conf_mode/protocols_segment-routing.py +++ b/src/conf_mode/protocols_segment-routing.py @@ -17,14 +17,17 @@ from sys import exit from vyos.config import Config -from vyos.configdict import node_changed -from vyos.template import render_to_string +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.ifconfig import Section from vyos.utils.dict import dict_search from vyos.utils.system import sysctl_write from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() def get_config(config=None): if config: @@ -32,25 +35,14 @@ def get_config(config=None): else: conf = Config() - base = ['protocols', 'segment-routing'] - sr = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) + return get_frrender_dict(conf) - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - sr['interface_removed'] = list(interfaces_removed) +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): + return None - import pprint - pprint.pprint(sr) - return sr + sr = config_dict['segment_routing'] -def verify(sr): if 'srv6' in sr: srv6_enable = False if 'interface' in sr: @@ -62,45 +54,41 @@ def verify(sr): raise ConfigError('SRv6 should be enabled on at least one interface!') return None -def generate(sr): - if not sr: +def generate(config_dict): + frrender.generate(config_dict) + return None + +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): return None - sr['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', sr) - return None + sr = config_dict['segment_routing'] -def apply(sr): - if 'interface_removed' in sr: - for interface in sr['interface_removed']: - # Disable processing of IPv6-SR packets - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + current_interfaces = Section.interfaces() + sr_interfaces = list(sr.get('interface', {}).keys()) - if 'interface' in sr: - for interface, interface_config in sr['interface'].items(): - # Accept or drop SR-enabled IPv6 packets on this interface - if 'srv6' in interface_config: - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1') - # Define HMAC policy for ingress SR-enabled packets on this interface - # It's a redundant check as HMAC has a default value - but better safe - # then sorry - tmp = dict_search('srv6.hmac', interface_config) - if tmp == 'accept': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0') - elif tmp == 'drop': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1') - elif tmp == 'ignore': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1') - else: - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + for interface in list_diff(current_interfaces, sr_interfaces): + # Disable processing of IPv6-SR packets + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.zebra_daemon) - frr_cfg.modify_section(r'^segment-routing') - if 'new_frr_config' in sr: - frr_cfg.add_before(frr.default_add_before, sr['new_frr_config']) - frr_cfg.commit_configuration(frr.zebra_daemon) + for interface, interface_config in sr.get('interface', {}).items(): + # Accept or drop SR-enabled IPv6 packets on this interface + if 'srv6' in interface_config: + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1') + # Define HMAC policy for ingress SR-enabled packets on this interface + # It's a redundant check as HMAC has a default value - but better safe + # then sorry + tmp = dict_search('srv6.hmac', interface_config) + if tmp == 'accept': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0') + elif tmp == 'drop': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1') + elif tmp == 'ignore': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1') + else: + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index c5dc77fd2..1e498b256 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -14,21 +14,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from ipaddress import IPv4Network from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import get_dhcp_interfaces -from vyos.configdict import get_pppoe_interfaces +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.template import render -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() + +vrf = None +if len(argv) > 1: + vrf = argv[1] config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' @@ -38,36 +42,17 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf) + +def verify(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'static', vrf): + return None - base_path = ['protocols', 'static'] # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path - static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - - # Assign the name of our VRF context - if vrf: static['vrf'] = vrf - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - static = dict_merge(tmp, static) - - # T3680 - get a list of all interfaces currently configured to use DHCP - tmp = get_dhcp_interfaces(conf, vrf) - if tmp: static.update({'dhcp' : tmp}) - tmp = get_pppoe_interfaces(conf, vrf) - if tmp: static.update({'pppoe' : tmp}) - - return static - -def verify(static): + static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] + static['policy'] = config_dict['policy'] + verify_common_route_maps(static) for route in ['route', 'route6']: @@ -90,33 +75,28 @@ def verify(static): raise ConfigError(f'Can not use both blackhole and reject for '\ f'prefix "{prefix}"!') + if 'multicast' in static and 'route' in static['multicast']: + for prefix, prefix_options in static['multicast']['route'].items(): + if not IPv4Network(prefix).is_multicast: + raise ConfigError(f'{prefix} is not a multicast network!') + return None -def generate(static): - if not static: +def generate(config_dict): + global vrf + if not has_frr_protocol_in_dict(config_dict, 'static', vrf): return None + # eqivalent of the C foo ? 'a' : 'b' statement + static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] + # Put routing table names in /etc/iproute2/rt_tables render(config_file, 'iproute2/static.conf.j2', static) - static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) + frrender.generate(config_dict) return None def apply(static): - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.mgmt_daemon) - - if 'vrf' in static: - vrf = static['vrf'] - frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True) - else: - frr_cfg.modify_section(r'^ip route .*') - frr_cfg.modify_section(r'^ipv6 route .*') - - if 'new_frr_config' in static: - frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) - frr_cfg.commit_configuration() - + frrender.apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py deleted file mode 100755 index 4393f3ed3..000000000 --- a/src/conf_mode/protocols_static_multicast.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-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/>. - - -from ipaddress import IPv4Address -from sys import exit - -from vyos import ConfigError -from vyos import frr -from vyos.config import Config -from vyos.template import render_to_string - -from vyos import airbag -airbag.enable() - -config_file = r'/tmp/static_mcast.frr' - -# Get configuration for static multicast route -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - mroute = { - 'old_mroute' : {}, - 'mroute' : {} - } - - base_path = "protocols static multicast" - - if not (conf.exists(base_path) or conf.exists_effective(base_path)): - return None - - conf.set_level(base_path) - - # Get multicast effective routes - for route in conf.list_effective_nodes('route'): - mroute['old_mroute'][route] = {} - for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)): - mroute['old_mroute'][route].update({ - next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) - }) - - # Get multicast effective interface-routes - for route in conf.list_effective_nodes('interface-route'): - if not route in mroute['old_mroute']: - mroute['old_mroute'][route] = {} - for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)): - mroute['old_mroute'][route].update({ - next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) - }) - - # Get multicast routes - for route in conf.list_nodes('route'): - mroute['mroute'][route] = {} - for next_hop in conf.list_nodes('route {0} next-hop'.format(route)): - mroute['mroute'][route].update({ - next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) - }) - - # Get multicast interface-routes - for route in conf.list_nodes('interface-route'): - if not route in mroute['mroute']: - mroute['mroute'][route] = {} - for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)): - mroute['mroute'][route].update({ - next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) - }) - - return mroute - -def verify(mroute): - if mroute is None: - return None - - for mcast_route in mroute['mroute']: - route = mcast_route.split('/') - if IPv4Address(route[0]) < IPv4Address('224.0.0.0'): - raise ConfigError(f'{mcast_route} not a multicast network') - - -def generate(mroute): - if mroute is None: - return None - - mroute['new_frr_config'] = render_to_string('frr/static_mcast.frr.j2', mroute) - return None - - -def apply(mroute): - if mroute is None: - return None - - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.mgmt_daemon) - - if 'old_mroute' in mroute: - for route_gr in mroute['old_mroute']: - for nh in mroute['old_mroute'][route_gr]: - if mroute['old_mroute'][route_gr][nh]: - frr_cfg.modify_section(f'^ip mroute {route_gr} {nh} {mroute["old_mroute"][route_gr][nh]}') - else: - frr_cfg.modify_section(f'^ip mroute {route_gr} {nh}') - - if 'new_frr_config' in mroute: - frr_cfg.add_before(frr.default_add_before, mroute['new_frr_config']) - - frr_cfg.commit_configuration() - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index 134662f85..1174b1238 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.py @@ -34,7 +34,6 @@ from vyos.utils.process import call from vyos.utils.permission import chmod_755 from vyos.version import get_version_data from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -261,16 +260,6 @@ def apply(snmp): # start SNMP daemon call(f'systemctl reload-or-restart {systemd_service}') - - # Enable AgentX in FRR - # This should be done for each daemon individually because common command - # works only if all the daemons started with SNMP support - # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS - frr_daemons_list = [frr.zebra_daemon, frr.bgp_daemon, frr.ospf_daemon, frr.ospf6_daemon, - frr.rip_daemon, frr.isis_daemon, frr.ldpd_daemon] - for frr_daemon in frr_daemons_list: - call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null') - return None if __name__ == '__main__': diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 3d3845fdf..a13bb8b1e 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -19,13 +19,14 @@ from jmespath import search from json import loads from vyos.config import Config +from vyos.configdict import get_frrender_dict from vyos.configdict import dict_merge 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.ifconfig import Interface from vyos.template import render -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members @@ -35,9 +36,9 @@ from vyos.utils.process import cmd from vyos.utils.process import popen from vyos.utils.system import sysctl_write from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() +frrender = FRRender() config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' k_mod = ['vrf'] @@ -132,6 +133,10 @@ def get_config(config=None): if 'name' in vrf: vrf['conntrack'] = conntrack_required(conf) + # We need to merge the FRR rendering dict into the VRF dict + # this is required to get the route-map information to FRR + vrf.update({'frrender' : get_frrender_dict(conf)}) + # We also need the route-map information from the config # # XXX: one MUST always call this without the key_mangling() option! See @@ -204,8 +209,9 @@ def verify(vrf): def generate(vrf): # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) - # Render VRF Kernel/Zebra route-map filters - vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) + + if 'frrender' in vrf: + frrender.generate(vrf['frrender']) return None @@ -347,13 +353,8 @@ def apply(vrf): if has_rule(afi, 2000, 'l3mdev'): call(f'ip {afi} rule del pref 2000 l3mdev unreachable') - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr.mgmt_daemon) - frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) - if 'frr_zebra_config' in vrf: - frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config']) - frr_cfg.commit_configuration() + if 'frrender' in vrf: + frrender.apply() return None |