diff --git a/data/configd-include.json b/data/configd-include.json index 84bc1f14e..a762a6d4c 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -1,89 +1,88 @@ [ "arp.py", "bcast_relay.py", "container.py", "conntrack.py", "conntrack_sync.py", "dhcp_relay.py", "dhcp_server.py", "dhcpv6_relay.py", "dhcpv6_server.py", "dns_forwarding.py", "dns_dynamic.py", "firewall.py", "flow_accounting_conf.py", "high-availability.py", "host_name.py", "igmp_proxy.py", "intel_qat.py", "interfaces-bonding.py", "interfaces-bridge.py", "interfaces-dummy.py", "interfaces-ethernet.py", "interfaces-geneve.py", "interfaces-l2tpv3.py", "interfaces-loopback.py", "interfaces-macsec.py", "interfaces-openvpn.py", "interfaces-pppoe.py", "interfaces-pseudo-ethernet.py", "interfaces-sstpc.py", "interfaces-tunnel.py", "interfaces-vti.py", "interfaces-vxlan.py", "interfaces-wireguard.py", "interfaces-wireless.py", "interfaces-wwan.py", "lldp.py", "nat.py", "nat66.py", "netns.py", "ntp.py", "pki.py", "policy.py", "policy-local-route.py", "protocols_bfd.py", "protocols_bgp.py", -"protocols_igmp.py", "protocols_isis.py", "protocols_mpls.py", "protocols_nhrp.py", "protocols_ospf.py", "protocols_ospfv3.py", "protocols_pim.py", "protocols_rip.py", "protocols_ripng.py", "protocols_rpki.py", "protocols_static.py", "protocols_static_multicast.py", "qos.py", "salt-minion.py", "service_console-server.py", "service_ids_fastnetmon.py", "service_ipoe-server.py", "service_mdns-repeater.py", "service_monitoring_telegraf.py", "service_pppoe-server.py", "service_router-advert.py", "service_upnp.py", "ssh.py", "system-ip.py", "system-ipv6.py", "system-login-banner.py", "system-logs.py", "system-option.py", "system-proxy.py", "system_sflow.py", "system_sysctl.py", "system-syslog.py", "system-timezone.py", "system_console.py", "system_lcd.py", "task_scheduler.py", "tftp_server.py", "vpn_l2tp.py", "vpn_pptp.py", "vpn_sstp.py", "vrf.py", "vrf_vni.py" ] diff --git a/data/templates/frr/igmp.frr.j2 b/data/templates/frr/igmp.frr.j2 deleted file mode 100644 index b75884484..000000000 --- a/data/templates/frr/igmp.frr.j2 +++ /dev/null @@ -1,41 +0,0 @@ -! -{% for iface in old_ifaces %} -interface {{ iface }} -{% for group in old_ifaces[iface].gr_join %} -{% if old_ifaces[iface].gr_join[group] %} -{% for source in old_ifaces[iface].gr_join[group] %} - no ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} - no ip igmp join {{ group }} -{% endif %} -{% endfor %} - no ip igmp -! -{% endfor %} -{% for interface, interface_config in ifaces.items() %} -interface {{ interface }} -{% if interface_config.version %} - ip igmp version {{ interface_config.version }} -{% else %} -{# IGMP default version 3 #} - ip igmp -{% endif %} -{% if interface_config.query_interval %} - ip igmp query-interval {{ interface_config.query_interval }} -{% endif %} -{% if interface_config.query_max_resp_time %} - ip igmp query-max-response-time {{ interface_config.query_max_resp_time }} -{% endif %} -{% for group, sources in interface_config.gr_join.items() %} -{% if sources is vyos_defined %} -{% for source in sources %} - ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} - ip igmp join {{ group }} -{% endif %} -{% endfor %} -! -{% endfor %} -! diff --git a/data/templates/frr/pim6d.frr.j2 b/data/templates/frr/pim6d.frr.j2 index 8e430541d..bac716fcc 100644 --- a/data/templates/frr/pim6d.frr.j2 +++ b/data/templates/frr/pim6d.frr.j2 @@ -1,38 +1,81 @@ ! {% if interface is vyos_defined %} {% for iface, iface_config in interface.items() %} +! interface {{ iface }} + ipv6 pim +{% if iface_config.no_bsm is vyos_defined %} + no ipv6 pim bsm +{% endif %} +{% if iface_config.dr_priority is vyos_defined %} + ipv6 pim drpriority {{ iface_config.dr_priority }} +{% endif %} +{% if iface_config.hello is vyos_defined %} + ipv6 pim hello {{ iface_config.hello }} +{% endif %} +{% if iface_config.no_unicast_bsm is vyos_defined %} + no ipv6 pim unicast-bsm +{% endif %} +{% if iface_config.passive is vyos_defined %} + ipv6 pim passive +{% endif %} {% if iface_config.mld is vyos_defined and iface_config.mld.disable is not vyos_defined %} ipv6 mld {% if iface_config.mld.version is vyos_defined %} ipv6 mld version {{ iface_config.mld.version }} {% endif %} {% if iface_config.mld.interval is vyos_defined %} ipv6 mld query-interval {{ iface_config.mld.interval }} {% endif %} {% if iface_config.mld.max_response_time is vyos_defined %} ipv6 mld query-max-response-time {{ iface_config.mld.max_response_time // 100 }} {% endif %} {% if iface_config.mld.last_member_query_count is vyos_defined %} ipv6 mld last-member-query-count {{ iface_config.mld.last_member_query_count }} {% endif %} {% if iface_config.mld.last_member_query_interval is vyos_defined %} ipv6 mld last-member-query-interval {{ iface_config.mld.last_member_query_interval // 100 }} {% endif %} {% if iface_config.mld.join is vyos_defined %} {% for group, group_config in iface_config.mld.join.items() %} {% if group_config.source is vyos_defined %} {% for source in group_config.source %} ipv6 mld join {{ group }} {{ source }} {% endfor %} {% else %} ipv6 mld join {{ group }} {% endif %} {% endfor %} {% endif %} {% endif %} exit -! {% endfor %} +{% endif %} ! +{% if join_prune_interval is vyos_defined %} +ipv6 pim join-prune-interval {{ join_prune_interval }} +{% endif %} +{% if keep_alive_timer is vyos_defined %} +ipv6 pim keep-alive-timer {{ keep_alive_timer }} +{% endif %} +{% if packets is vyos_defined %} +ipv6 pim packets {{ packets }} +{% endif %} +{% if register_suppress_time is vyos_defined %} +ipv6 pim register-suppress-time {{ register_suppress_time }} +{% endif %} +{% if rp.address is vyos_defined %} +{% for address, address_config in rp.address.items() %} +{% if address_config.group is vyos_defined %} +{% for group in address_config.group %} +ipv6 pim rp {{ address }} {{ group }} +{% endfor %} +{% endif %} +{% if address_config.prefix_list6 is vyos_defined %} +ipv6 pim rp {{ address }} prefix-list {{ address_config.prefix_list6 }} +{% endif %} +{% endfor %} +{% endif %} +{% if rp.keep_alive_timer is vyos_defined %} +ipv6 pim rp keep-alive-timer {{ rp.keep_alive_timer }} {% endif %} diff --git a/data/templates/frr/pimd.frr.j2 b/data/templates/frr/pimd.frr.j2 index cb2f2aa98..68edf4a5c 100644 --- a/data/templates/frr/pimd.frr.j2 +++ b/data/templates/frr/pimd.frr.j2 @@ -1,34 +1,95 @@ +{% if interface is vyos_defined %} +{% for iface, iface_config in interface.items() %} ! -{% for rp_addr in old_pim.rp %} -{% for group in old_pim.rp[rp_addr] %} -no ip pim rp {{ rp_addr }} {{ group }} +interface {{ iface }} + ip pim +{% if iface_config.bfd is vyos_defined %} + ip pim bfd {{ 'profile ' ~ iface_config.bfd.profile if iface_config.bfd.profile is vyos_defined }} +{% endif %} +{% if iface_config.no_bsm is vyos_defined %} + no ip pim bsm +{% endif %} +{% if iface_config.dr_priority is vyos_defined %} + ip pim drpriority {{ iface_config.dr_priority }} +{% endif %} +{% if iface_config.hello is vyos_defined %} + ip pim hello {{ iface_config.hello }} +{% endif %} +{% if iface_config.no_unicast_bsm is vyos_defined %} + no ip pim unicast-bsm +{% endif %} +{% if iface_config.passive is vyos_defined %} + ip pim passive +{% endif %} +{% if iface_config.source_address is vyos_defined %} + ip pim use-source {{ iface_config.source_address }} +{% endif %} +{% if iface_config.igmp is vyos_defined and iface_config.igmp.disable is not vyos_defined %} + ip igmp +{% if iface_config.igmp.query_interval %} + ip igmp query-interval {{ iface_config.igmp.query_interval }} +{% endif %} +{% if iface_config.igmp.query_max_response_time %} + ip igmp query-max-response-time {{ iface_config.igmp.query_max_response_time }} +{% endif %} +{% if iface_config.igmp.version is vyos_defined %} + ip igmp version {{ iface_config.igmp.version }} +{% endif %} +{% if iface_config.igmp.join is vyos_defined %} +{% for join, join_config in iface_config.igmp.join.items() %} +{% if join_config.source_address is vyos_defined %} +{% for source_address in join_config.source_address %} + ip igmp join {{ join }} {{ source_address }} +{% endfor %} +{% else %} + ip igmp join {{ join }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} +exit {% endfor %} -{% endfor %} -{% if old_pim.rp_keep_alive %} -no ip pim rp keep-alive-timer {{ old_pim.rp_keep_alive }} {% endif %} -{% for iface in old_pim.ifaces %} -interface {{ iface }} -no ip pim -! -{% endfor %} -{% for iface in pim.ifaces %} -interface {{ iface }} -ip pim -{% if pim.ifaces[iface].dr_prio %} -ip pim drpriority {{ pim.ifaces[iface].dr_prio }} -{% endif %} -{% if pim.ifaces[iface].hello %} -ip pim hello {{ pim.ifaces[iface].hello }} -{% endif %} ! -{% endfor %} -{% for rp_addr in pim.rp %} -{% for group in pim.rp[rp_addr] %} -ip pim rp {{ rp_addr }} {{ group }} +{% if ecmp is vyos_defined %} +ip pim ecmp {{ 'rebalance' if ecmp.rebalance is vyos_defined }} +{% endif %} +{% if join_prune_interval is vyos_defined %} +ip pim join-prune-interval {{ join_prune_interval }} +{% endif %} +{% if keep_alive_timer is vyos_defined %} +ip pim keep-alive-timer {{ keep_alive_timer }} +{% endif %} +{% if packets is vyos_defined %} +ip pim packets {{ packets }} +{% endif %} +{% if register_accept_list.prefix_list is vyos_defined %} +ip pim register-accept-list {{ register_accept_list.prefix_list }} +{% endif %} +{% if register_suppress_time is vyos_defined %} +ip pim register-suppress-time {{ register_suppress_time }} +{% endif %} +{% if rp.address is vyos_defined %} +{% for address, address_config in rp.address.items() %} +{% for group in address_config.group %} +ip pim rp {{ address }} {{ group }} +{% endfor %} {% endfor %} -{% endfor %} -{% if pim.rp_keep_alive %} -ip pim rp keep-alive-timer {{ pim.rp_keep_alive }} +{% endif %} +{% if rp.keep_alive_timer is vyos_defined %} +ip pim rp keep-alive-timer {{ rp.keep_alive_timer }} +{% endif %} +{% if no_v6_secondary is vyos_defined %} +no ip pim send-v6-secondary +{% endif %} +{% if spt_switchover.infinity_and_beyond is vyos_defined %} +ip pim spt-switchover infinity-and-beyond {{ 'prefix-list ' ~ spt_switchover.infinity_and_beyond.prefix_list if spt_switchover.infinity_and_beyond.prefix_list is defined }} +{% endif %} +{% if ssm.prefix_list is vyos_defined %} +ip pim ssm prefix-list {{ ssm.prefix_list }} +{% endif %} +! +{% if igmp.watermark_warning is vyos_defined %} +ip igmp watermark-warn {{ igmp.watermark_warning }} {% endif %} ! diff --git a/interface-definitions/include/pim/bsm.xml.i b/interface-definitions/include/pim/bsm.xml.i new file mode 100644 index 000000000..cc2cf14ca --- /dev/null +++ b/interface-definitions/include/pim/bsm.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pim/bsm.xml.i --> +<leafNode name="no-bsm"> + <properties> + <help>Do not process bootstrap messages</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="no-unicast-bsm"> + <properties> + <help>Do not process unicast bootstrap messages</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/dr-priority.xml.i b/interface-definitions/include/pim/dr-priority.xml.i new file mode 100644 index 000000000..e4b3067c2 --- /dev/null +++ b/interface-definitions/include/pim/dr-priority.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pim/dr-priority.xml.i --> +<leafNode name="dr-priority"> + <properties> + <help>Designated router election priority</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>DR Priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/hello.xml.i b/interface-definitions/include/pim/hello.xml.i new file mode 100644 index 000000000..0c7601be7 --- /dev/null +++ b/interface-definitions/include/pim/hello.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pim/hello.xml.i --> +<leafNode name="hello"> + <properties> + <help>Hello Interval</help> + <valueHelp> + <format>u32:1-180</format> + <description>Hello Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-180"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/join-prune-interval.xml.i b/interface-definitions/include/pim/join-prune-interval.xml.i new file mode 100644 index 000000000..882787d3f --- /dev/null +++ b/interface-definitions/include/pim/join-prune-interval.xml.i @@ -0,0 +1,15 @@ +<!-- include start from pim/join-prune-interval.xml.i --> +<leafNode name="join-prune-interval"> + <properties> + <help>Join prune send interval</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>60</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/keep-alive-timer.xml.i b/interface-definitions/include/pim/keep-alive-timer.xml.i new file mode 100644 index 000000000..0dd27d6e7 --- /dev/null +++ b/interface-definitions/include/pim/keep-alive-timer.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pim/keep-alive-timer.xml.i --> +<leafNode name="keep-alive-timer"> + <properties> + <help>Keep alive Timer</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Keep alive Timer in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/packets.xml.i b/interface-definitions/include/pim/packets.xml.i new file mode 100644 index 000000000..1dc00c971 --- /dev/null +++ b/interface-definitions/include/pim/packets.xml.i @@ -0,0 +1,15 @@ +<!-- include start from pim/packets.xml.i --> +<leafNode name="packets"> + <properties> + <help>Packets to process at once</help> + <valueHelp> + <format>u32:1-255</format> + <description>Number of packets</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>3</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/passive.xml.i b/interface-definitions/include/pim/passive.xml.i new file mode 100644 index 000000000..e4e9ca0b1 --- /dev/null +++ b/interface-definitions/include/pim/passive.xml.i @@ -0,0 +1,8 @@ +<!-- include start from pim/passive.xml.i --> +<leafNode name="passive"> + <properties> + <help>Disable sending and receiving PIM control packets on the interface</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pim/register-suppress-time.xml.i b/interface-definitions/include/pim/register-suppress-time.xml.i new file mode 100644 index 000000000..919945b52 --- /dev/null +++ b/interface-definitions/include/pim/register-suppress-time.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pim/register-suppress-time.xml.i --> +<leafNode name="register-suppress-time"> + <properties> + <help>Register suppress timer</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Timer in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/policy/prefix-list.xml.i b/interface-definitions/include/policy/prefix-list.xml.i new file mode 100644 index 000000000..5d7980ee2 --- /dev/null +++ b/interface-definitions/include/policy/prefix-list.xml.i @@ -0,0 +1,14 @@ +<!-- include start from policy/prefix-list.xml.i --> +<leafNode name="prefix-list"> + <properties> + <help>Prefix-list to use</help> + <valueHelp> + <format>txt</format> + <description>Prefix-list to apply (IPv4)</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/policy/prefix-list6.xml.i b/interface-definitions/include/policy/prefix-list6.xml.i new file mode 100644 index 000000000..101702f1f --- /dev/null +++ b/interface-definitions/include/policy/prefix-list6.xml.i @@ -0,0 +1,14 @@ +<!-- include start from policy/prefix-list6.xml.i --> +<leafNode name="prefix-list6"> + <properties> + <help>Prefix-list to use</help> + <valueHelp> + <format>txt</format> + <description>Prefix-list to apply (IPv6)</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/source-address-ipv4-multi.xml.i b/interface-definitions/include/source-address-ipv4-multi.xml.i new file mode 100644 index 000000000..319a118f3 --- /dev/null +++ b/interface-definitions/include/source-address-ipv4-multi.xml.i @@ -0,0 +1,18 @@ +<!-- include start from source-address-ipv4-multi.xml.i --> +<leafNode name="source-address"> + <properties> + <help>IPv4 source address used to initiate connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/pim-version.xml.i b/interface-definitions/include/version/pim-version.xml.i new file mode 100644 index 000000000..24cc38cdf --- /dev/null +++ b/interface-definitions/include/version/pim-version.xml.i @@ -0,0 +1,3 @@ +<!-- include start from include/version/pim-version.xml.i --> +<syntaxVersion component='pim' version='1'></syntaxVersion> +<!-- include end --> diff --git a/interface-definitions/protocols-igmp.xml.in b/interface-definitions/protocols-igmp.xml.in deleted file mode 100644 index a055db71e..000000000 --- a/interface-definitions/protocols-igmp.xml.in +++ /dev/null @@ -1,95 +0,0 @@ -<?xml version="1.0"?> -<!-- Internet Group Management Protocol (IGMP) configuration --> -<interfaceDefinition> - <node name="protocols"> - <children> - <node name="igmp" owner="${vyos_conf_scripts_dir}/protocols_igmp.py"> - <properties> - <help>Internet Group Management Protocol (IGMP)</help> - </properties> - <children> - <tagNode name="interface"> - <properties> - <help>IGMP interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - <children> - <tagNode name="join"> - <properties> - <help>IGMP join multicast group</help> - <valueHelp> - <format>ipv4</format> - <description>Multicast group address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - <children> - <leafNode name="source"> - <properties> - <help>Source address</help> - <valueHelp> - <format>ipv4</format> - <description>Source address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - </children> - </tagNode> - <leafNode name="version"> - <properties> - <help>IGMP version</help> - <completionHelp> - <list>2 3</list> - </completionHelp> - <valueHelp> - <format>2</format> - <description>IGMP version 2</description> - </valueHelp> - <valueHelp> - <format>3</format> - <description>IGMP version 3</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 2-3"/> - </constraint> - </properties> - </leafNode> - <leafNode name="query-interval"> - <properties> - <help>IGMP host query interval</help> - <valueHelp> - <format>u32:1-1800</format> - <description>Query interval in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-1800"/> - </constraint> - </properties> - </leafNode> - <leafNode name="query-max-response-time"> - <properties> - <help>IGMP max query response time</help> - <valueHelp> - <format>u32:10-250</format> - <description>Query response value in deci-seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 10-250"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in index e9475930c..4a20c0d9b 100644 --- a/interface-definitions/protocols-pim.xml.in +++ b/interface-definitions/protocols-pim.xml.in @@ -1,96 +1,210 @@ <?xml version="1.0"?> <!-- Protocol Independent Multicast (PIM) configuration --> <interfaceDefinition> <node name="protocols"> <children> <node name="pim" owner="${vyos_conf_scripts_dir}/protocols_pim.py"> <properties> - <help>Protocol Independent Multicast (PIM)</help> + <help>Protocol Independent Multicast (PIM) and IGMP</help> <priority>400</priority> </properties> <children> <tagNode name="interface"> <properties> <help>PIM interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> </properties> <children> - <leafNode name="dr-priority"> + #include <include/bfd/bfd.xml.i> + #include <include/pim/bsm.xml.i> + #include <include/pim/dr-priority.xml.i> + #include <include/pim/hello.xml.i> + #include <include/pim/passive.xml.i> + #include <include/source-address-ipv4.xml.i> + <node name="igmp"> + <properties> + <help>Internet Group Management Protocol (IGMP) options</help> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <tagNode name="join"> + <properties> + <help>IGMP join multicast group</help> + <valueHelp> + <format>ipv4</format> + <description>Multicast group address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + #include <include/source-address-ipv4-multi.xml.i> + </children> + </tagNode> + <leafNode name="query-interval"> + <properties> + <help>IGMP host query interval</help> + <valueHelp> + <format>u32:1-1800</format> + <description>Query interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1800"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-max-response-time"> + <properties> + <help>IGMP max query response time</help> + <valueHelp> + <format>u32:10-250</format> + <description>Query response value in deci-seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-250"/> + </constraint> + </properties> + </leafNode> + <leafNode name="version"> + <properties> + <help>Interface IGMP version</help> + <completionHelp> + <list>2 3</list> + </completionHelp> + <valueHelp> + <format>2</format> + <description>IGMP version 2</description> + </valueHelp> + <valueHelp> + <format>3</format> + <description>IGMP version 3</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-3"/> + </constraint> + </properties> + <defaultValue>3</defaultValue> + </leafNode> + </children> + </node> + </children> + </tagNode> + <node name="ecmp"> + <properties> + <help>Enable PIM ECMP</help> + </properties> + <children> + <leafNode name="rebalance"> <properties> - <help>Designated Router Election Priority</help> - <valueHelp> - <format>u32:1-4294967295</format> - <description>Value of the new DR Priority</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967295"/> - </constraint> + <help>Enable PIM ECMP Rebalance</help> + <valueless/> </properties> </leafNode> - <leafNode name="hello"> + </children> + </node> + <node name="igmp"> + <properties> + <help>Internet Group Management Protocol (IGMP) options</help> + </properties> + <children> + <leafNode name="watermark-warning"> <properties> - <help>Hello Interval</help> + <help>Configure group limit for watermark warning</help> <valueHelp> - <format>u32:1-180</format> - <description>Hello Interval in seconds</description> + <format>u32:1-65535</format> + <description>Group count to generate watermark warning</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-180"/> + <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> </leafNode> </children> - </tagNode> + </node> + #include <include/pim/join-prune-interval.xml.i> + #include <include/pim/keep-alive-timer.xml.i> + #include <include/pim/packets.xml.i> + #include <include/pim/register-suppress-time.xml.i> + <node name="register-accept-list"> + <properties> + <help>Only accept registers from a specific source prefix list</help> + </properties> + <children> + #include <include/policy/prefix-list.xml.i> + </children> + </node> <node name="rp"> <properties> <help>Rendezvous Point</help> </properties> <children> <tagNode name="address"> <properties> <help>Rendezvous Point address</help> <valueHelp> <format>ipv4</format> <description>Rendezvous Point address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> <children> <leafNode name="group"> <properties> <help>Group Address range</help> <valueHelp> <format>ipv4net</format> <description>Group Address range RFC 3171</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> </children> </tagNode> - <leafNode name="keep-alive-timer"> + #include <include/pim/keep-alive-timer.xml.i> + </children> + </node> + <leafNode name="no-v6-secondary"> + <properties> + <help>Disable IPv6 secondary address in hello packets</help> + <valueless/> + </properties> + </leafNode> + <node name="spt-switchover"> + <properties> + <help>Shortest-path tree (SPT) switchover</help> + </properties> + <children> + <node name="infinity-and-beyond"> <properties> - <help>Keep alive Timer</help> - <valueHelp> - <format>u32:31-60000</format> - <description>Keep alive Timer in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 31-60000"/> - </constraint> + <help>Never switch to SPT Tree</help> </properties> - </leafNode> + <children> + #include <include/policy/prefix-list.xml.i> + </children> + </node> + </children> + </node> + <node name="ssm"> + <properties> + <help>Source-Specific Multicast</help> + </properties> + <children> + #include <include/policy/prefix-list.xml.i> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/protocols-pim6.xml.in b/interface-definitions/protocols-pim6.xml.in index 58ef5a1e3..8bd3f3fee 100644 --- a/interface-definitions/protocols-pim6.xml.in +++ b/interface-definitions/protocols-pim6.xml.in @@ -1,132 +1,179 @@ <?xml version="1.0"?> <!-- Protocol Independent Multicast for IPv6 (PIMv6) configuration --> <interfaceDefinition> <node name="protocols"> <children> <node name="pim6" owner="${vyos_conf_scripts_dir}/protocols_pim6.py"> <properties> - <help>Protocol Independent Multicast for IPv6 (PIMv6)</help> + <help>Protocol Independent Multicast for IPv6 (PIMv6) and MLD</help> <priority>400</priority> </properties> <children> <tagNode name="interface"> <properties> <help>PIMv6 interface</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> </properties> <children> + #include <include/pim/bsm.xml.i> + #include <include/pim/dr-priority.xml.i> + #include <include/pim/hello.xml.i> + #include <include/pim/passive.xml.i> <node name="mld"> <properties> <help>Multicast Listener Discovery (MLD)</help> </properties> <children> #include <include/generic-disable-node.xml.i> <tagNode name="join"> <properties> <help>MLD join multicast group</help> <valueHelp> <format>ipv6</format> <description>Multicast group address</description> </valueHelp> <constraint> <validator name="ipv6-address"/> </constraint> </properties> <children> <leafNode name="source"> <properties> <help>Source address</help> <valueHelp> <format>ipv6</format> <description>Source address</description> </valueHelp> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script> </completionHelp> <constraint> <validator name="ipv6-address"/> </constraint> <multi/> </properties> </leafNode> </children> </tagNode> - <leafNode name="version"> + <leafNode name="last-member-query-count"> <properties> - <help>MLD version</help> - <completionHelp> - <list>1 2</list> - </completionHelp> + <help>Last member query count</help> <valueHelp> - <format>1</format> - <description>MLD version 1</description> + <format>u32:1-255</format> + <description>Count</description> </valueHelp> - <valueHelp> - <format>2</format> - <description>MLD version 2</description> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="last-member-query-interval"> + <properties> + <help>Last member query interval</help> + <valueHelp> + <format>u32:100-6553500</format> + <description>Last member query interval in milliseconds</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-2"/> + <validator name="numeric" argument="--range 100-6553500"/> </constraint> </properties> - <defaultValue>2</defaultValue> </leafNode> <leafNode name="interval"> <properties> <help>Query interval</help> <valueHelp> <format>u32:1-65535</format> <description>Query interval in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> </leafNode> <leafNode name="max-response-time"> <properties> <help>Max query response time</help> <valueHelp> <format>u32:100-6553500</format> <description>Query response value in milliseconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 100-6553500"/> </constraint> </properties> </leafNode> - <leafNode name="last-member-query-count"> + <leafNode name="version"> <properties> - <help>Last member query count</help> + <help>MLD version</help> + <completionHelp> + <list>1 2</list> + </completionHelp> <valueHelp> - <format>u32:1-255</format> - <description>Count</description> + <format>1</format> + <description>MLD version 1</description> + </valueHelp> + <valueHelp> + <format>2</format> + <description>MLD version 2</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-255"/> + <validator name="numeric" argument="--range 1-2"/> </constraint> </properties> + <defaultValue>2</defaultValue> </leafNode> - <leafNode name="last-member-query-interval"> + </children> + </node> + </children> + </tagNode> + #include <include/pim/join-prune-interval.xml.i> + #include <include/pim/keep-alive-timer.xml.i> + #include <include/pim/packets.xml.i> + #include <include/pim/register-suppress-time.xml.i> + <node name="rp"> + <properties> + <help>Rendezvous Point</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Rendezvous Point address</help> + <valueHelp> + <format>ipv6</format> + <description>Rendezvous Point address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="group"> <properties> - <help>Last member query interval</help> + <help>Group Address range</help> <valueHelp> - <format>u32:100-6553500</format> - <description>Last member query interval in milliseconds</description> + <format>ipv6net</format> + <description>Group Address range</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 100-6553500"/> + <validator name="ipv6-prefix"/> </constraint> + <multi/> </properties> </leafNode> + #include <include/policy/prefix-list6.xml.i> </children> - </node> + </tagNode> + #include <include/pim/keep-alive-timer.xml.i> </children> - </tagNode> + </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in index cae3423dc..10a1be242 100644 --- a/interface-definitions/xml-component-version.xml.in +++ b/interface-definitions/xml-component-version.xml.in @@ -1,50 +1,51 @@ <?xml version="1.0"?> <interfaceDefinition> #include <include/version/bgp-version.xml.i> #include <include/version/broadcast-relay-version.xml.i> #include <include/version/cluster-version.xml.i> #include <include/version/config-management-version.xml.i> #include <include/version/conntrack-sync-version.xml.i> #include <include/version/conntrack-version.xml.i> #include <include/version/container-version.xml.i> #include <include/version/dhcp-relay-version.xml.i> #include <include/version/dhcp-server-version.xml.i> #include <include/version/dhcpv6-server-version.xml.i> #include <include/version/dns-dynamic-version.xml.i> #include <include/version/dns-forwarding-version.xml.i> #include <include/version/firewall-version.xml.i> #include <include/version/flow-accounting-version.xml.i> #include <include/version/https-version.xml.i> #include <include/version/interfaces-version.xml.i> #include <include/version/ids-version.xml.i> #include <include/version/ipoe-server-version.xml.i> #include <include/version/ipsec-version.xml.i> #include <include/version/openvpn-version.xml.i> #include <include/version/isis-version.xml.i> #include <include/version/l2tp-version.xml.i> #include <include/version/lldp-version.xml.i> #include <include/version/mdns-version.xml.i> #include <include/version/monitoring-version.xml.i> #include <include/version/nat66-version.xml.i> #include <include/version/nat-version.xml.i> #include <include/version/ntp-version.xml.i> #include <include/version/openconnect-version.xml.i> #include <include/version/ospf-version.xml.i> + #include <include/version/pim-version.xml.i> #include <include/version/policy-version.xml.i> #include <include/version/pppoe-server-version.xml.i> #include <include/version/pptp-version.xml.i> #include <include/version/qos-version.xml.i> #include <include/version/quagga-version.xml.i> #include <include/version/rip-version.xml.i> #include <include/version/rpki-version.xml.i> #include <include/version/salt-version.xml.i> #include <include/version/snmp-version.xml.i> #include <include/version/ssh-version.xml.i> #include <include/version/sstp-version.xml.i> #include <include/version/system-version.xml.i> #include <include/version/vrf-version.xml.i> #include <include/version/vrrp-version.xml.i> #include <include/version/vyos-accel-ppp-version.xml.i> #include <include/version/wanloadbalance-version.xml.i> #include <include/version/webproxy-version.xml.i> </interfaceDefinition> diff --git a/op-mode-definitions/show-ip-igmp.xml.in b/op-mode-definitions/show-ip-igmp.xml.in index 855c5d508..1fd86ba54 100644 --- a/op-mode-definitions/show-ip-igmp.xml.in +++ b/op-mode-definitions/show-ip-igmp.xml.in @@ -1,48 +1,48 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="show"> <children> <node name="ip"> <children> <node name="igmp"> <properties> <help>Show IGMP (Internet Group Management Protocol) information</help> </properties> <children> <leafNode name="groups"> <properties> <help>IGMP groups information</help> </properties> - <command>vtysh -c "show ip igmp groups"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> - <leafNode name="interfaces"> + <leafNode name="interface"> <properties> <help>IGMP interfaces information</help> </properties> - <command>vtysh -c "show ip igmp interface"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="join"> <properties> <help>IGMP static join information</help> </properties> - <command>vtysh -c "show ip igmp join"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="sources"> <properties> <help>IGMP sources information</help> </properties> - <command>vtysh -c "show ip igmp sources"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="statistics"> <properties> <help>IGMP statistics</help> </properties> - <command>vtysh -c "show ip igmp statistics"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-ip-pim.xml.in b/op-mode-definitions/show-ip-pim.xml.in index fa317a944..9deba1f07 100644 --- a/op-mode-definitions/show-ip-pim.xml.in +++ b/op-mode-definitions/show-ip-pim.xml.in @@ -1,72 +1,156 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="show"> <children> <node name="ip"> <children> <node name="pim"> <properties> <help>Show PIM (Protocol Independent Multicast) information</help> </properties> <children> - <leafNode name="interfaces"> + <leafNode name="assert"> + <properties> + <help>PIM interfaces assert</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="assert-internal"> + <properties> + <help>PIM interface internal assert state</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="assert-metric"> + <properties> + <help>PIM interface assert metric</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="assert-winner-metric"> + <properties> + <help>PIM interface assert winner metric</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="bsm-database"> + <properties> + <help>PIM cached bsm packets information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="bsr"> + <properties> + <help>PIM boot-strap router information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="bsrp-info"> + <properties> + <help>PIM cached group-rp mappings information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="channel"> + <properties> + <help>PIM downstream channel info</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="group-type"> + <properties> + <help>PIM multicast group type</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="interface"> <properties> <help>PIM interfaces information</help> </properties> - <command>vtysh -c "show ip pim interface"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="join"> <properties> <help>PIM join information</help> </properties> - <command>vtysh -c "show ip pim join"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="local-membership"> + <properties> + <help>PIM interface local-membership</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="neighbor"> <properties> <help>PIM neighbor information</help> </properties> - <command>vtysh -c "show ip pim neighbor"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="nexthop"> <properties> <help>PIM cached nexthop rpf information</help> </properties> - <command>vtysh -c "show ip pim nexthop"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="rp-info"> + <properties> + <help>PIM rendezvous point information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="rpf"> + <properties> + <help>PIM reverse path forwarding information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="secondary"> + <properties> + <help>PIM neighbor addresses</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="state"> <properties> <help>PIM state information</help> </properties> - <command>vtysh -c "show ip pim state"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> <leafNode name="statistics"> <properties> <help>PIM statistics</help> </properties> - <command>vtysh -c "show ip pim statistics"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="upstream"> + <properties> + <help>PIM upstream information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> - <leafNode name="rp"> + <leafNode name="upstream-join-desired"> <properties> - <help>PIM RP (Rendevous Point) information</help> + <help>PIM upstream join-desired</help> </properties> - <command>vtysh -c "show ip pim rp-info"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> - <leafNode name="rpf"> + <leafNode name="upstream-rpf"> <properties> - <help>PIM cached source rpf information</help> + <help>PIM upstream source reverse path forwarding</help> </properties> - <command>vtysh -c "show ip pim rpf"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> - <leafNode name="upstream"> + <leafNode name="vxlan-groups"> <properties> - <help>PIM upstream information</help> + <help>VXLAN BUM groups</help> </properties> - <command>vtysh -c "show ip pim upstream"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-ipv6-mld.xml.in b/op-mode-definitions/show-ipv6-mld.xml.in new file mode 100644 index 000000000..5c719f700 --- /dev/null +++ b/op-mode-definitions/show-ipv6-mld.xml.in @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ipv6"> + <children> + <node name="mld"> + <properties> + <help>Show MLD (Multicast Listener Discovery) information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>MLD group information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="interface"> + <properties> + <help>MLD interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="joins"> + <properties> + <help>MLD joined groups and sources</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>MLD statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ipv6-pim.xml.in b/op-mode-definitions/show-ipv6-pim.xml.in new file mode 100644 index 000000000..7cc3ce742 --- /dev/null +++ b/op-mode-definitions/show-ipv6-pim.xml.in @@ -0,0 +1,120 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ipv6"> + <children> + <node name="pim"> + <properties> + <help>Show PIM (Protocol Independent Multicast) information</help> + </properties> + <children> + <leafNode name="bsm-database"> + <properties> + <help>PIM cached bsm packets information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="bsr"> + <properties> + <help>PIM boot-strap router information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="bsrp-info"> + <properties> + <help>PIM cached group-rp mappings information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="channel"> + <properties> + <help>PIM downstream channel info</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="interface"> + <properties> + <help>PIM interfaces information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="join"> + <properties> + <help>PIM join information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="local-membership"> + <properties> + <help>PIM interface local-membership</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="neighbor"> + <properties> + <help>PIM neighbor information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="nexthop"> + <properties> + <help>PIM cached nexthop rpf information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="rp-info"> + <properties> + <help>PIM rendezvous point information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="rpf"> + <properties> + <help>PIM reverse path forwarding information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="secondary"> + <properties> + <help>PIM neighbor addresses</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="state"> + <properties> + <help>PIM state information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>PIM statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="upstream"> + <properties> + <help>PIM upstream information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="upstream-join-desired"> + <properties> + <help>PIM upstream join-desired</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="upstream-rpf"> + <properties> + <help>PIM upstream source reverse path forwarding</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/frr.py b/python/vyos/frr.py index ad5c207f5..a01d967e4 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -1,550 +1,546 @@ # Copyright 2020 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/>. r""" A Library for interracting with the FRR daemon suite. It supports simple configuration manipulation and loading using the official tools supplied with FRR (vtysh and frr-reload) All configuration management and manipulation is done using strings and regex. Example Usage ##### # Reading configuration from frr: ``` >>> original_config = get_configuration() >>> repr(original_config) '!\nfrr version 7.3.1\nfrr defaults traditional\nhostname debian\n...... ``` # Modify a configuration section: ``` >>> new_bgp_section = 'router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n' >>> modified_config = replace_section(original_config, new_bgp_section, replace_re=r'router bgp \d+') >>> repr(modified_config) '............router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n...........' ``` Remove a configuration section: ``` >>> modified_config = remove_section(original_config, r'router ospf') ``` Test the new configuration: ``` >>> try: >>> mark_configuration(modified configuration) >>> except ConfigurationNotValid as e: >>> print('resulting configuration is not valid') >>> sys.exit(1) ``` Apply the new configuration: ``` >>> try: >>> replace_configuration(modified_config) >>> except CommitError as e: >>> print('Exception while commiting the supplied configuration') >>> print(e) >>> exit(1) ``` """ import tempfile import re from vyos.utils.permission import chown from vyos.utils.process import cmd from vyos.utils.process import popen from vyos.utils.process import STDOUT import logging from logging.handlers import SysLogHandler import os import sys LOG = logging.getLogger(__name__) DEBUG = False ch = SysLogHandler(address='/dev/log') ch2 = logging.StreamHandler(stream=sys.stdout) LOG.addHandler(ch) LOG.addHandler(ch2) -# Full list of FRR 9.0/stable daemons for reference -#_frr_daemons = ['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', -# 'isisd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'sharpd', 'bfdd', -# 'fabricd', 'pathd'] _frr_daemons = ['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', - 'isisd', 'pim6d', 'ldpd', 'babeld', 'bfdd'] + 'isisd', 'pimd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'bfdd'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' path_config = '/run/frr' default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)' class FrrError(Exception): pass class ConfigurationNotValid(FrrError): """ The configuratioin supplied to vtysh is not valid """ pass class CommitError(FrrError): """ Commiting the supplied configuration failed to commit by a unknown reason see commit error and/or run mark_configuration on the specified configuration to se error generated used by: reload_configuration() """ pass class ConfigSectionNotFound(FrrError): """ Removal of configuration failed because it is not existing in the supplied configuration """ pass def init_debugging(): global DEBUG DEBUG = os.path.exists('/tmp/vyos.frr.debug') if DEBUG: LOG.setLevel(logging.DEBUG) def get_configuration(daemon=None, marked=False): """ Get current running FRR configuration daemon: Collect only configuration for the specified FRR daemon, supplying daemon=None retrieves the complete configuration marked: Mark the configuration with "end" tags return: string containing the running configuration from frr """ if daemon and daemon not in _frr_daemons: raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') cmd = f"{path_vtysh} -c 'show run'" if daemon: cmd += f' -d {daemon}' output, code = popen(cmd, stderr=STDOUT) if code: raise OSError(code, output) config = output.replace('\r', '') # Remove first header lines from FRR config config = config.split("\n", 3)[-1] # Mark the configuration with end tags if marked: config = mark_configuration(config) return config def mark_configuration(config): """ Add end marks and Test the configuration for syntax faults If the configuration is valid a marked version of the configuration is returned, or else it failes with a ConfigurationNotValid Exception config: The configuration string to mark/test return: The marked configuration from FRR """ output, code = popen(f"{path_vtysh} -m -f -", stderr=STDOUT, input=config) if code == 2: raise ConfigurationNotValid(str(output)) elif code: raise OSError(code, output) config = output.replace('\r', '') return config def reload_configuration(config, daemon=None): """ Execute frr-reload with the new configuration This will try to reapply the supplied configuration inside FRR. The configuration needs to be a complete configuration from the integrated config or from a daemon. config: The configuration to apply daemon: Apply the conigutaion to the specified FRR daemon, supplying daemon=None applies to the integrated configuration return: None """ if daemon and daemon not in _frr_daemons: raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') f = tempfile.NamedTemporaryFile('w') f.write(config) f.flush() LOG.debug(f'reload_configuration: Reloading config using temporary file: {f.name}') cmd = f'{path_frr_reload} --reload' if daemon: cmd += f' --daemon {daemon}' if DEBUG: cmd += f' --debug --stdout' cmd += f' {f.name}' LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"') output, code = popen(cmd, stderr=STDOUT) f.close() for i, e in enumerate(output.split('\n')): LOG.debug(f'frr-reload output: {i:3} {e}') if code == 1: raise CommitError('FRR configuration failed while running commit. Please ' \ 'enable debugging to examine logs.\n\n\n' \ 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ 'and "sudo systemctl stop vyos-configd"') elif code: raise OSError(code, output) return output def save_configuration(): """ T3217: Save FRR configuration to /run/frr/config/frr.conf """ return cmd(f'{path_vtysh} -n -w') def execute(command): """ Run commands inside vtysh command: str containing commands to execute inside a vtysh session """ if not isinstance(command, str): raise ValueError(f'command needs to be a string: {repr(command)}') cmd = f"{path_vtysh} -c '{command}'" output, code = popen(cmd, stderr=STDOUT) if code: raise OSError(code, output) config = output.replace('\r', '') return config def configure(lines, daemon=False): """ run commands inside config mode vtysh lines: list or str conaining commands to execute inside a configure session only one command executed on each configure() Executing commands inside a subcontext uses the list to describe the context ex: ['router bgp 6500', 'neighbor 192.0.2.1 remote-as 65000'] return: None """ if isinstance(lines, str): lines = [lines] elif not isinstance(lines, list): raise ValueError('lines needs to be string or list of commands') if daemon and daemon not in _frr_daemons: raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') cmd = f'{path_vtysh}' if daemon: cmd += f' -d {daemon}' cmd += " -c 'configure terminal'" for x in lines: cmd += f" -c '{x}'" output, code = popen(cmd, stderr=STDOUT) if code == 1: raise ConfigurationNotValid(f'Configuration FRR failed: {repr(output)}') elif code: raise OSError(code, output) config = output.replace('\r', '') return config def _replace_section(config, replacement, replace_re, before_re): r"""Replace a section of FRR config config: full original configuration replacement: replacement configuration section replace_re: The regex to replace example: ^router bgp \d+$.?*^!$ this will replace everything between ^router bgp X$ and ^!$ before_re: When replace_re is not existant, the config will be added before this tag example: ^line vty$ return: modified configuration as a text file """ # DEPRECATED, this is replaced by a new implementation # Check if block is configured, remove the existing instance else add a new one if re.findall(replace_re, config, flags=re.MULTILINE | re.DOTALL): # Section is in the configration, replace it return re.sub(replace_re, replacement, config, count=1, flags=re.MULTILINE | re.DOTALL) if before_re: if not re.findall(before_re, config, flags=re.MULTILINE | re.DOTALL): raise ConfigSectionNotFound(f"Config section {before_re} not found in config") # If no section is in the configuration, add it before the line vty line return re.sub(before_re, rf'{replacement}\n\g<1>', config, count=1, flags=re.MULTILINE | re.DOTALL) raise ConfigSectionNotFound(f"Config section {replacement} not found in config") def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line vty'): r"""Replace a section of FRR config config: full original configuration replacement: replacement configuration section from_re: Regex for the start of section matching example: 'router bgp \d+' to_re: Regex for stop of section matching default: '!' example: '!' or 'end' before_re: When from_re/to_re does not return a match, the config will be added before this tag default: ^line vty$ startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es """ # DEPRECATED, this is replaced by a new implementation return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^({before_re})$') def remove_section(config, from_re, to_re='!'): # DEPRECATED, this is replaced by a new implementation return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None) def _find_first_block(config, start_pattern, stop_pattern, start_at=0): '''Find start and stop line numbers for a config block config: (list) A list conaining the configuration that is searched start_pattern: (raw-str) The pattern searched for a a start of block tag stop_pattern: (raw-str) The pattern searched for to signify the end of the block start_at: (int) The index to start searching at in the <config> Returns: None: No complete block could be found set(int, int): A complete block found between the line numbers returned in the set The object <config> is searched from the start for the regex <start_pattern> until the first match is found. On a successful match it continues the search for the regex <stop_pattern> until it is found. After a successful run a set is returned containing the start and stop line numbers. ''' LOG.debug(f'_find_first_block: find start={repr(start_pattern)} stop={repr(stop_pattern)} start_at={start_at}') _start = None for i, element in enumerate(config[start_at:], start=start_at): # LOG.debug(f'_find_first_block: running line {i:3} "{element}"') if not _start: if not re.match(start_pattern, element): LOG.debug(f'_find_first_block: no match {i:3} "{element}"') continue _start = i LOG.debug(f'_find_first_block: Found start {i:3} "{element}"') continue if not re.match(stop_pattern, element): LOG.debug(f'_find_first_block: no match {i:3} "{element}"') continue LOG.debug(f'_find_first_block: Found stop {i:3} "{element}"') return (_start, i) LOG.debug('_find_first_block: exit start={repr(start_pattern)} stop={repr(stop_pattern)} start_at={start_at}') return None def _find_first_element(config, pattern, start_at=0): '''Find the first element that matches the current pattern in config config: (list) A list containing the configuration that is searched start_pattern: (raw-str) The pattern searched for start_at: (int) The index to start searching at in the <config> return: Line index of the line containing the searched pattern TODO: for now it returns -1 on a no-match because 0 also returns as False TODO: that means that we can not use False matching to tell if its ''' LOG.debug(f'_find_first_element: find start="{pattern}" start_at={start_at}') for i, element in enumerate(config[start_at:], start=0): if re.match(pattern + '$', element): LOG.debug(f'_find_first_element: Found stop {i:3} "{element}"') return i LOG.debug(f'_find_first_element: no match {i:3} "{element}"') LOG.debug(f'_find_first_element: Did not find any match, exiting') return -1 def _find_elements(config, pattern, start_at=0): '''Find all instances of pattern and return a list containing all element indexes config: (list) A list containing the configuration that is searched start_pattern: (raw-str) The pattern searched for start_at: (int) The index to start searching at in the <config> return: A list of line indexes containing the searched pattern TODO: refactor this to return a generator instead ''' return [i for i, element in enumerate(config[start_at:], start=0) if re.match(pattern + '$', element)] class FRRConfig: '''Main FRR Configuration manipulation object Using this object the user could load, manipulate and commit the configuration to FRR ''' def __init__(self, config=[]): self.imported_config = '' if isinstance(config, list): self.config = config.copy() self.original_config = config.copy() elif isinstance(config, str): self.config = config.split('\n') self.original_config = self.config.copy() else: raise ValueError( 'The config element needs to be a string or list type object') if config: LOG.debug(f'__init__: frr library initiated with initial config') for i, e in enumerate(self.config): LOG.debug(f'__init__: initial {i:3} {e}') def load_configuration(self, daemon=None): '''Load the running configuration from FRR into the config object daemon: str with name of the FRR Daemon to load configuration from or None to load the consolidated config Using this overwrites the current loaded config objects and replaces the original loaded config ''' init_debugging() self.imported_config = get_configuration(daemon=daemon) if daemon: LOG.debug(f'load_configuration: Configuration loaded from FRR daemon {daemon}') else: LOG.debug(f'load_configuration: Configuration loaded from FRR integrated config') self.original_config = self.imported_config.split('\n') self.config = self.original_config.copy() for i, e in enumerate(self.imported_config.split('\n')): LOG.debug(f'load_configuration: loaded {i:3} {e}') return def test_configuration(self): '''Test the current configuration against FRR This will exception if FRR failes to load the current configuration object ''' LOG.debug('test_configation: Testing configuration') mark_configuration('\n'.join(self.config)) def commit_configuration(self, daemon=None): ''' Commit the current configuration to FRR daemon: str with name of the FRR daemon to commit to or None to use the consolidated config. Configuration is automatically saved after apply ''' LOG.debug('commit_configuration: Commiting configuration') for i, e in enumerate(self.config): LOG.debug(f'commit_configuration: new_config {i:3} {e}') # https://github.com/FRRouting/frr/issues/10132 # https://github.com/FRRouting/frr/issues/10133 count = 0 count_max = 5 while count < count_max: count += 1 try: reload_configuration('\n'.join(self.config), daemon=daemon) break except: # we just need to re-try the commit of the configuration # for the listed FRR issues above pass if count >= count_max: raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!') # Save configuration to /run/frr/config/frr.conf save_configuration() def modify_section(self, start_pattern, replacement='!', stop_pattern=r'\S+', remove_stop_mark=False, count=0): if isinstance(replacement, str): replacement = replacement.split('\n') elif not isinstance(replacement, list): return ValueError("The replacement element needs to be a string or list type object") LOG.debug(f'modify_section: starting search for {repr(start_pattern)} until {repr(stop_pattern)}') _count = 0 _next_start = 0 while True: if count and count <= _count: # Break out of the loop after specified amount of matches LOG.debug(f'modify_section: reached limit ({_count}), exiting loop at line {_next_start}') break # While searching, always assume that the user wants to search for the exact pattern he entered # To be more specific the user needs a override, eg. a "pattern.*" _w = _find_first_block( self.config, start_pattern+'$', stop_pattern, start_at=_next_start) if not _w: # Reached the end, no more elements to remove LOG.debug(f'modify_section: No more config sections found, exiting') break start_element, end_element = _w LOG.debug(f'modify_section: found match between {start_element} and {end_element}') for i, e in enumerate(self.config[start_element:end_element+1 if remove_stop_mark else end_element], start=start_element): LOG.debug(f'modify_section: remove {i:3} {e}') del self.config[start_element:end_element + 1 if remove_stop_mark else end_element] if replacement: # Append the replacement config at the current position for i, e in enumerate(replacement, start=start_element): LOG.debug(f'modify_section: add {i:3} {e}') self.config[start_element:start_element] = replacement _count += 1 _next_start = start_element + len(replacement) return _count def add_before(self, before_pattern, addition): '''Add config block before this element in the configuration''' if isinstance(addition, str): addition = addition.split('\n') elif not isinstance(addition, list): return ValueError("The replacement element needs to be a string or list type object") start = _find_first_element(self.config, before_pattern) if start < 0: return False for i, e in enumerate(addition, start=start): LOG.debug(f'add_before: add {i:3} {e}') self.config[start:start] = addition return True def __str__(self): return '\n'.join(self.config) def __repr__(self): return f'frr({repr(str(self))})' diff --git a/smoketest/config-tests/igmp-pim-small b/smoketest/config-tests/igmp-pim-small new file mode 100644 index 000000000..207c17d45 --- /dev/null +++ b/smoketest/config-tests/igmp-pim-small @@ -0,0 +1,17 @@ +set interfaces ethernet eth1 address '100.64.0.1/24' +set interfaces ethernet eth2 address '172.16.0.2/24' +set protocols pim interface eth1 igmp join 224.1.0.0 source-address '1.1.1.1' +set protocols pim interface eth1 igmp join 224.1.0.0 source-address '1.1.1.2' +set protocols pim interface eth1 igmp query-interval '1000' +set protocols pim interface eth1 igmp query-max-response-time '30' +set protocols pim interface eth1 igmp version '2' +set protocols pim interface eth2 +set protocols pim rp address 172.16.255.1 group '224.0.0.0/4' +set service ntp server 0.pool.ntp.org +set service ntp server 1.pool.ntp.org +set service ntp server 2.pool.ntp.org +set system domain-name 'vyos.io' +set system host-name 'vyos' +set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' +set system login user vyos authentication plaintext-password '' +set system console device ttyS0 speed '115200' diff --git a/smoketest/configs/igmp-pim-small b/smoketest/configs/igmp-pim-small new file mode 100644 index 000000000..f433ff8d7 --- /dev/null +++ b/smoketest/configs/igmp-pim-small @@ -0,0 +1,84 @@ +interfaces { + ethernet eth0 { + duplex auto + speed auto + } + ethernet eth1 { + address 100.64.0.1/24 + duplex auto + speed auto + } + ethernet eth2 { + address 172.16.0.2/24 + duplex auto + speed auto + } +} +protocols { + igmp { + interface eth1 { + join 224.1.0.0 { + source 1.1.1.1 + source 1.1.1.2 + } + query-interval 1000 + query-max-response-time 30 + version 2 + } + } + pim { + interface eth1 { + } + interface eth2 { + } + rp { + address 172.16.255.1 { + group 224.0.0.0/4 + } + } + } +} +system { + config-management { + commit-revisions 200 + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.io + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + } + } + ntp { + server 0.pool.ntp.org { + } + server 1.pool.ntp.org { + } + server 2.pool.ntp.org { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } + time-zone Europe/Berlin +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@1:conntrack-sync@1:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@18:ipoe-server@1:ipsec@5:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@7:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@20:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0 diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 51ccbc9e6..73b4e9764 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,1072 +1,1068 @@ # Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os -import unittest - from binascii import hexlify from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.defaults import directories from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.file import read_file from vyos.utils.dict import dict_search from vyos.utils.process import process_named_running from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_vrf from vyos.utils.process import cmd from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.xml_ref import cli_defined dhclient_base_dir = directories['isc_dhclient_dir'] dhclient_process_name = 'dhclient' dhcp6c_base_dir = directories['dhcp6_client_dir'] dhcp6c_process_name = 'dhcp6c' def is_mirrored_to(interface, mirror_if, qdisc): """ Ask TC if we are mirroring traffic to a discrete interface. interface: source interface mirror_if: destination where we mirror our data to qdisc: must be ffff or 1 for ingress/egress """ if qdisc not in ['ffff', '1']: raise ValueError() ret_val = False tmp = cmd(f'tc -s -p filter ls dev {interface} parent {qdisc}: | grep mirred') tmp = tmp.lower() if mirror_if in tmp: ret_val = True return ret_val class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): _test_dhcp = False _test_ip = False _test_mtu = False _test_vlan = False _test_qinq = False _test_ipv6 = False _test_ipv6_pd = False _test_ipv6_dhcpc6 = False _test_mirror = False _test_vrf = False _base_path = [] _options = {} _interfaces = [] _qinq_range = ['10', '20', '30'] _vlan_range = ['100', '200', '300', '2000'] _test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32', '2001:db8:1::ffff/64', '2001:db8:101::1/112'] _mirror_interfaces = [] # choose IPv6 minimum MTU value for tests - this must always work _mtu = '1280' @classmethod def setUpClass(cls): super(BasicInterfaceTest.TestCase, cls).setUpClass() # XXX the case of test_vif_8021q_mtu_limits, below, shows that # we should extend cli_defined to support more complex queries cls._test_vlan = cli_defined(cls._base_path, 'vif') cls._test_qinq = cli_defined(cls._base_path, 'vif-s') cls._test_dhcp = cli_defined(cls._base_path, 'dhcp-options') cls._test_ip = cli_defined(cls._base_path, 'ip') cls._test_ipv6 = cli_defined(cls._base_path, 'ipv6') cls._test_ipv6_dhcpc6 = cli_defined(cls._base_path, 'dhcpv6-options') cls._test_ipv6_pd = cli_defined(cls._base_path + ['dhcpv6-options'], 'pd') cls._test_mtu = cli_defined(cls._base_path, 'mtu') cls._test_vrf = cli_defined(cls._base_path, 'vrf') # Setup mirror interfaces for SPAN (Switch Port Analyzer) for span in cls._mirror_interfaces: section = Section.section(span) cls.cli_set(cls, ['interfaces', section, span]) @classmethod def tearDownClass(cls): # Tear down mirror interfaces for SPAN (Switch Port Analyzer) for span in cls._mirror_interfaces: section = Section.section(span) cls.cli_delete(cls, ['interfaces', section, span]) super(BasicInterfaceTest.TestCase, cls).tearDownClass() def tearDown(self): self.cli_delete(self._base_path) self.cli_commit() # Verify that no previously interface remained on the system for intf in self._interfaces: self.assertNotIn(intf, interfaces()) # No daemon started during tests should remain running for daemon in ['dhcp6c', 'dhclient']: # if _interface list is populated do a more fine grained search # by also checking the cmd arguments passed to the daemon if self._interfaces: for tmp in self._interfaces: self.assertFalse(process_named_running(daemon, tmp)) else: self.assertFalse(process_named_running(daemon)) def test_dhcp_disable_interface(self): if not self._test_dhcp: self.skipTest('not supported') # When interface is configured as admin down, it must be admin down # even when dhcpc starts on the given interface for interface in self._interfaces: self.cli_set(self._base_path + [interface, 'disable']) for option in self._options.get(interface, []): self.cli_set(self._base_path + [interface] + option.split()) # Also enable DHCP (ISC DHCP always places interface in admin up # state so we check that we do not start DHCP client. # https://vyos.dev/T2767 self.cli_set(self._base_path + [interface, 'address', 'dhcp']) self.cli_commit() # Validate interface state for interface in self._interfaces: flags = read_file(f'/sys/class/net/{interface}/flags') self.assertEqual(int(flags, 16) & 1, 0) def test_dhcp_client_options(self): if not self._test_dhcp or not self._test_vrf: self.skipTest('not supported') distance = '100' for interface in self._interfaces: for option in self._options.get(interface, []): self.cli_set(self._base_path + [interface] + option.split()) self.cli_set(self._base_path + [interface, 'address', 'dhcp']) self.cli_set(self._base_path + [interface, 'dhcp-options', 'default-route-distance', distance]) self.cli_commit() for interface in self._interfaces: # Check if dhclient process runs dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface) self.assertTrue(dhclient_pid) dhclient_config = read_file(f'{dhclient_base_dir}/dhclient_{interface}.conf') self.assertIn('request subnet-mask, broadcast-address, routers, domain-name-servers', dhclient_config) self.assertIn('require subnet-mask;', dhclient_config) # and the commandline has the appropriate options cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') self.assertIn(f'-e\x00IF_METRIC={distance}', cmdline) def test_dhcp_vrf(self): if not self._test_dhcp or not self._test_vrf: self.skipTest('not supported') vrf_name = 'purple4' self.cli_set(['vrf', 'name', vrf_name, 'table', '65000']) for interface in self._interfaces: for option in self._options.get(interface, []): self.cli_set(self._base_path + [interface] + option.split()) self.cli_set(self._base_path + [interface, 'address', 'dhcp']) self.cli_set(self._base_path + [interface, 'vrf', vrf_name]) self.cli_commit() # Validate interface state for interface in self._interfaces: tmp = get_interface_vrf(interface) self.assertEqual(tmp, vrf_name) # Check if dhclient process runs dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface) self.assertTrue(dhclient_pid) # .. inside the appropriate VRF instance vrf_pids = cmd(f'ip vrf pids {vrf_name}') self.assertIn(str(dhclient_pid), vrf_pids) # and the commandline has the appropriate options cmdline = read_file(f'/proc/{dhclient_pid}/cmdline') self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value self.cli_delete(['vrf', 'name', vrf_name]) def test_dhcpv6_vrf(self): if not self._test_ipv6_dhcpc6 or not self._test_vrf: self.skipTest('not supported') vrf_name = 'purple6' self.cli_set(['vrf', 'name', vrf_name, 'table', '65001']) # When interface is configured as admin down, it must be admin down # even when dhcpc starts on the given interface for interface in self._interfaces: for option in self._options.get(interface, []): self.cli_set(self._base_path + [interface] + option.split()) self.cli_set(self._base_path + [interface, 'address', 'dhcpv6']) self.cli_set(self._base_path + [interface, 'vrf', vrf_name]) self.cli_commit() # Validate interface state for interface in self._interfaces: tmp = get_interface_vrf(interface) self.assertEqual(tmp, vrf_name) # Check if dhclient process runs tmp = process_named_running(dhcp6c_process_name, cmdline=interface) self.assertTrue(tmp) # .. inside the appropriate VRF instance vrf_pids = cmd(f'ip vrf pids {vrf_name}') self.assertIn(str(tmp), vrf_pids) self.cli_delete(['vrf', 'name', vrf_name]) def test_span_mirror(self): if not self._mirror_interfaces: self.skipTest('not supported') # Check the two-way mirror rules of ingress and egress for mirror in self._mirror_interfaces: for interface in self._interfaces: self.cli_set(self._base_path + [interface, 'mirror', 'ingress', mirror]) self.cli_set(self._base_path + [interface, 'mirror', 'egress', mirror]) self.cli_commit() # Verify config for mirror in self._mirror_interfaces: for interface in self._interfaces: self.assertTrue(is_mirrored_to(interface, mirror, 'ffff')) self.assertTrue(is_mirrored_to(interface, mirror, '1')) def test_interface_disable(self): # Check if description can be added to interface and # can be read back for intf in self._interfaces: self.cli_set(self._base_path + [intf, 'disable']) for option in self._options.get(intf, []): self.cli_set(self._base_path + [intf] + option.split()) self.cli_commit() # Validate interface description for intf in self._interfaces: self.assertEqual(Interface(intf).get_admin_state(), 'down') def test_interface_description(self): # Check if description can be added to interface and # can be read back for intf in self._interfaces: test_string=f'Description-Test-{intf}' self.cli_set(self._base_path + [intf, 'description', test_string]) for option in self._options.get(intf, []): self.cli_set(self._base_path + [intf] + option.split()) self.cli_commit() # Validate interface description for intf in self._interfaces: test_string=f'Description-Test-{intf}' tmp = read_file(f'/sys/class/net/{intf}/ifalias') self.assertEqual(tmp, test_string) self.assertEqual(Interface(intf).get_alias(), test_string) self.cli_delete(self._base_path + [intf, 'description']) self.cli_commit() # Validate remove interface description "empty" for intf in self._interfaces: tmp = read_file(f'/sys/class/net/{intf}/ifalias') self.assertEqual(tmp, str()) self.assertEqual(Interface(intf).get_alias(), str()) def test_add_single_ip_address(self): addr = '192.0.2.0/31' for intf in self._interfaces: self.cli_set(self._base_path + [intf, 'address', addr]) for option in self._options.get(intf, []): self.cli_set(self._base_path + [intf] + option.split()) self.cli_commit() for intf in self._interfaces: self.assertTrue(is_intf_addr_assigned(intf, addr)) self.assertEqual(Interface(intf).get_admin_state(), 'up') def test_add_multiple_ip_addresses(self): # Add address for intf in self._interfaces: for option in self._options.get(intf, []): self.cli_set(self._base_path + [intf] + option.split()) for addr in self._test_addr: self.cli_set(self._base_path + [intf, 'address', addr]) self.cli_commit() # Validate address for intf in self._interfaces: for af in AF_INET, AF_INET6: for addr in ifaddresses(intf)[af]: # checking link local addresses makes no sense if is_ipv6_link_local(addr['addr']): continue self.assertTrue(is_intf_addr_assigned(intf, addr['addr'])) def test_ipv6_link_local_address(self): # Common function for IPv6 link-local address assignemnts if not self._test_ipv6: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] # just set the interface base without any option - some interfaces # (VTI) do not require any option to be brought up self.cli_set(base) for option in self._options.get(interface, []): self.cli_set(base + option.split()) # after commit we must have an IPv6 link-local address self.cli_commit() for interface in self._interfaces: self.assertIn(AF_INET6, ifaddresses(interface)) for addr in ifaddresses(interface)[AF_INET6]: self.assertTrue(is_ipv6_link_local(addr['addr'])) # disable IPv6 link-local address assignment for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['ipv6', 'address', 'no-default-link-local']) # after commit we must have no IPv6 link-local address self.cli_commit() for interface in self._interfaces: self.assertNotIn(AF_INET6, ifaddresses(interface)) def test_interface_mtu(self): if not self._test_mtu: self.skipTest('not supported') for intf in self._interfaces: base = self._base_path + [intf] self.cli_set(base + ['mtu', self._mtu]) for option in self._options.get(intf, []): self.cli_set(base + option.split()) # commit interface changes self.cli_commit() # verify changed MTU for intf in self._interfaces: tmp = get_interface_config(intf) self.assertEqual(tmp['mtu'], int(self._mtu)) def test_mtu_1200_no_ipv6_interface(self): # Testcase if MTU can be changed to 1200 on non IPv6 # enabled interfaces if not self._test_mtu: self.skipTest('not supported') old_mtu = self._mtu self._mtu = '1200' for intf in self._interfaces: base = self._base_path + [intf] self.cli_set(base + ['mtu', self._mtu]) for option in self._options.get(intf, []): self.cli_set(base + option.split()) # check validate() - can not set low MTU if 'no-default-link-local' # is not set on CLI with self.assertRaises(ConfigSessionError): self.cli_commit() for intf in self._interfaces: base = self._base_path + [intf] self.cli_set(base + ['ipv6', 'address', 'no-default-link-local']) # commit interface changes self.cli_commit() # verify changed MTU for intf in self._interfaces: tmp = get_interface_config(intf) self.assertEqual(tmp['mtu'], int(self._mtu)) self._mtu = old_mtu def test_vif_8021q_interfaces(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(base + option.split()) for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] for address in self._test_addr: self.cli_set(base + ['address', address]) self.cli_commit() for intf in self._interfaces: for vlan in self._vlan_range: vif = f'{intf}.{vlan}' for address in self._test_addr: self.assertTrue(is_intf_addr_assigned(vif, address)) self.assertEqual(Interface(vif).get_admin_state(), 'up') # T4064: Delete interface addresses, keep VLAN interface for interface in self._interfaces: base = self._base_path + [interface] for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] self.cli_delete(base + ['address']) self.cli_commit() # Verify no IP address is assigned for interface in self._interfaces: for vlan in self._vlan_range: vif = f'{intf}.{vlan}' for address in self._test_addr: self.assertFalse(is_intf_addr_assigned(vif, address)) def test_vif_8021q_mtu_limits(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan or not self._test_mtu: self.skipTest('not supported') mtu_1500 = '1500' mtu_9000 = '9000' for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['mtu', mtu_1500]) for option in self._options.get(interface, []): self.cli_set(base + option.split()) if 'source-interface' in option: iface = option.split()[-1] iface_type = Section.section(iface) self.cli_set(['interfaces', iface_type, iface, 'mtu', mtu_9000]) for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] self.cli_set(base + ['mtu', mtu_9000]) # check validate() - VIF MTU must not be larger the parent interface # MTU size. with self.assertRaises(ConfigSessionError): self.cli_commit() # Change MTU on base interface to be the same as on the VIF interface for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['mtu', mtu_9000]) self.cli_commit() # Verify MTU on base and VIF interfaces for interface in self._interfaces: tmp = get_interface_config(interface) self.assertEqual(tmp['mtu'], int(mtu_9000)) for vlan in self._vlan_range: tmp = get_interface_config(f'{interface}.{vlan}') self.assertEqual(tmp['mtu'], int(mtu_9000)) def test_vif_8021q_qos_change(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_vlan: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(base + option.split()) for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] self.cli_set(base + ['ingress-qos', '0:1']) self.cli_set(base + ['egress-qos', '1:6']) self.cli_commit() for intf in self._interfaces: for vlan in self._vlan_range: vif = f'{intf}.{vlan}' tmp = get_interface_config(f'{vif}') tmp2 = dict_search('linkinfo.info_data.ingress_qos', tmp) for item in tmp2 if tmp2 else []: from_key = item['from'] to_key = item['to'] self.assertEqual(from_key, 0) self.assertEqual(to_key, 1) tmp2 = dict_search('linkinfo.info_data.egress_qos', tmp) for item in tmp2 if tmp2 else []: from_key = item['from'] to_key = item['to'] self.assertEqual(from_key, 1) self.assertEqual(to_key, 6) self.assertEqual(Interface(vif).get_admin_state(), 'up') new_ingress_qos_from = 1 new_ingress_qos_to = 6 new_egress_qos_from = 2 new_egress_qos_to = 7 for interface in self._interfaces: base = self._base_path + [interface] for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] self.cli_set(base + ['ingress-qos', f'{new_ingress_qos_from}:{new_ingress_qos_to}']) self.cli_set(base + ['egress-qos', f'{new_egress_qos_from}:{new_egress_qos_to}']) self.cli_commit() for intf in self._interfaces: for vlan in self._vlan_range: vif = f'{intf}.{vlan}' tmp = get_interface_config(f'{vif}') tmp2 = dict_search('linkinfo.info_data.ingress_qos', tmp) if tmp2: from_key = tmp2[0]['from'] to_key = tmp2[0]['to'] self.assertEqual(from_key, new_ingress_qos_from) self.assertEqual(to_key, new_ingress_qos_to) tmp2 = dict_search('linkinfo.info_data.egress_qos', tmp) if tmp2: from_key = tmp2[0]['from'] to_key = tmp2[0]['to'] self.assertEqual(from_key, new_egress_qos_from) self.assertEqual(to_key, new_egress_qos_to) def test_vif_8021q_lower_up_down(self): # Testcase for https://vyos.dev/T3349 if not self._test_vlan: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(base + option.split()) # disable the lower interface self.cli_set(base + ['disable']) for vlan in self._vlan_range: vlan_base = self._base_path + [interface, 'vif', vlan] # disable the vlan interface self.cli_set(vlan_base + ['disable']) self.cli_commit() # re-enable all lower interfaces for interface in self._interfaces: base = self._base_path + [interface] self.cli_delete(base + ['disable']) self.cli_commit() # verify that the lower interfaces are admin up and the vlan # interfaces are all admin down for interface in self._interfaces: self.assertEqual(Interface(interface).get_admin_state(), 'up') for vlan in self._vlan_range: ifname = f'{interface}.{vlan}' self.assertEqual(Interface(ifname).get_admin_state(), 'down') def test_vif_s_8021ad_vlan_interfaces(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(base + option.split()) for vif_s in self._qinq_range: for vif_c in self._vlan_range: base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c] self.cli_set(base + ['mtu', self._mtu]) for address in self._test_addr: self.cli_set(base + ['address', address]) self.cli_commit() for interface in self._interfaces: for vif_s in self._qinq_range: tmp = get_interface_config(f'{interface}.{vif_s}') self.assertEqual(dict_search('linkinfo.info_data.protocol', tmp), '802.1ad') for vif_c in self._vlan_range: vif = f'{interface}.{vif_s}.{vif_c}' # For an unknown reason this regularely fails on the QEMU builds, # thus the test for reading back IP addresses is temporary # disabled. There is no big deal here, as this uses the same # methods on 802.1q and here it works and is verified. # for address in self._test_addr: # self.assertTrue(is_intf_addr_assigned(vif, address)) tmp = get_interface_config(vif) self.assertEqual(tmp['mtu'], int(self._mtu)) # T4064: Delete interface addresses, keep VLAN interface for interface in self._interfaces: base = self._base_path + [interface] for vif_s in self._qinq_range: for vif_c in self._vlan_range: self.cli_delete(self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c, 'address']) self.cli_commit() # Verify no IP address is assigned for interface in self._interfaces: base = self._base_path + [interface] for vif_s in self._qinq_range: for vif_c in self._vlan_range: vif = f'{interface}.{vif_s}.{vif_c}' for address in self._test_addr: self.assertFalse(is_intf_addr_assigned(vif, address)) # T3972: remove vif-c interfaces from vif-s for interface in self._interfaces: base = self._base_path + [interface] for vif_s in self._qinq_range: base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c'] self.cli_delete(base) self.cli_commit() def test_vif_s_protocol_change(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! if not self._test_qinq: self.skipTest('not supported') for interface in self._interfaces: base = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(base + option.split()) for vif_s in self._qinq_range: for vif_c in self._vlan_range: base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c] for address in self._test_addr: self.cli_set(base + ['address', address]) self.cli_commit() for interface in self._interfaces: for vif_s in self._qinq_range: tmp = get_interface_config(f'{interface}.{vif_s}') # check for the default value self.assertEqual(tmp['linkinfo']['info_data']['protocol'], '802.1ad') # T3532: now change ethertype new_protocol = '802.1q' for interface in self._interfaces: for vif_s in self._qinq_range: base = self._base_path + [interface, 'vif-s', vif_s] self.cli_set(base + ['protocol', new_protocol]) self.cli_commit() # Verify new ethertype configuration for interface in self._interfaces: for vif_s in self._qinq_range: tmp = get_interface_config(f'{interface}.{vif_s}') self.assertEqual(tmp['linkinfo']['info_data']['protocol'], new_protocol.upper()) def test_interface_ip_options(self): if not self._test_ip: self.skipTest('not supported') arp_tmo = '300' mss = '1420' for interface in self._interfaces: path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) # Options if cli_defined(self._base_path + ['ip'], 'adjust-mss'): self.cli_set(path + ['ip', 'adjust-mss', mss]) if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'): self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo]) if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'): self.cli_set(path + ['ip', 'disable-arp-filter']) if cli_defined(self._base_path + ['ip'], 'disable-forwarding'): self.cli_set(path + ['ip', 'disable-forwarding']) if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'): self.cli_set(path + ['ip', 'enable-directed-broadcast']) if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'): self.cli_set(path + ['ip', 'enable-arp-accept']) if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'): self.cli_set(path + ['ip', 'enable-arp-announce']) if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'): self.cli_set(path + ['ip', 'enable-arp-ignore']) if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'): self.cli_set(path + ['ip', 'enable-proxy-arp']) if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'): self.cli_set(path + ['ip', 'proxy-arp-pvlan']) if cli_defined(self._base_path + ['ip'], 'source-validation'): self.cli_set(path + ['ip', 'source-validation', 'loose']) self.cli_commit() for interface in self._interfaces: if cli_defined(self._base_path + ['ip'], 'adjust-mss'): base_options = f'oifname "{interface}"' out = cmd('sudo nft list chain raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): self.assertIn(f'tcp option maxseg size set {mss}', line) if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'): tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds proc_base = f'/proc/sys/net/ipv4/conf/{interface}' if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'): tmp = read_file(f'{proc_base}/arp_filter') self.assertEqual('0', tmp) if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'): tmp = read_file(f'{proc_base}/arp_accept') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'): tmp = read_file(f'{proc_base}/arp_announce') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'): tmp = read_file(f'{proc_base}/arp_ignore') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'disable-forwarding'): tmp = read_file(f'{proc_base}/forwarding') self.assertEqual('0', tmp) if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'): tmp = read_file(f'{proc_base}/bc_forwarding') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'): tmp = read_file(f'{proc_base}/proxy_arp') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'): tmp = read_file(f'{proc_base}/proxy_arp_pvlan') self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'source-validation'): base_options = f'iifname "{interface}"' out = cmd('sudo nft list chain ip raw vyos_rpfilter') for line in out.splitlines(): if line.startswith(base_options): self.assertIn('fib saddr oif 0', line) self.assertIn('drop', line) def test_interface_ipv6_options(self): if not self._test_ipv6: self.skipTest('not supported') mss = '1400' dad_transmits = '10' accept_dad = '0' source_validation = 'strict' for interface in self._interfaces: path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) # Options if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'): self.cli_set(path + ['ipv6', 'adjust-mss', mss]) if cli_defined(self._base_path + ['ipv6'], 'accept-dad'): self.cli_set(path + ['ipv6', 'accept-dad', accept_dad]) if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'): self.cli_set(path + ['ipv6', 'dup-addr-detect-transmits', dad_transmits]) if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'): self.cli_set(path + ['ipv6', 'disable-forwarding']) if cli_defined(self._base_path + ['ipv6'], 'source-validation'): self.cli_set(path + ['ipv6', 'source-validation', source_validation]) self.cli_commit() for interface in self._interfaces: proc_base = f'/proc/sys/net/ipv6/conf/{interface}' if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'): base_options = f'oifname "{interface}"' out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): self.assertIn(f'tcp option maxseg size set {mss}', line) if cli_defined(self._base_path + ['ipv6'], 'accept-dad'): tmp = read_file(f'{proc_base}/accept_dad') self.assertEqual(accept_dad, tmp) if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'): tmp = read_file(f'{proc_base}/dad_transmits') self.assertEqual(dad_transmits, tmp) if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'): tmp = read_file(f'{proc_base}/forwarding') self.assertEqual('0', tmp) if cli_defined(self._base_path + ['ipv6'], 'source-validation'): base_options = f'iifname "{interface}"' out = cmd('sudo nft list chain ip6 raw vyos_rpfilter') for line in out.splitlines(): if line.startswith(base_options): self.assertIn('fib saddr . iif oif 0', line) self.assertIn('drop', line) def test_dhcpv6_client_options(self): if not self._test_ipv6_dhcpc6: self.skipTest('not supported') duid_base = 10 for interface in self._interfaces: duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) # Enable DHCPv6 client self.cli_set(path + ['address', 'dhcpv6']) self.cli_set(path + ['dhcpv6-options', 'no-release']) self.cli_set(path + ['dhcpv6-options', 'rapid-commit']) self.cli_set(path + ['dhcpv6-options', 'parameters-only']) self.cli_set(path + ['dhcpv6-options', 'duid', duid]) duid_base += 1 self.cli_commit() duid_base = 10 for interface in self._interfaces: duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base) dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf') self.assertIn(f'interface {interface} ' + '{', dhcpc6_config) self.assertIn(f' request domain-name-servers;', dhcpc6_config) self.assertIn(f' request domain-name;', dhcpc6_config) self.assertIn(f' information-only;', dhcpc6_config) self.assertIn(f' send ia-na 0;', dhcpc6_config) self.assertIn(f' send rapid-commit;', dhcpc6_config) self.assertIn(f' send client-id {duid};', dhcpc6_config) self.assertIn('};', dhcpc6_config) duid_base += 1 # Better ask the process about it's commandline in the future pid = process_named_running(dhcp6c_process_name, cmdline=interface) self.assertTrue(pid) dhcp6c_options = read_file(f'/proc/{pid}/cmdline') self.assertIn('-n', dhcp6c_options) def test_dhcpv6pd_auto_sla_id(self): if not self._test_ipv6_pd: self.skipTest('not supported') prefix_len = '56' sla_len = str(64 - int(prefix_len)) # Create delegatee interfaces first to avoid any confusion by dhcpc6 # this is mainly an "issue" with virtual-ethernet interfaces delegatees = ['dum2340', 'dum2341', 'dum2342', 'dum2343', 'dum2344'] for delegatee in delegatees: section = Section.section(delegatee) self.cli_set(['interfaces', section, delegatee]) self.cli_commit() for interface in self._interfaces: path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) address = '1' # prefix delegation stuff pd_base = path + ['dhcpv6-options', 'pd', '0'] self.cli_set(pd_base + ['length', prefix_len]) for delegatee in delegatees: self.cli_set(pd_base + ['interface', delegatee, 'address', address]) # increment interface address address = str(int(address) + 1) self.cli_commit() for interface in self._interfaces: dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf') # verify DHCPv6 prefix delegation self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config) address = '1' sla_id = '0' for delegatee in delegatees: self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config) self.assertIn(f'ifid {address};', dhcpc6_config) self.assertIn(f'sla-id {sla_id};', dhcpc6_config) self.assertIn(f'sla-len {sla_len};', dhcpc6_config) # increment sla-id sla_id = str(int(sla_id) + 1) # increment interface address address = str(int(address) + 1) # Check for running process self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface)) for delegatee in delegatees: # we can already cleanup the test delegatee interface here # as until commit() is called, nothing happens section = Section.section(delegatee) self.cli_delete(['interfaces', section, delegatee]) def test_dhcpv6pd_manual_sla_id(self): if not self._test_ipv6_pd: self.skipTest('not supported') prefix_len = '56' sla_len = str(64 - int(prefix_len)) # Create delegatee interfaces first to avoid any confusion by dhcpc6 # this is mainly an "issue" with virtual-ethernet interfaces delegatees = ['dum3340', 'dum3341', 'dum3342', 'dum3343', 'dum3344'] for delegatee in delegatees: section = Section.section(delegatee) self.cli_set(['interfaces', section, delegatee]) self.cli_commit() for interface in self._interfaces: path = self._base_path + [interface] for option in self._options.get(interface, []): self.cli_set(path + option.split()) # prefix delegation stuff address = '1' sla_id = '1' pd_base = path + ['dhcpv6-options', 'pd', '0'] self.cli_set(pd_base + ['length', prefix_len]) for delegatee in delegatees: self.cli_set(pd_base + ['interface', delegatee, 'address', address]) self.cli_set(pd_base + ['interface', delegatee, 'sla-id', sla_id]) # increment interface address address = str(int(address) + 1) sla_id = str(int(sla_id) + 1) self.cli_commit() # Verify dhcpc6 client configuration for interface in self._interfaces: address = '1' sla_id = '1' dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf') # verify DHCPv6 prefix delegation self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config) for delegatee in delegatees: self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config) self.assertIn(f'ifid {address};', dhcpc6_config) self.assertIn(f'sla-id {sla_id};', dhcpc6_config) self.assertIn(f'sla-len {sla_len};', dhcpc6_config) # increment sla-id sla_id = str(int(sla_id) + 1) # increment interface address address = str(int(address) + 1) # Check for running process self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface)) for delegatee in delegatees: # we can already cleanup the test delegatee interface here # as until commit() is called, nothing happens section = Section.section(delegatee) self.cli_delete(['interfaces', section, delegatee]) diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index f694f539d..140642806 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -1,116 +1,117 @@ # Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import unittest import paramiko from time import sleep from typing import Type from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos import ConfigError from vyos.defaults import commit_lock from vyos.utils.process import cmd from vyos.utils.process import run save_config = '/tmp/vyos-smoketest-save' # This class acts as shim between individual Smoketests developed for VyOS and # the Python UnitTest framework. Before every test is loaded, we dump the current # system configuration and reload it after the test - despite the test results. # # Using this approach we can not render a live system useless while running any # kind of smoketest. In addition it adds debug capabilities like printing the # command used to execute the test. class VyOSUnitTestSHIM: class TestCase(unittest.TestCase): # if enabled in derived class, print out each and every set/del command # on the CLI. This is usefull to grap all the commands required to # trigger the certain failure condition. # Use "self.debug = True" in derived classes setUp() method debug = False @classmethod def setUpClass(cls): cls._session = ConfigSession(os.getpid()) cls._session.save_config(save_config) pass @classmethod def tearDownClass(cls): # discard any pending changes which might caused a messed up config cls._session.discard() # ... and restore the initial state cls._session.migrate_and_load_config(save_config) try: cls._session.commit() except (ConfigError, ConfigSessionError): cls._session.discard() cls.fail(cls) def cli_set(self, config): if self.debug: print('set ' + ' '.join(config)) self._session.set(config) def cli_delete(self, config): if self.debug: print('del ' + ' '.join(config)) self._session.delete(config) def cli_commit(self): self._session.commit() # during a commit there is a process opening commit_lock, and run() returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) - def getFRRconfig(self, string, end='$', endsection='^!', daemon=''): + def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''): """ Retrieve current "running configuration" from FRR """ - command = f'vtysh -c "show run {daemon} no-header" | sed -n "/^{string}{end}/,/{endsection}/p"' + command = f'vtysh -c "show run {daemon} no-header"' + if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' out = cmd(command) if self.debug: import pprint print(f'\n\ncommand "{command}" returned:\n') pprint.pprint(out) return out @staticmethod def ssh_send_cmd(command, username, password, hostname='localhost'): """ SSH command execution helper """ # Try to login via SSH ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh_client.connect(hostname=hostname, username=username, password=password) _, stdout, stderr = ssh_client.exec_command(command) output = stdout.read().decode().strip() error = stderr.read().decode().strip() ssh_client.close() return output, error # standard construction; typing suggestion: https://stackoverflow.com/a/70292317 def ignore_warning(warning: Type[Warning]): import warnings from functools import wraps def inner(f): @wraps(f) def wrapped(*args, **kwargs): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=warning) return f(*args, **kwargs) return wrapped return inner diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py new file mode 100755 index 000000000..ccfced138 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_pim.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.utils.process import process_named_running + +PROCESS_NAME = 'pimd' +base_path = ['protocols', 'pim'] + +class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # pimd process must be running + self.assertTrue(process_named_running(PROCESS_NAME)) + + self.cli_delete(base_path) + self.cli_commit() + + # pimd process must be stopped by now + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_pim_basic(self): + rp = '127.0.0.1' + group = '224.0.0.0/4' + hello = '100' + dr_priority = '64' + + self.cli_set(base_path + ['rp', 'address', rp, 'group', group]) + + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface , 'bfd']) + self.cli_set(base_path + ['interface', interface , 'dr-priority', dr_priority]) + self.cli_set(base_path + ['interface', interface , 'hello', hello]) + self.cli_set(base_path + ['interface', interface , 'no-bsm']) + self.cli_set(base_path + ['interface', interface , 'no-unicast-bsm']) + self.cli_set(base_path + ['interface', interface , 'passive']) + + # commit changes + self.cli_commit() + + # Verify FRR pimd configuration + frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ip pim rp {rp} {group}', frrconfig) + + for interface in interfaces: + frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', frrconfig) + self.assertIn(f' ip pim', frrconfig) + self.assertIn(f' ip pim bfd', frrconfig) + self.assertIn(f' ip pim drpriority {dr_priority}', frrconfig) + self.assertIn(f' ip pim hello {hello}', frrconfig) + self.assertIn(f' no ip pim bsm', frrconfig) + self.assertIn(f' no ip pim unicast-bsm', frrconfig) + self.assertIn(f' ip pim passive', frrconfig) + + self.cli_commit() + + def test_02_pim_advanced(self): + rp = '127.0.0.2' + group = '224.0.0.0/4' + join_prune_interval = '123' + rp_keep_alive_timer = '190' + keep_alive_timer = '180' + packets = '10' + prefix_list = 'pim-test' + register_suppress_time = '300' + + self.cli_set(base_path + ['rp', 'address', rp, 'group', group]) + self.cli_set(base_path + ['rp', 'keep-alive-timer', rp_keep_alive_timer]) + + self.cli_set(base_path + ['ecmp', 'rebalance']) + self.cli_set(base_path + ['join-prune-interval', join_prune_interval]) + self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer]) + self.cli_set(base_path + ['packets', packets]) + self.cli_set(base_path + ['register-accept-list', 'prefix-list', prefix_list]) + self.cli_set(base_path + ['register-suppress-time', register_suppress_time]) + self.cli_set(base_path + ['no-v6-secondary']) + self.cli_set(base_path + ['spt-switchover', 'infinity-and-beyond', 'prefix-list', prefix_list]) + self.cli_set(base_path + ['ssm', 'prefix-list', prefix_list]) + + # check validate() - PIM require defined interfaces! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + # commit changes + self.cli_commit() + + # Verify FRR pimd configuration + frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ip pim rp {rp} {group}', frrconfig) + self.assertIn(f'ip pim rp keep-alive-timer {rp_keep_alive_timer}', frrconfig) + self.assertIn(f'ip pim ecmp rebalance', frrconfig) + self.assertIn(f'ip pim join-prune-interval {join_prune_interval}', frrconfig) + self.assertIn(f'ip pim keep-alive-timer {keep_alive_timer}', frrconfig) + self.assertIn(f'ip pim packets {packets}', frrconfig) + self.assertIn(f'ip pim register-accept-list {prefix_list}', frrconfig) + self.assertIn(f'ip pim register-suppress-time {register_suppress_time}', frrconfig) + self.assertIn(f'no ip pim send-v6-secondary', frrconfig) + self.assertIn(f'ip pim spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig) + self.assertIn(f'ip pim ssm prefix-list {prefix_list}', frrconfig) + + def test_03_pim_igmp_proxy(self): + igmp_proxy = ['protocols', 'igmp-proxy'] + rp = '127.0.0.1' + group = '224.0.0.0/4' + + self.cli_set(base_path) + self.cli_set(igmp_proxy) + + # check validate() - can not set both IGMP proxy and PIM + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(igmp_proxy) + + self.cli_set(base_path + ['rp', 'address', rp, 'group', group]) + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface , 'bfd']) + + # commit changes + self.cli_commit() + + def test_04_igmp(self): + watermark_warning = '2000' + query_interval = '1000' + query_max_response_time = '200' + version = '2' + + igmp_join = { + '224.1.1.1' : { 'source' : ['1.1.1.1', '2.2.2.2', '3.3.3.3'] }, + '224.1.2.2' : { 'source' : [] }, + '224.1.3.3' : {}, + } + + self.cli_set(base_path + ['igmp', 'watermark-warning', watermark_warning]) + interfaces = Section.interfaces('ethernet') + for interface in interfaces: + self.cli_set(base_path + ['interface', interface , 'igmp', 'version', version]) + self.cli_set(base_path + ['interface', interface , 'igmp', 'query-interval', query_interval]) + self.cli_set(base_path + ['interface', interface , 'igmp', 'query-max-response-time', query_max_response_time]) + + for join, join_config in igmp_join.items(): + self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join]) + if 'source' in join_config: + for source in join_config['source']: + self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join, 'source-address', source]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig) + + for interface in interfaces: + frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f'interface {interface}', frrconfig) + self.assertIn(f' ip igmp', frrconfig) + self.assertIn(f' ip igmp version {version}', frrconfig) + self.assertIn(f' ip igmp query-interval {query_interval}', frrconfig) + self.assertIn(f' ip igmp query-max-response-time {query_max_response_time}', frrconfig) + + for join, join_config in igmp_join.items(): + if 'source' in join_config: + for source in join_config['source']: + self.assertIn(f' ip igmp join {join} {source}', frrconfig) + else: + self.assertIn(f' ip igmp join {join}', frrconfig) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py index 1be12836d..e22a7c722 100755 --- a/smoketest/scripts/cli/test_protocols_pim6.py +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -1,110 +1,139 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import process_named_running PROCESS_NAME = 'pim6d' base_path = ['protocols', 'pim6'] - class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) self.cli_commit() + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_pim6_01_mld_simple(self): # commit changes interfaces = Section.interfaces('ethernet') - for interface in interfaces: self.cli_set(base_path + ['interface', interface]) self.cli_commit() # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig( - f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertNotIn(f' ipv6 mld version 1', config) # Change to MLD version 1 for interface in interfaces: - self.cli_set(base_path + ['interface', - interface, 'mld', 'version', '1']) + self.cli_set(base_path + ['interface', interface, 'mld', 'version', '1']) self.cli_commit() # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig( - f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld', config) self.assertIn(f' ipv6 mld version 1', config) def test_pim6_02_mld_join(self): - # commit changes interfaces = Section.interfaces('ethernet') - - # Use an invalid multiple group address + # Use an invalid multicast group address for interface in interfaces: - self.cli_set(base_path + ['interface', - interface, 'mld', 'join', 'fd00::1234']) + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'fd00::1234']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_path + ['interface']) - # Use a valid multiple group address + # Use a valid multicast group address for interface in interfaces: - self.cli_set(base_path + ['interface', - interface, 'mld', 'join', 'ff18::1234']) + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff18::1234']) self.cli_commit() # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig( - f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join ff18::1234', config) # Join a source-specific multicast group for interface in interfaces: - self.cli_set(base_path + ['interface', interface, - 'mld', 'join', 'ff38::5678', 'source', '2001:db8::5678']) + self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff38::5678', 'source', '2001:db8::5678']) self.cli_commit() # Verify FRR pim6d configuration for interface in interfaces: - config = self.getFRRconfig( - f'interface {interface}', daemon=PROCESS_NAME) + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config) + def test_pim6_03_basic(self): + interfaces = Section.interfaces('ethernet') + join_prune_interval = '123' + keep_alive_timer = '77' + packets = '5' + register_suppress_time = '99' + dr_priority = '100' + hello = '50' + + self.cli_set(base_path + ['join-prune-interval', join_prune_interval]) + self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer]) + self.cli_set(base_path + ['packets', packets]) + self.cli_set(base_path + ['register-suppress-time', register_suppress_time]) + + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'dr-priority', dr_priority]) + self.cli_set(base_path + ['interface', interface, 'hello', hello]) + self.cli_set(base_path + ['interface', interface, 'no-bsm']) + self.cli_set(base_path + ['interface', interface, 'no-unicast-bsm']) + self.cli_set(base_path + ['interface', interface, 'passive']) + + self.cli_commit() + + # Verify FRR pim6d configuration + config = self.getFRRconfig(daemon=PROCESS_NAME) + self.assertIn(f'ipv6 pim join-prune-interval {join_prune_interval}', config) + self.assertIn(f'ipv6 pim keep-alive-timer {keep_alive_timer}', config) + self.assertIn(f'ipv6 pim packets {packets}', config) + self.assertIn(f'ipv6 pim register-suppress-time {register_suppress_time}', config) + + for interface in interfaces: + config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) + self.assertIn(f' ipv6 pim drpriority {dr_priority}', config) + self.assertIn(f' ipv6 pim hello {hello}', config) + self.assertIn(f' no ipv6 pim bsm', config) + self.assertIn(f' no ipv6 pim unicast-bsm', config) + self.assertIn(f' ipv6 pim passive', config) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py deleted file mode 100755 index 435189025..000000000 --- a/src/conf_mode/protocols_igmp.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-2023 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os - -from ipaddress import IPv4Address -from sys import exit - -from vyos import ConfigError -from vyos.config import Config -from vyos.utils.process import process_named_running -from vyos.utils.process import call -from vyos.template import render -from signal import SIGTERM - -from vyos import airbag -airbag.enable() - -# Required to use the full path to pimd, in another case daemon will not be started -pimd_cmd = f'/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1' - -config_file = r'/tmp/igmp.frr' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - igmp_conf = { - 'igmp_conf' : False, - 'pim_conf' : False, - 'igmp_proxy_conf' : False, - 'old_ifaces' : {}, - 'ifaces' : {} - } - if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')): - return None - - if conf.exists('protocols igmp-proxy'): - igmp_conf['igmp_proxy_conf'] = True - - if conf.exists('protocols pim'): - igmp_conf['pim_conf'] = True - - if conf.exists('protocols igmp'): - igmp_conf['igmp_conf'] = True - - conf.set_level('protocols igmp') - - # # Get interfaces - for iface in conf.list_effective_nodes('interface'): - igmp_conf['old_ifaces'].update({ - iface : { - 'version' : conf.return_effective_value('interface {0} version'.format(iface)), - 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)), - 'gr_join' : {} - } - }) - for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)): - igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join)) - - for iface in conf.list_nodes('interface'): - igmp_conf['ifaces'].update({ - iface : { - 'version' : conf.return_value('interface {0} version'.format(iface)), - 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)), - 'gr_join' : {} - } - }) - for gr_join in conf.list_nodes('interface {0} join'.format(iface)): - igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join)) - - return igmp_conf - -def verify(igmp): - if igmp is None: - return None - - if igmp['igmp_conf']: - # Check conflict with IGMP-Proxy - if igmp['igmp_proxy_conf']: - raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") - - # Check interfaces - if not igmp['ifaces']: - raise ConfigError(f"IGMP require defined interfaces!") - # Check, is this multicast group - for intfc in igmp['ifaces']: - for gr_addr in igmp['ifaces'][intfc]['gr_join']: - if not IPv4Address(gr_addr).is_multicast: - raise ConfigError(gr_addr + " not a multicast group") - -def generate(igmp): - if igmp is None: - return None - - render(config_file, 'frr/igmp.frr.j2', igmp) - return None - -def apply(igmp): - if igmp is None: - return None - - pim_pid = process_named_running('pimd') - if igmp['igmp_conf'] or igmp['pim_conf']: - if not pim_pid: - call(pimd_cmd) - - if os.path.exists(config_file): - call(f'vtysh -d pimd -f {config_file}') - os.remove(config_file) - elif pim_pid: - os.kill(int(pim_pid), SIGTERM) - - 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/protocols_pim.py b/src/conf_mode/protocols_pim.py index 0aaa0d2c6..09c3be8df 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -1,167 +1,162 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -from ipaddress import IPv4Address +from ipaddress import IPv4Network +from signal import SIGTERM from sys import exit from vyos.config import Config -from vyos import ConfigError +from vyos.config import config_dict_merge +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists from vyos.utils.process import process_named_running from vyos.utils.process import call -from vyos.template import render -from signal import SIGTERM - +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() -# Required to use the full path to pimd, in another case daemon will not be started -pimd_cmd = f'/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1' - -config_file = r'/tmp/pimd.frr' - def get_config(config=None): if config: conf = config else: conf = Config() - pim_conf = { - 'pim_conf' : False, - 'igmp_conf' : False, - 'igmp_proxy_conf' : False, - 'old_pim' : { - 'ifaces' : {}, - 'rp' : {} - }, - 'pim' : { - 'ifaces' : {}, - 'rp' : {} - } - } - if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')): - return None - - if conf.exists('protocols igmp-proxy'): - pim_conf['igmp_proxy_conf'] = True - - if conf.exists('protocols igmp'): - pim_conf['igmp_conf'] = True - - if conf.exists('protocols pim'): - pim_conf['pim_conf'] = True - - conf.set_level('protocols pim') - - # Get interfaces - for iface in conf.list_effective_nodes('interface'): - pim_conf['old_pim']['ifaces'].update({ - iface : { - 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)), - 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface)) - } - }) - for iface in conf.list_nodes('interface'): - pim_conf['pim']['ifaces'].update({ - iface : { - 'hello' : conf.return_value('interface {0} hello'.format(iface)), - 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)), - } - }) - - conf.set_level('protocols pim rp') - - # Get RPs addresses - for rp_addr in conf.list_effective_nodes('address'): - pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr)) - - for rp_addr in conf.list_nodes('address'): - pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr)) - - # Get RP keep-alive-timer - if conf.exists_effective('rp keep-alive-timer'): - pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer') - if conf.exists('rp keep-alive-timer'): - pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer') - - return pim_conf + base = ['protocols', 'pim'] + + pim = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + # We can not run both IGMP proxy and PIM at the same time - get IGMP + # proxy status + if conf.exists(['protocols', 'igmp-proxy']): + pim.update({'igmp_proxy_enabled' : {}}) + + # 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: + pim['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): + pim.update({'deleted' : ''}) + return pim + + # 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(**pim.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. + for interface in pim.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 'igmp' not in pim['interface'][interface]: + del default_values['interface'][interface]['igmp'] + + pim = config_dict_merge(default_values, pim) + return pim def verify(pim): - if pim is None: + if not pim or 'deleted' in pim: return None - if pim['pim_conf']: - # Check conflict with IGMP-Proxy - if pim['igmp_proxy_conf']: - raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") - - # Check interfaces - if not pim['pim']['ifaces']: - raise ConfigError(f"PIM require defined interfaces!") + if 'igmp_proxy_enabled' in pim: + raise ConfigError('IGMP proxy and PIM cannot be configured at the same time!') - if not pim['pim']['rp']: - raise ConfigError(f"RP address required") + if 'interface' not in pim: + raise ConfigError('PIM require defined interfaces!') - # Check unique multicast groups - uniq_groups = [] - for rp_addr in pim['pim']['rp']: - if not pim['pim']['rp'][rp_addr]: - raise ConfigError(f"Group should be specified for RP " + rp_addr) - for group in pim['pim']['rp'][rp_addr]: - if (group in uniq_groups): - raise ConfigError(f"Group range " + group + " specified cannot exact match another") + for interface in pim['interface']: + verify_interface_exists(interface) - # Check, is this multicast group - gr_addr = group.split('/') - if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'): - raise ConfigError(group + " not a multicast group") + if 'rp' in pim: + if 'address' not in pim['rp']: + raise ConfigError('PIM rendezvous point needs to be defined!') - uniq_groups.extend(pim['pim']['rp'][rp_addr]) + # Check unique multicast groups + unique = [] + pim_base_error = 'PIM rendezvous point group' + for address, address_config in pim['rp']['address'].items(): + if 'group' not in address_config: + raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') + + # Check if it is a multicast group + for gr_addr in address_config['group']: + if not IPv4Network(gr_addr).is_multicast: + raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') + if gr_addr in unique: + raise ConfigError(f'{pim_base_error} must be unique!') + unique.append(gr_addr) def generate(pim): - if pim is None: + if not pim or 'deleted' in pim: return None - - render(config_file, 'frr/pimd.frr.j2', pim) + pim['frr_pimd_config'] = render_to_string('frr/pimd.frr.j2', pim) return None def apply(pim): - if pim is None: + pim_daemon = 'pimd' + pim_pid = process_named_running(pim_daemon) + + if not pim or 'deleted' in pim: + if 'deleted' in pim: + os.kill(int(pim_pid), SIGTERM) + return None - pim_pid = process_named_running('pimd') - if pim['igmp_conf'] or pim['pim_conf']: - if not pim_pid: - call(pimd_cmd) + if not pim_pid: + call('/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1') + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(pim_daemon) + frr_cfg.modify_section(f'^ip pim') + frr_cfg.modify_section(f'^ip igmp') - if os.path.exists(config_file): - call("vtysh -d pimd -f " + config_file) - os.remove(config_file) - elif pim_pid: - os.kill(int(pim_pid), SIGTERM) + for key in ['interface', 'interface_removed']: + if key not in pim: + continue + for interface in pim[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + if 'frr_pimd_config' in pim: + frr_cfg.add_before(frr.default_add_before, pim['frr_pimd_config']) + frr_cfg.commit_configuration(pim_daemon) 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/protocols_pim6.py b/src/conf_mode/protocols_pim6.py index 6a1235ba5..2003a1014 100755 --- a/src/conf_mode/protocols_pim6.py +++ b/src/conf_mode/protocols_pim6.py @@ -1,102 +1,133 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 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 IPv6Address +from ipaddress import IPv6Network from sys import exit -from typing import Optional -from vyos import ConfigError, airbag, frr -from vyos.config import Config, ConfigDict +from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_interface_exists from vyos.template import render_to_string - +from vyos import ConfigError +from vyos import frr +from vyos import airbag airbag.enable() - def get_config(config=None): if config: conf = config else: conf = Config() base = ['protocols', 'pim6'] pim6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=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: pim6['interface_removed'] = list(interfaces_removed) - return pim6 + # 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): + pim6.update({'deleted' : ''}) + return pim6 + # 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(**pim6.kwargs, recursive=True) + + pim6 = config_dict_merge(default_values, pim6) + return pim6 def verify(pim6): - if pim6 is None: + if not pim6 or 'deleted' in pim6: return for interface, interface_config in pim6.get('interface', {}).items(): verify_interface_exists(interface) if 'mld' in interface_config: mld = interface_config['mld'] for group in mld.get('join', {}).keys(): # Validate multicast group address if not IPv6Address(group).is_multicast: raise ConfigError(f"{group} is not a multicast group") + if 'rp' in pim6: + if 'address' not in pim6['rp']: + raise ConfigError('PIM6 rendezvous point needs to be defined!') + + # Check unique multicast groups + unique = [] + pim_base_error = 'PIM6 rendezvous point group' + + if {'address', 'prefix-list6'} <= set(pim6['rp']): + raise ConfigError(f'{pim_base_error} supports either address or a prefix-list!') + + for address, address_config in pim6['rp']['address'].items(): + if 'group' not in address_config: + raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') + + # Check if it is a multicast group + for gr_addr in address_config['group']: + if not IPv6Network(gr_addr).is_multicast: + raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') + if gr_addr in unique: + raise ConfigError(f'{pim_base_error} must be unique!') + unique.append(gr_addr) def generate(pim6): - if pim6 is None: + if not pim6 or 'deleted' in pim6: return - pim6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pim6) - + return None def apply(pim6): if pim6 is None: return pim6_daemon = 'pim6d' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(pim6_daemon) for key in ['interface', 'interface_removed']: if key not in pim6: continue for interface in pim6[key]: - frr_cfg.modify_section( - f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) if 'new_frr_config' in pim6: frr_cfg.add_before(frr.default_add_before, pim6['new_frr_config']) frr_cfg.commit_configuration(pim6_daemon) - + 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/migration-scripts/pim/0-to-1 b/src/migration-scripts/pim/0-to-1 new file mode 100755 index 000000000..bf8af733c --- /dev/null +++ b/src/migration-scripts/pim/0-to-1 @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# T5736: igmp: migrate "protocols igmp" to "protocols pim" + +import sys +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['protocols', 'igmp'] +pim_base = ['protocols', 'pim'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +for interface in config.list_nodes(base + ['interface']): + base_igmp_iface = base + ['interface', interface] + pim_base_iface = pim_base + ['interface', interface] + + # Create IGMP note under PIM interface + if not config.exists(pim_base_iface + ['igmp']): + config.set(pim_base_iface + ['igmp']) + + if config.exists(base_igmp_iface + ['join']): + config.copy(base_igmp_iface + ['join'], pim_base_iface + ['igmp', 'join']) + config.set_tag(pim_base_iface + ['igmp', 'join']) + + new_join_base = pim_base_iface + ['igmp', 'join'] + for address in config.list_nodes(new_join_base): + if config.exists(new_join_base + [address, 'source']): + config.rename(new_join_base + [address, 'source'], 'source-address') + + if config.exists(base_igmp_iface + ['query-interval']): + config.copy(base_igmp_iface + ['query-interval'], pim_base_iface + ['igmp', 'query-interval']) + + if config.exists(base_igmp_iface + ['query-max-response-time']): + config.copy(base_igmp_iface + ['query-max-response-time'], pim_base_iface + ['igmp', 'query-max-response-time']) + + if config.exists(base_igmp_iface + ['version']): + config.copy(base_igmp_iface + ['version'], pim_base_iface + ['igmp', 'version']) + +config.delete(base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 820a3846c..8841b0eca 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -1,183 +1,181 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import argparse import logging import psutil from logging.handlers import SysLogHandler from shutil import rmtree from vyos.base import Warning from vyos.utils.io import ask_yes_no from vyos.utils.file import makedir from vyos.utils.process import call from vyos.utils.process import process_named_running # some default values watchfrr = '/usr/lib/frr/watchfrr.sh' vtysh = '/usr/bin/vtysh' frrconfig_tmp = '/tmp/frr_restart' # configure logging logger = logging.getLogger(__name__) logs_handler = SysLogHandler('/dev/log') logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s')) logger.addHandler(logs_handler) logger.setLevel(logging.INFO) # check if it is safe to restart FRR def _check_safety(): try: # print warning if not ask_yes_no('WARNING: This is a potentially unsafe function!\n' \ 'You may lose the connection to the router or active configuration after\n' \ 'running this command. Use it at your own risk!\n\n' 'Continue?'): return False # check if another restart process already running if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1: message = 'Another restart_frr.py process is already running!' logger.error(message) if not ask_yes_no(f'\n{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if watchfrr.sh is running tmp = os.path.basename(watchfrr) if process_named_running(tmp): message = f'Another {tmp} process is already running.' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if vtysh is running if process_named_running('vtysh'): message = 'vtysh process is executed by another task.' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False # check if temporary directory exists if os.path.exists(frrconfig_tmp): message = f'Temporary directory "{frrconfig_tmp}" already exists!' logger.error(message) if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \ 'Do you want to process anyway?'): return False except: logger.error("Something goes wrong in _check_safety()") return False # return True if all check was passed or user confirmed to ignore they results return True # write active config to file def _write_config(): # create temporary directory makedir(frrconfig_tmp) # save frr.conf to it command = f'{vtysh} -n -w --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) if return_code != 0: logger.error(f'Failed to save active config: "{command}" returned exit code: {return_code}') return False logger.info(f'Active config saved to {frrconfig_tmp}') return True # clear and remove temporary directory def _cleanup(): if os.path.isdir(frrconfig_tmp): rmtree(frrconfig_tmp) # restart daemon def _daemon_restart(daemon): command = f'{watchfrr} restart {daemon}' return_code = call(command) if not return_code == 0: logger.error(f'Failed to restart daemon "{daemon}"!') return False # return True if restarted successfully logger.info(f'Daemon "{daemon}" restarted!') return True # reload old config def _reload_config(daemon): if daemon != '': command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} -d {daemon} 2> /dev/null' else: command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} 2> /dev/null' return_code = call(command) if not return_code == 0: logger.error('Failed to re-install configuration!') return False # return True if restarted successfully logger.info('Configuration re-installed successfully!') return True # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -# Full list of FRR 9.0/stable daemons for reference -#cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'sharpd', 'bfdd', 'fabricd', 'pathd'], required=False, nargs='*', help='select single or multiple daemons') -cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() # main logic # restart daemon if cmd_args.action == 'restart': # check if it is safe to restart FRR if not _check_safety(): print("\nOne of the safety checks was failed or user aborted command. Exiting.") exit(1) if not _write_config(): print("Failed to save active config") _cleanup() exit(1) # a little trick to make further commands more clear if not cmd_args.daemon: cmd_args.daemon = [''] # check all daemons if they are running if cmd_args.daemon != ['']: for daemon in cmd_args.daemon: if not process_named_running(daemon): Warning('some of listed daemons are not running!') # run command to restart daemon for daemon in cmd_args.daemon: if not _daemon_restart(daemon): print('Failed to restart daemon: {daemon}') _cleanup() exit(1) # reinstall old configuration _reload_config(daemon) # cleanup after all actions _cleanup() exit(0)