diff options
| author | Viacheslav <v.gletenko@vyos.io> | 2025-08-07 08:43:24 +0000 |
|---|---|---|
| committer | Viacheslav <v.gletenko@vyos.io> | 2025-08-07 08:43:24 +0000 |
| commit | 9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720 (patch) | |
| tree | d93a2fce931a8203219cfacf39b4137c82042c80 | |
| parent | 8b9eabd3b636ab5bc1542843f265718c2e245baf (diff) | |
| parent | 1550cb9b84595a672d99694b838aa633800452ec (diff) | |
| download | vyos-1x-9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720.tar.gz vyos-1x-9e8b9eea45d92d6e886b1fbd6ac1b832ae44c720.zip | |
T7697: Merge remote-tracking branch 'vpp/current' into vyos-1x
78 files changed, 12881 insertions, 0 deletions
diff --git a/data/config-mode-dependencies/vyos-vpp.json b/data/config-mode-dependencies/vyos-vpp.json new file mode 100644 index 000000000..32b53508e --- /dev/null +++ b/data/config-mode-dependencies/vyos-vpp.json @@ -0,0 +1,69 @@ +{ + "vpp": { + "ethernet": ["interfaces_ethernet"], + "vpp_interfaces_bonding": ["vpp_interfaces_bonding"], + "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], + "vpp_interfaces_ethernet": ["vpp_interfaces_ethernet"], + "vpp_interfaces_geneve": ["vpp_interfaces_geneve"], + "vpp_interfaces_gre": ["vpp_interfaces_gre"], + "vpp_interfaces_ipip": ["vpp_interfaces_ipip"], + "vpp_interfaces_loopback": ["vpp_interfaces_loopback"], + "vpp_interfaces_vxlan": ["vpp_interfaces_vxlan"], + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"], + "vpp_sflow": ["vpp_sflow"] + }, + "vpp_interfaces_bonding": { + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_ethernet": { + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_geneve": { + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_gre": { + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_ipip": { + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_loopback": { + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + }, + "vpp_interfaces_vxlan": { + "vpp_interfaces_bridge": ["vpp_interfaces_bridge"], + "vpp_interfaces_xconnect": ["vpp_interfaces_xconnect"], + "vpp_acl": ["vpp_acl"], + "vpp_nat": ["vpp_nat"], + "vpp_nat_cgnat": ["vpp_nat_cgnat"], + "vpp_kernel_interface": ["vpp_kernel-interfaces"] + } +} + diff --git a/data/templates/vpp/override.conf.j2 b/data/templates/vpp/override.conf.j2 new file mode 100644 index 000000000..04bb7be15 --- /dev/null +++ b/data/templates/vpp/override.conf.j2 @@ -0,0 +1,14 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists= +ConditionPathExists=/run/vpp/vpp.conf +OnFailure=vpp-failure-handler.service + +[Service] +EnvironmentFile= +ExecStart= +ExecStart=/usr/bin/vpp -c /run/vpp/vpp.conf +WorkingDirectory= +WorkingDirectory=/run/vpp +Restart=no diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 new file mode 100644 index 000000000..b7515889a --- /dev/null +++ b/data/templates/vpp/startup.conf.j2 @@ -0,0 +1,209 @@ +# Generated by ${vyos_conf_scripts_dir}/vpp.py + +unix { + nodaemon + log /var/log/vpp.log + full-coredump + cli-listen /run/vpp/cli.sock + gid vpp + # exec /etc/vpp/bootstrap.vpp +{% if unix is vyos_defined %} +{% if unix.poll_sleep_usec is vyos_defined %} + poll-sleep-usec {{ unix.poll_sleep_usec }} +{% endif %} +{% endif %} +} + +{% if cpu is vyos_defined %} +cpu { +{% if cpu.main_core is vyos_defined %} + main-core {{ cpu.main_core }} +{% endif %} +{% if cpu.corelist_workers is vyos_defined %} + corelist-workers {{ cpu.corelist_workers | join(',') }} +{% endif %} +{% if cpu.skip_cores is vyos_defined %} + skip-cores {{ cpu.skip_cores }} +{% endif %} +{% if cpu.workers is vyos_defined %} + workers {{ cpu.workers }} +{% endif %} +} +{% endif %} + +{% if buffers is vyos_defined %} +buffers { +{% if buffers.buffers_per_numa is vyos_defined %} + buffers-per-numa {{ buffers.buffers_per_numa }} +{% endif %} +{% if buffers.data_size is vyos_defined %} + default data-size {{ buffers.data_size }} +{% endif %} +{% if buffers.page_size is vyos_defined %} + page-size {{ buffers.page_size }} +{% endif %} +} +{% endif %} + +{% if ipv6 is vyos_defined %} +ip6 { +{% if ipv6.hash_buckets is vyos_defined %} + hash-buckets {{ ipv6.hash_buckets }} +{% endif %} +{% if ipv6.heap_size is vyos_defined %} + heap-size {{ ipv6.heap_size }} +{% endif %} +} +{% endif %} + +{% if l2learn is vyos_defined %} +l2learn { +{% if l2learn.limit is vyos_defined %} + limit {{ l2learn.limit }} +{% endif %} +} +{% endif %} + +{% if logging is vyos_defined %} +logging { +{% if logging.default_log_level is vyos_defined %} + default-log-level {{ logging.default_log_level }} + default-syslog-log-level {{ logging.default_log_level }} +{% endif %} +} +{% endif %} + +{% if physmem is vyos_defined %} +physmem { +{% if physmem.max_size is vyos_defined %} + max-size {{ physmem.max_size }} +{% endif %} +} +{% endif %} + +plugins { + path /usr/lib/x86_64-linux-gnu/vpp_plugins/ + plugin default { disable } + plugin af_xdp_plugin.so { enable } + plugin avf_plugin.so { enable } + plugin dpdk_plugin.so { enable } + plugin vmxnet3_plugin.so { enable } + plugin lacp_plugin.so { enable } + plugin linux_cp_plugin.so { enable } + plugin linux_nl_plugin.so { enable } + plugin pppoe_plugin.so { enable } + plugin sflow_plugin.so { enable } + # NAT uncomment if needed + # plugin cnat_plugin.so { enable } + plugin nat_plugin.so { enable } + plugin nat44_ei_plugin.so { enable } + plugin det44_plugin.so { enable } + # plugin nat44_ei_plugin.so { enable } + # plugin nat64_plugin.so { enable } + # plugin nat66_plugin.so { enable } + # plugin pnat_plugin.so { enable } + # Tunnels + plugin geneve_plugin.so { enable } + plugin gre_plugin.so { enable } + plugin vxlan_plugin.so { enable } + # Secure + # plugin ikev2_plugin.so { enable } + # plugin dns_plugin.so { enable } # Probably required for FQDN peers +{% if ipsec is vyos_defined %} + plugin crypto_ipsecmb_plugin.so { enable } + plugin crypto_native_plugin.so { enable } + plugin crypto_openssl_plugin.so { enable } +{% endif %} + # plugin wireguard_plugin.so { enable } + # ACL + plugin acl_plugin.so { enable } +} + +linux-cp { + lcp-sync + lcp-auto-subint +{% if lcp.ignore_kernel_routes is not vyos_defined %} + route-no-paths +{% endif %} +} + +{% if lcp.netlink is vyos_defined %} +linux-nl { +{% if lcp.netlink.rx_buffer_size is vyos_defined %} + nl-rx-buffer-size {{ lcp.netlink.rx_buffer_size }} +{% endif %} +{% if lcp.netlink.batch_size is vyos_defined %} + nl-batch-size {{ lcp.netlink.batch_size }} +{% endif %} +{% if lcp.netlink.batch_delay_ms is vyos_defined %} + nl-batch-delay-ms {{ lcp.netlink.batch_delay_ms }} +{% endif %} +} +{% endif %} + +{% if statseg is vyos_defined %} +statseg { +{% if statseg.size is vyos_defined %} + size {{ statseg.size }} +{% endif %} +{% if statseg.page_size is vyos_defined %} + page-size {{ statseg.page_size }} +{% endif %} +} +{% endif %} + +{% if memory is vyos_defined %} +memory { +{% if memory.main_heap_size is vyos_defined %} + main-heap-size {{ memory.main_heap_size }} +{% endif %} +{% if memory.main_heap_page_size is vyos_defined %} + main-heap-page-size {{ memory.main_heap_page_size }} +{% endif %} +{% if memory.default_hugepage_size is vyos_defined %} + default-hugepage-size {{ memory.default_hugepage_size }} +{% endif %} +} +{% endif %} + +dpdk { + # Whitelist the fake PCI address 0000:00:00.0 + # This prevents all devices from being added to VPP-DPDK by default + dev 0000:00:00.0 +{% for iface, iface_config in interface.items() %} +{% if iface_config.driver == 'dpdk' %} + dev {{ iface_config.dpdk_options.dev_id }} { + name {{ iface }} +{% if iface_config.dpdk_options.num_rx_desc is vyos_defined %} + num-rx-desc {{ iface_config.dpdk_options.num_rx_desc }} +{% endif %} +{% if iface_config.dpdk_options.num_tx_desc is vyos_defined %} + num-tx-desc {{ iface_config.dpdk_options.num_tx_desc }} +{% endif %} +{% if iface_config.dpdk_options.num_rx_queues is vyos_defined %} + num-rx-queues {{ iface_config.dpdk_options.num_rx_queues }} +{% endif %} +{% if iface_config.dpdk_options.num_tx_queues is vyos_defined %} + num-tx-queues {{ iface_config.dpdk_options.num_tx_queues }} +{% endif %} + } +{% endif %} +{% endfor %} + uio-bind-force +} + +{% if ipsec is vyos_defined %} +linux-xfrm-nl { + enable-route-mode-ipsec + interface {{ ipsec.interface_type }} +{% if ipsec.netlink.batch_delay_ms is vyos_defined %} + nl-batch-delay-ms {{ ipsec.netlink.batch_delay_ms }} +{% endif %} +{% if ipsec.netlink.batch_size is vyos_defined %} + nl-batch-size {{ ipsec.netlink.batch_size }} +{% endif %} +{% if ipsec.netlink.rx_buffer_size is vyos_defined %} + nl-rx-buffer-size {{ ipsec.netlink.rx_buffer_size }} +{% endif %} +} +{% endif %} diff --git a/interface-definitions/include/kernel-interface-tap.xml.i b/interface-definitions/include/kernel-interface-tap.xml.i new file mode 100644 index 000000000..83399f1f4 --- /dev/null +++ b/interface-definitions/include/kernel-interface-tap.xml.i @@ -0,0 +1,15 @@ +<!-- include start from kernel-interface-tap.xml.i --> +<leafNode name="kernel-interface"> + <properties> + <help>Kernel interface name</help> + <valueHelp> + <format>vpptapN</format> + <description>Kernel interface name</description> + </valueHelp> + <constraint> + <regex>vpptap\d+</regex> + </constraint> + <constraintErrorMessage>Kernel interface must start with vpptapN</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/kernel-interface-tun.xml.i b/interface-definitions/include/kernel-interface-tun.xml.i new file mode 100644 index 000000000..3cbd5ab30 --- /dev/null +++ b/interface-definitions/include/kernel-interface-tun.xml.i @@ -0,0 +1,15 @@ +<!-- include start from kernel-interface-tun.xml.i --> +<leafNode name="kernel-interface"> + <properties> + <help>Kernel interface name</help> + <valueHelp> + <format>vpptunN</format> + <description>Kernel interface name</description> + </valueHelp> + <constraint> + <regex>vpptun\d+</regex> + </constraint> + <constraintErrorMessage>Kernel interface must start with vpptunN</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/unformat_log2_page_size.xml.i b/interface-definitions/include/unformat_log2_page_size.xml.i new file mode 100644 index 000000000..ce6f70dc8 --- /dev/null +++ b/interface-definitions/include/unformat_log2_page_size.xml.i @@ -0,0 +1,17 @@ +<!-- include start from unformat_log2_page_size.xml.i --> +<completionHelp> + <list>default default-hugepage</list> + <script>sudo ${vyos_completion_dir}/list_mem_page_size.py</script> +</completionHelp> +<valueHelp> + <format>default</format> + <description>Default</description> +</valueHelp> +<valueHelp> + <format>default-hugepage</format> + <description>Default huge-page</description> +</valueHelp> +<constraint> + <regex>(default|default-hugepage|4K|8K|1024K|64K|256K|2048K|4096K|16384K|262144K|1048576K|16777216K|1M|2M|4M|16M|256M|1024M|16384M|1G|16G)</regex> +</constraint> +<!-- include end --> diff --git a/interface-definitions/include/unformat_memory_size.xml.i b/interface-definitions/include/unformat_memory_size.xml.i new file mode 100644 index 000000000..bb8f4ae67 --- /dev/null +++ b/interface-definitions/include/unformat_memory_size.xml.i @@ -0,0 +1,22 @@ +<!-- include start from unformat_memory_size.xml.i --> +<valueHelp> + <format><number></format> + <description>byte</description> +</valueHelp> +<valueHelp> + <format><number>K</format> + <description>Kilobyte</description> +</valueHelp> +<valueHelp> + <format><number>M</format> + <description>Megabyte</description> +</valueHelp> +<valueHelp> + <format><number>G</format> + <description>Gigabyte</description> +</valueHelp> +<constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <regex>(\d+|\d+K|\d+M|\d+G)</regex> +</constraint> +<!-- include end --> diff --git a/interface-definitions/include/version/vpp-version.xml.i b/interface-definitions/include/version/vpp-version.xml.i new file mode 100644 index 000000000..96a69c92f --- /dev/null +++ b/interface-definitions/include/version/vpp-version.xml.i @@ -0,0 +1,3 @@ +<!-- include start from include/version/vpp-version.xml.i --> +<syntaxVersion component='vpp' version='1'></syntaxVersion> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i new file mode 100644 index 000000000..a719a6223 --- /dev/null +++ b/interface-definitions/include/vpp/acl_common_interface_ip_rule.xml.i @@ -0,0 +1,25 @@ +<!-- include start from vpp/acl_common_interface_ip_rule.xml.i --> +<tagNode name="acl-tag"> + <properties> + <help>ACL rule (tag) number</help> + <valueHelp> + <format>u32</format> + <description>Number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Number must be between 1 and 4294967295</constraintErrorMessage> + </properties> + <children> + <leafNode name="tag-name"> + <properties> + <help>ACL tag name</help> + <completionHelp> + <path>vpp acl ip tag-name</path> + </completionHelp> + </properties> + </leafNode> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/acl_port_range.xml.i b/interface-definitions/include/vpp/acl_port_range.xml.i new file mode 100644 index 000000000..26bd2de45 --- /dev/null +++ b/interface-definitions/include/vpp/acl_port_range.xml.i @@ -0,0 +1,18 @@ +<!-- include start from vpp/acl_port_range.xml.i --> +<leafNode name="port"> + <properties> + <help>Port number or range</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <valueHelp> + <format>range</format> + <description>Numbered port range (e.g., 1001-1005)</description> + </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/acl_prefix.xml.i b/interface-definitions/include/vpp/acl_prefix.xml.i new file mode 100644 index 000000000..790153c8f --- /dev/null +++ b/interface-definitions/include/vpp/acl_prefix.xml.i @@ -0,0 +1,20 @@ +<!-- include start from vpp/acl_prefix.xml.i --> +<leafNode name="prefix"> + <properties> + <help>IP prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> + diff --git a/interface-definitions/include/vpp/acl_tcp_flags.xml.i b/interface-definitions/include/vpp/acl_tcp_flags.xml.i new file mode 100644 index 000000000..da17f3fe5 --- /dev/null +++ b/interface-definitions/include/vpp/acl_tcp_flags.xml.i @@ -0,0 +1,50 @@ +<!-- include start from vpp/acl_tcp_flags.xml.i --> +<leafNode name="syn"> + <properties> + <help>Synchronise flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="ack"> + <properties> + <help>Acknowledge flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="fin"> + <properties> + <help>Finish flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="rst"> + <properties> + <help>Reset flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="urg"> + <properties> + <help>Urgent flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="psh"> + <properties> + <help>Push flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="ecn"> + <properties> + <help>Explicit Congestion Notification flag</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="cwr"> + <properties> + <help>Congestion Window Reduced flag</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/iface_rx_mode.xml.i b/interface-definitions/include/vpp/iface_rx_mode.xml.i new file mode 100644 index 000000000..374654b08 --- /dev/null +++ b/interface-definitions/include/vpp/iface_rx_mode.xml.i @@ -0,0 +1,25 @@ +<!-- include start from vpp/iface_rx_mode.xml.i --> +<leafNode name="rx-mode"> + <properties> + <help>Receive packet processing mode</help> + <completionHelp> + <list>polling interrupt adaptive</list> + </completionHelp> + <valueHelp> + <format>polling</format> + <description>Constantly check for new data</description> + </valueHelp> + <valueHelp> + <format>interrupt</format> + <description>Interrupt mode</description> + </valueHelp> + <valueHelp> + <format>adaptive</format> + <description>Adaptive mode</description> + </valueHelp> + <constraint> + <regex>(polling|interrupt|adaptive)</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/nat_address_range.xml.i b/interface-definitions/include/vpp/nat_address_range.xml.i new file mode 100644 index 000000000..48648f91a --- /dev/null +++ b/interface-definitions/include/vpp/nat_address_range.xml.i @@ -0,0 +1,20 @@ +<!-- include start from vpp/nat_address_range.xml.i --> +<leafNode name="address"> + <properties> + <help>IP address or range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-range"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/nat_interface.xml.i b/interface-definitions/include/vpp/nat_interface.xml.i new file mode 100644 index 000000000..20a7356bf --- /dev/null +++ b/interface-definitions/include/vpp/nat_interface.xml.i @@ -0,0 +1,11 @@ +<!-- include start from vpp/nat_interface.xml.i --> +<leafNode name="interface"> + <properties> + <help>Add IP address from an interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/nat_protocol.xml.i b/interface-definitions/include/vpp/nat_protocol.xml.i new file mode 100644 index 000000000..b88fd2b62 --- /dev/null +++ b/interface-definitions/include/vpp/nat_protocol.xml.i @@ -0,0 +1,30 @@ +<!-- include start from vpp/nat_protocol.xml.i --> +<leafNode name="protocol"> + <properties> + <help>Protocol</help> + <completionHelp> + <list>tcp udp icmp all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All protocols (TCP, UDP, and ICMP)</description> + </valueHelp> + <valueHelp> + <format>icmp</format> + <description>Internet Control Message Protocol (ICMP)</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>Transmission Control Protocol (TCP)</description> + </valueHelp> + <valueHelp> + <format>udp</format> + <description>User Datagram Protocol (UDP)</description> + </valueHelp> + <constraint> + <regex>(tcp|udp|icmp|all)</regex> + </constraint> + </properties> + <defaultValue>all</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp/netlink.xml.i b/interface-definitions/include/vpp/netlink.xml.i new file mode 100644 index 000000000..903100d00 --- /dev/null +++ b/interface-definitions/include/vpp/netlink.xml.i @@ -0,0 +1,45 @@ +<!-- include start from vpp/netlink.xml.i --> +<node name="netlink"> + <properties> + <help>Netlink options</help> + </properties> + <children> + <leafNode name="rx-buffer-size"> + <properties> + <help>Receive buffer size</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Receive buffer size</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="batch-size"> + <properties> + <help>Batch size</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Batch size</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="batch-delay-ms"> + <properties> + <help>Batch delay</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Batch delay (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> +</children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/vpp/vif.xml.i b/interface-definitions/include/vpp/vif.xml.i new file mode 100644 index 000000000..8f0e9772f --- /dev/null +++ b/interface-definitions/include/vpp/vif.xml.i @@ -0,0 +1,21 @@ +<!-- include start from vpp/vif.xml.i --> +<tagNode name="vif"> + <properties> + <help>Virtual Local Area Network (VLAN) ID</help> + <valueHelp> + <format>u32:0-4094</format> + <description>Virtual Local Area Network (VLAN) ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface/disable.xml.i> + #include <include/interface/mtu-68-16000.xml.i> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/vpp_host_resources.xml.i b/interface-definitions/include/vpp_host_resources.xml.i new file mode 100644 index 000000000..1706c8c87 --- /dev/null +++ b/interface-definitions/include/vpp_host_resources.xml.i @@ -0,0 +1,35 @@ +<!-- include start from vpp_host_resources.xml.i --> +<node name="host-resources"> + <properties> + <help>Host resources control</help> + </properties> + <children> + <leafNode name="max-map-count"> + <properties> + <help>Maximum number of memory map areas a process may have</help> + <valueHelp> + <format>u32:65535-2147483647</format> + <description>Areas count</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 65535-2147483647"/> + </constraint> + </properties> + <defaultValue>65535</defaultValue> + </leafNode> + <leafNode name="shmmax"> + <properties> + <help>Maximum shared memory segment size that can be created</help> + <valueHelp> + <format>u32:0-18446744073709551612</format> + <description>Size in bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-18446744073709551612"/> + </constraint> + </properties> + <defaultValue>2147483648</defaultValue> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in new file mode 100644 index 000000000..dff56a228 --- /dev/null +++ b/interface-definitions/vpp.xml.in @@ -0,0 +1,1578 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpp" owner="${vyos_conf_scripts_dir}/vpp.py"> + <properties> + <help>Accelerated data-plane</help> + <priority>295</priority> + </properties> + <children> + <node name='interfaces'> + <properties> + <help>Network interfaces</help> + <priority>300</priority> + </properties> + <children> + <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/vpp_interfaces_bonding.py"> + <properties> + <priority>324</priority> + <help>Bonding Interface/Link Aggregation</help> + <constraint> + <regex>bond[0-9]+</regex> + </constraint> + <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage> + <valueHelp> + <format>bondN</format> + <description>Bonding interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + <leafNode name="hash-policy"> + <properties> + <help>Bonding transmit hash policy</help> + <completionHelp> + <list>layer2 layer2+3 layer3+4</list> + </completionHelp> + <valueHelp> + <format>layer2</format> + <description>use MAC addresses to generate the hash</description> + </valueHelp> + <valueHelp> + <format>layer2+3</format> + <description>combine MAC address and IP address to make hash</description> + </valueHelp> + <valueHelp> + <format>layer3+4</format> + <description>combine IP address and port to make hash</description> + </valueHelp> + <constraint> + <regex>(layer2\+3|layer3\+4|layer2)</regex> + </constraint> + <constraintErrorMessage>hash-policy must be layer2 layer2+3 layer3+4</constraintErrorMessage> + </properties> + <defaultValue>layer2</defaultValue> + </leafNode> + #include <include/interface/mac.xml.i> + #include <include/kernel-interface-tun.xml.i> + <leafNode name="mode"> + <properties> + <help>Bonding mode</help> + <completionHelp> + <list>802.3ad active-backup broadcast round-robin xor-hash</list> + </completionHelp> + <valueHelp> + <format>802.3ad</format> + <description>IEEE 802.3ad Dynamic link aggregation</description> + </valueHelp> + <valueHelp> + <format>active-backup</format> + <description>Fault tolerant: only one slave in the bond is active</description> + </valueHelp> + <valueHelp> + <format>broadcast</format> + <description>Fault tolerant: transmits everything on all slave interfaces</description> + </valueHelp> + <valueHelp> + <format>round-robin</format> + <description>Load balance: transmit packets in sequential order</description> + </valueHelp> + <valueHelp> + <format>xor-hash</format> + <description>Distribute based on MAC address</description> + </valueHelp> + <constraint> + <regex>(802.3ad|active-backup|broadcast|round-robin|xor-hash)</regex> + </constraint> + <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, or xor</constraintErrorMessage> + </properties> + <defaultValue>802.3ad</defaultValue> + </leafNode> + <node name="member"> + <properties> + <help>Bonding member interfaces</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --bondable</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/vpp_interfaces_bridge.py"> + <properties> + <priority>327</priority> + <help>Bridge domain</help> + <constraint> + <!-- Bridge domain 0 is reserved to vpp default --> + <regex>br(?!0$)[0-9]+</regex> + </constraint> + <constraintErrorMessage>Bridge domain must be named brN and br0 is reserved</constraintErrorMessage> + <valueHelp> + <format>brN</format> + <description>Bridge interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + <node name="member"> + <properties> + <help>Bridge member interfaces</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="bvi"> + <properties> + <help>Bridge Virtual Interface (BVI)</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </tagNode> + <!-- Disable as geneve interface index in the VPP always 0 and not configured --> + <!-- <tagNode name="geneve" owner="${vyos_conf_scripts_dir}/vpp_interfaces_geneve.py"> + <properties> + <priority>325</priority> + <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help> + <constraint> + <regex>geneve[0-9]+</regex> + </constraint> + <constraintErrorMessage>GENEVE interface must be named geneveN</constraintErrorMessage> + <valueHelp> + <format>geneveN</format> + <description>GENEVE interface name</description> + </valueHelp> + </properties> + <children> --> + <!-- #include <include/source-address-ipv4-ipv6.xml.i> --> + <!-- #include <include/interface/tunnel-remote.xml.i> --> + <!-- #include <include/vni.xml.i> --> + <!-- #include <include/kernel-interface-tun.xml.i> --> + <!-- </children> --> + <!-- </tagNode> --> + <tagNode name="gre" owner="${vyos_conf_scripts_dir}/vpp_interfaces_gre.py"> + <properties> + <priority>325</priority> + <help>Generic Network Encapsulation (GRE) Interface</help> + <constraint> + <regex>gre[0-9]+</regex> + </constraint> + <constraintErrorMessage>GRE interface must be named greN</constraintErrorMessage> + <valueHelp> + <format>greN</format> + <description>GRE interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + <leafNode name="mode"> + <properties> + <help>GRE tunnel mode</help> + <completionHelp> + <list>point-to-point point-to-multipoint</list> + </completionHelp> + <valueHelp> + <format>point-to-point</format> + <description>Point to point mode</description> + </valueHelp> + <valueHelp> + <format>point-to-multipoint</format> + <description>Point to multipoint mode</description> + </valueHelp> + <constraint> + <regex>(point-to-point|point-to-multipoint)</regex> + </constraint> + <constraintErrorMessage>Invalid mode, must be one of: point-to-point or point-to-multipoint</constraintErrorMessage> + </properties> + <defaultValue>point-to-point</defaultValue> + </leafNode> + <leafNode name="tunnel-type"> + <properties> + <help>GRE tunnel type</help> + <completionHelp> + <list>erspan l3 teb</list> + </completionHelp> + <valueHelp> + <format>erspan</format> + <description>Encapsulated Remote Switched Port Analyzer</description> + </valueHelp> + <valueHelp> + <format>l3</format> + <description>Generic Routing Encapsulation (network layer)</description> + </valueHelp> + <valueHelp> + <format>teb</format> + <description>L2 Transparent Ethernet Bridge</description> + </valueHelp> + <constraint> + <regex>(erspan|l3|teb)</regex> + </constraint> + <constraintErrorMessage>Invalid encapsulation, must be one of: l3, teb or erspan</constraintErrorMessage> + </properties> + <defaultValue>l3</defaultValue> + </leafNode> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/interface/tunnel-remote.xml.i> + #include <include/kernel-interface-tun.xml.i> + </children> + </tagNode> + <tagNode name="ipip" owner="${vyos_conf_scripts_dir}/vpp_interfaces_ipip.py"> + <properties> + <priority>325</priority> + <help>IP encapsulation tunnel interface</help> + <constraint> + <regex>ipip[0-9]+</regex> + </constraint> + <constraintErrorMessage>IPIP tunnel interface must be named ipipN</constraintErrorMessage> + <valueHelp> + <format>ipipN</format> + <description>IPIP tunnel interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/interface/tunnel-remote.xml.i> + #include <include/kernel-interface-tun.xml.i> + </children> + </tagNode> + <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/vpp_interfaces_loopback.py"> + <properties> + <priority>325</priority> + <help>Loopback Interface</help> + <constraint> + <regex>lo[0-9]+</regex> + </constraint> + <constraintErrorMessage>Loopback interface must be named loN</constraintErrorMessage> + <valueHelp> + <format>loN</format> + <description>Loopback interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + #include <include/kernel-interface-tun.xml.i> + </children> + </tagNode> + <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/vpp_interfaces_vxlan.py"> + <properties> + <priority>325</priority> + <help>Virtual Extensible LAN (VXLAN) Interface</help> + <constraint> + <regex>vxlan[0-9]+</regex> + </constraint> + <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage> + <valueHelp> + <format>vxlanN</format> + <description>VXLAN interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/interface/tunnel-remote.xml.i> + #include <include/vni.xml.i> + #include <include/kernel-interface-tap.xml.i> + </children> + </tagNode> + <tagNode name="xconnect" owner="${vyos_conf_scripts_dir}/vpp_interfaces_xconnect.py"> + <properties> + <help>Layer 2 cross connect</help> + <priority>325</priority> + <constraint> + <regex>xcon[0-9]+</regex> + </constraint> + <constraintErrorMessage>Cross connect must be named xconN</constraintErrorMessage> + <valueHelp> + <format>xconN</format> + <description>Cross connect interface name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + <node name="member"> + <properties> + <help>Cross connect pair mamber interfaces</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <node name="settings"> + <properties> + <help>VPP settings</help> + </properties> + <children> + <node name="buffers"> + <properties> + <help>Buffer settings</help> + </properties> + <children> + <leafNode name="buffers-per-numa"> + <properties> + <help>Number of buffers per numa node</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Number of buffers</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="data-size"> + <properties> + <help>Size of buffer data area</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Size of buffer data area</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="page-size"> + <properties> + <help>Set the page-size for buffer allocation</help> + #include <include/unformat_log2_page_size.xml.i> + </properties> + </leafNode> + </children> + </node> + <node name="cpu"> + <properties> + <help>CPU settings</help> + </properties> + <children> + <leafNode name="corelist-workers"> + <properties> + <help>List of cores worker threads</help> + <valueHelp> + <format><id></format> + <description>CPU core id</description> + </valueHelp> + <valueHelp> + <format><idN>-<idM></format> + <description>CPU core id range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-512"/> + </constraint> + <constraintErrorMessage>not a valid CPU core value or range</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="main-core"> + <properties> + <help>Main core</help> + <valueHelp> + <format>u32:0-512</format> + <description>Assign main thread to specific core</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-512"/> + </constraint> + </properties> + </leafNode> + <leafNode name="skip-cores"> + <properties> + <help>Skip cores</help> + <valueHelp> + <format>u32:1-512</format> + <description>Skip cores</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-512"/> + </constraint> + </properties> + </leafNode> + <leafNode name="workers"> + <properties> + <help>Create worker threads</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Worker threads</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-512"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + #include <include/vpp_host_resources.xml.i> + <tagNode name="interface"> + <properties> + <help>Interface</help> + <valueHelp> + <format>ethN</format> + <description>Interface name</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --type ethernet</script> + </completionHelp> + <constraint> + <validator name="ethernet-interface"/> + </constraint> + <constraintErrorMessage>Invalid interface name</constraintErrorMessage> + </properties> + <children> + <leafNode name="driver"> + <properties> + <help>Driver mode</help> + <completionHelp> + <list>dpdk xdp</list> + </completionHelp> + <valueHelp> + <format>dpdk</format> + <description>Data Plane Development Kit (DPDK)</description> + </valueHelp> + <valueHelp> + <format>xdp</format> + <description>eXpress Data Path (XDP)</description> + </valueHelp> + <constraint> + <regex>(dpdk|xdp)</regex> + </constraint> + </properties> + </leafNode> + #include <include/vpp/iface_rx_mode.xml.i> + <node name="dpdk-options"> + <properties> + <help>DPDK settings</help> + </properties> + <children> + <leafNode name="num-rx-desc"> + <properties> + <help>Receive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of descriptors in receive ring</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-tx-desc"> + <properties> + <help>Tranceive ring descriptors</help> + <valueHelp> + <format>u32:256-8192</format> + <description>Number of descriptors in tranceive ring</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-rx-queues"> + <properties> + <help>Number of receive queues</help> + <valueHelp> + <format>u32:1-8192</format> + <description>Number of receive queues</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="num-tx-queues"> + <properties> + <help>Number of tranceive queues</help> + <valueHelp> + <format>u32:1-8192</format> + <description>Number of tranceive queues</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-8192"/> + </constraint> + </properties> + </leafNode> + <leafNode name="promisc"> + <properties> + <help>Enable promisc mode on a vpp interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="xdp-options"> + <properties> + <help>XDP settings</help> + </properties> + <children> + <leafNode name="no-syscall-lock"> + <properties> + <help>Unlock multithreading in interrupt and adaptive modes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="num-rx-queues"> + <properties> + <help>Number of receive queues</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Number of receive queues to connect to</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>All</description> + </valueHelp> + <constraint> + <regex>(all)</regex> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>all</defaultValue> + </leafNode> + <leafNode name="promisc"> + <properties> + <help>Enable promisc mode on a real interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rx-queue-size"> + <properties> + <help>Receive queue size</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Size of receive queue</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="tx-queue-size"> + <properties> + <help>Tranceive queue size</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Size of tranceive queue</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="zero-copy"> + <properties> + <help>Enable zero-copy mode</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <node name="ipv6"> + <properties> + <help>IPv6 settings</help> + </properties> + <children> + <leafNode name="heap-size"> + <properties> + <help>IPv6 heap size</help> + #include <include/unformat_memory_size.xml.i> + </properties> + <defaultValue>32M</defaultValue> + </leafNode> + <leafNode name="hash-buckets"> + <properties> + <help>IPv6 forwarding table hash buckets</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>IPv6 forwarding table hash buckets</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>65536</defaultValue> + </leafNode> + </children> + </node> + <node name="ipsec"> + <properties> + <help>IPsec settings</help> + </properties> + <children> + <leafNode name="interface-type"> + <properties> + <help>IPsec interface type</help> + <completionHelp> + <list>ipsec ipip</list> + </completionHelp> + <valueHelp> + <format>ipsec</format> + <description>IPsec interface type</description> + </valueHelp> + <valueHelp> + <format>ipip</format> + <description>IPIP tunnel interface type</description> + </valueHelp> + <constraint> + <regex>(ipsec|ipip)</regex> + </constraint> + </properties> + <defaultValue>ipsec</defaultValue> + </leafNode> + #include <include/vpp/netlink.xml.i> + </children> + </node> + <node name="l2learn"> + <properties> + <help>Level 2 MAC address learning settings</help> + </properties> + <children> + <leafNode name="limit"> + <properties> + <help>Number of MAC addresses in the L2 FIB</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>Number of concurent entries</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + <defaultValue>4194304</defaultValue> + </leafNode> + </children> + </node> + <node name="lcp"> + <properties> + <help>Linux control plane setting</help> + </properties> + <children> + #include <include/vpp/netlink.xml.i> + <leafNode name="ignore-kernel-routes"> + <properties> + <help>Ignore kernel routes</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="logging"> + <properties> + <help>Logging settings</help> + </properties> + <children> + <leafNode name="default-log-level"> + <properties> + <help>default-log-level</help> + <completionHelp> + <list>alert crit debug disabled emerg err info notice warn</list> + </completionHelp> + <valueHelp> + <format>alert</format> + <description>Alert</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug</description> + </valueHelp> + <valueHelp> + <format>disabled</format> + <description>Disabled</description> + </valueHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Notice</description> + </valueHelp> + <valueHelp> + <format>warn</format> + <description>Warning</description> + </valueHelp> + <constraint> + <regex>(alert|crit|debug|disabled|emerg|err|info|notice|warn)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="memory"> + <properties> + <help>Memory settings</help> + </properties> + <children> + <leafNode name="main-heap-size"> + <properties> + <help>Main heap size</help> + #include <include/unformat_memory_size.xml.i> + </properties> + <defaultValue>3G</defaultValue> + </leafNode> + <leafNode name="main-heap-page-size"> + <properties> + <help>Main heap page size</help> + #include <include/unformat_log2_page_size.xml.i> + </properties> + </leafNode> + <leafNode name="default-hugepage-size"> + <properties> + <help>Default hugepage size</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_mem_page_size.py --hugepage_only True </script> + </completionHelp> + </properties> + <defaultValue>2M</defaultValue> + </leafNode> + </children> + </node> + <node name="nat44"> + <properties> + <help>NAT settings</help> + </properties> + <children> + <node name="timeout"> + <properties> + <help>NAT44 session timeouts</help> + </properties> + <children> + <leafNode name="icmp"> + <properties> + <help>ICMP timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 60)</description> + </valueHelp> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + <leafNode name="tcp-established"> + <properties> + <help>TCP established timeout</help> + <valueHelp> + <format>u32</format> + <description>TCP established timeout in seconds (default: 7440)</description> + </valueHelp> + </properties> + <defaultValue>7440</defaultValue> + </leafNode> + <leafNode name="tcp-transitory"> + <properties> + <help>TCP transitory timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 240)</description> + </valueHelp> + </properties> + <defaultValue>240</defaultValue> + </leafNode> + <leafNode name="udp"> + <properties> + <help>UDP timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 300)</description> + </valueHelp> + </properties> + <defaultValue>300</defaultValue> + </leafNode> + </children> + </node> + <leafNode name="session-limit"> + <properties> + <help>Maximum number of sessions per thread</help> + <valueHelp> + <format>u32</format> + <description>Number of sessions</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Number of sessions must be between 1 and 4294967295</constraintErrorMessage> + </properties> + <defaultValue>64512</defaultValue> + </leafNode> + <leafNode name="workers"> + <properties> + <help>List of NAT workers</help> + <valueHelp> + <format><id></format> + <description>Worker id</description> + </valueHelp> + <valueHelp> + <format><idN>-<idM></format> + <description>Worker id range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-512"/> + </constraint> + <constraintErrorMessage>Not a valid value or range</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="no-forwarding"> + <properties> + <help>Do not forward packets which do not match existing NAT translations (static or dynamic)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="physmem"> + <properties> + <help>Physical memory settings</help> + </properties> + <children> + <leafNode name="max-size"> + <properties> + <help>Set memory size for protectable memory allocator (pmalloc) memory space</help> + #include <include/unformat_memory_size.xml.i> + </properties> + </leafNode> + </children> + </node> + <node name="statseg"> + <properties> + <help>Stats settings</help> + </properties> + <children> + <leafNode name="size"> + <properties> + <help>Size of stats segment</help> + #include <include/unformat_memory_size.xml.i> + </properties> + </leafNode> + <leafNode name="page-size"> + <properties> + <help>Stats page size</help> + #include <include/unformat_log2_page_size.xml.i> + </properties> + </leafNode> + </children> + </node> + <node name="unix"> + <properties> + <help>Unix settings</help> + </properties> + <children> + <leafNode name="poll-sleep-usec"> + <properties> + <help>Add a fixed-sleep between main loop poll</help> + <valueHelp> + <format>u32:0-500000</format> + <description>Sleep interval in microseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-500000"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + <node name="sflow" owner="${vyos_conf_scripts_dir}/vpp_sflow.py"> + <properties> + <help>VPP data-plane sFlow</help> + <priority>322</priority> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="sample-rate"> + <properties> + <help>sFlow sample rate</help> + <valueHelp> + <format>u32</format> + <description>sFlow sample rate</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <node name="nat"> + <properties> + <help>Network Address Translation (NAT) settings</help> + </properties> + <children> + <node name="cgnat" owner="${vyos_conf_scripts_dir}/vpp_nat_cgnat.py"> + <properties> + <help>Carrier-grade NAT (CGNAT)</help> + <priority>331</priority> + </properties> + <children> + <node name="interface"> + <properties> + <help>CGNAT interface setting</help> + </properties> + <children> + <leafNode name="inside"> + <properties> + <help>CGNAT inside interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="outside"> + <properties> + <help>CGNAT outside interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + <tagNode name="rule"> + <properties> + <help>Rule number for CGNAT</help> + <valueHelp> + <format>u32</format> + <description>Number of rule</description> + </valueHelp> + </properties> + <children> + <leafNode name="inside-prefix"> + <properties> + <help>Inside IPv4 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="outside-prefix"> + <properties> + <help>Outside IPv4 prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + #include <include/generic-description.xml.i> + </children> + </tagNode> + <node name="timeout"> + <properties> + <help>Timeouts for CGNAT sessions</help> + </properties> + <children> + <leafNode name="icmp"> + <properties> + <help>ICMP timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 60)</description> + </valueHelp> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + <leafNode name="tcp-established"> + <properties> + <help>TCP established timeout</help> + <valueHelp> + <format>u32</format> + <description>TCP established timeout in seconds (default: 7440)</description> + </valueHelp> + </properties> + <defaultValue>7440</defaultValue> + </leafNode> + <leafNode name="tcp-transitory"> + <properties> + <help>TCP transitory timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 240)</description> + </valueHelp> + </properties> + <defaultValue>240</defaultValue> + </leafNode> + <leafNode name="udp"> + <properties> + <help>UDP timeout</help> + <valueHelp> + <format>u32</format> + <description>Timeout in seconds (default: 300)</description> + </valueHelp> + </properties> + <defaultValue>300</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="nat44" owner="${vyos_conf_scripts_dir}/vpp_nat.py"> + <properties> + <help>NAT44</help> + <priority>330</priority> + </properties> + <children> + <node name="interface"> + <properties> + <help>NAT interface setting</help> + </properties> + <children> + <leafNode name="inside"> + <properties> + <help>NAT inside interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="outside"> + <properties> + <help>NAT outside interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="address-pool"> + <properties> + <help>NAT address pool</help> + </properties> + <children> + <node name="translation"> + <properties> + <help>NAT translation pool</help> + </properties> + <children> + #include <include/vpp/nat_address_range.xml.i> + #include <include/vpp/nat_interface.xml.i> + </children> + </node> + <node name="twice-nat"> + <properties> + <help>NAT twice-nat pool</help> + </properties> + <children> + #include <include/vpp/nat_address_range.xml.i> + #include <include/vpp/nat_interface.xml.i> + </children> + </node> + </children> + </node> + <node name="static"> + <properties> + <help>Static NAT rules</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + <valueHelp> + <format>u32</format> + <description>Number of NAT rule</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + <node name="external"> + <properties> + <help>NAT external parameters</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/port-number.xml.i> + </children> + </node> + <node name="local"> + <properties> + <help>NAT local parameters</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/port-number.xml.i> + </children> + </node> + <node name="options"> + <properties> + <help>NAT static mapping options</help> + </properties> + <children> + <leafNode name="twice-nat"> + <properties> + <help>Rewrite source IP addresses on packets sent from outside to inside</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="self-twice-nat"> + <properties> + <help>Rewrite source IP addresses on packets sent only from a local address to an external address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="out-to-in-only"> + <properties> + <help>Only apply rule for traffic from outside to inside interfaces</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="twice-nat-address"> + <properties> + <help>Force use of specific IP address from twice-nat address pool</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + #include <include/vpp/nat_protocol.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="exclude"> + <properties> + <help>Exclude packets matching these rules from NAT</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Rule number</help> + <valueHelp> + <format>u32</format> + <description>Number of rule</description> + </valueHelp> + </properties> + <children> + <leafNode name="local-address"> + <properties> + <help>IP address of the internal (local) device</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="local-port"> + <properties> + <help>Port number used by connection on internal device</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> + </properties> + </leafNode> + #include <include/vpp/nat_protocol.xml.i> + <leafNode name="external-interface"> + <properties> + <help>External interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + </leafNode> + #include <include/generic-description.xml.i> + </children> + </tagNode> + </children> + </node> + </children> + </node> + <node name="acl" owner="${vyos_conf_scripts_dir}/vpp_acl.py"> + <properties> + <help>Access Control Lists (ACLs)</help> + <priority>332</priority> + </properties> + <children> + <node name="ip"> + <properties> + <help>Access Control List by IPv4/IPv6</help> + </properties> + <children> + <tagNode name="tag-name"> + <properties> + <help>ACL tag name</help> + <valueHelp> + <format>txt</format> + <description>Tag name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + <tagNode name="rule"> + <properties> + <help>Rule number for Access control element (ACE)</help> + <valueHelp> + <format>u32</format> + <description>Number of ACE</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Rule number must be between 1 and 4294967295</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + <leafNode name="action"> + <properties> + <help>Rule action</help> + <completionHelp> + <list>permit deny permit-reflect</list> + </completionHelp> + <valueHelp> + <format>permit</format> + <description>Permit matching traffic</description> + </valueHelp> + <valueHelp> + <format>deny</format> + <description>Deny matching traffic</description> + </valueHelp> + <valueHelp> + <format>permit-reflect</format> + <description>Permit the matching outbound traffic and allow the reverse traffic</description> + </valueHelp> + <constraint> + <regex>(permit|deny|permit-reflect)</regex> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/vpp/acl_prefix.xml.i> + #include <include/vpp/acl_port_range.xml.i> + </children> + </node> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/vpp/acl_prefix.xml.i> + #include <include/vpp/acl_port_range.xml.i> + </children> + </node> + <leafNode name="protocol"> + <properties> + <help>Protocol</help> + <completionHelp> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + <defaultValue>all</defaultValue> + </leafNode> + <node name="tcp-flags"> + <properties> + <help>TCP flags</help> + </properties> + <children> + #include <include/vpp/acl_tcp_flags.xml.i> + <node name="not"> + <properties> + <help>Match flags not set</help> + </properties> + <children> + #include <include/vpp/acl_tcp_flags.xml.i> + </children> + </node> + </children> + </node> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Apply an ACL to an interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <children> + <node name="input"> + <properties> + <help>Input direction</help> + </properties> + <children> + #include <include/vpp/acl_common_interface_ip_rule.xml.i> + </children> + </node> + <node name="output"> + <properties> + <help>Output direction</help> + </properties> + <children> + #include <include/vpp/acl_common_interface_ip_rule.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <node name="macip"> + <properties> + <help>Access Control List by mac address</help> + </properties> + <children> + <tagNode name="tag-name"> + <properties> + <help>ACL tag name</help> + <valueHelp> + <format>txt</format> + <description>ACL name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + <tagNode name="rule"> + <properties> + <help>Rule number for Access control element (ACE)</help> + <valueHelp> + <format>u32</format> + <description>Number of ACE</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Rule number must be between 1 and 4294967295</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + <leafNode name="action"> + <properties> + <help>Rule action</help> + <completionHelp> + <list>permit deny</list> + </completionHelp> + <valueHelp> + <format>permit</format> + <description>Permit matching traffic</description> + </valueHelp> + <valueHelp> + <format>deny</format> + <description>Deny matching traffic</description> + </valueHelp> + <constraint> + <regex>(permit|deny)</regex> + </constraint> + </properties> + </leafNode> + #include <include/vpp/acl_prefix.xml.i> + <leafNode name="prefix"> + <properties> + <help>Source IP prefix</help> + </properties> + </leafNode> + <leafNode name="mac-address"> + <properties> + <help>Source MAC address</help> + <valueHelp> + <format>macaddr</format> + <description>MAC address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mac-mask"> + <properties> + <help>Source MAC mask (default ff:ff:ff:ff:ff:ff)</help> + <valueHelp> + <format>macaddr</format> + <description>MAC mask</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + <defaultValue>ff:ff:ff:ff:ff:ff</defaultValue> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Apply an ACL to an input interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <children> + <leafNode name="tag-name"> + <properties> + <help>ACL tag name</help> + <completionHelp> + <path>vpp acl macip tag-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + <tagNode name="kernel-interfaces" owner="${vyos_conf_scripts_dir}/vpp_kernel-interfaces.py"> + <properties> + <help>VPP kernel interface settings</help> + <priority>328</priority> + <valueHelp> + <format>vpptapN</format> + <description>Kernel interface name</description> + </valueHelp> + <valueHelp> + <format>vpptunN</format> + <description>Kernel interface name</description> + </valueHelp> + <constraint> + <regex>vpp(tun|tap)\d+</regex> + </constraint> + <constraintErrorMessage>Kernel interface must start with vpp and include tun or tap N</constraintErrorMessage> + </properties> + <children> + #include <include/interface/address-ipv4-ipv6.xml.i> + #include <include/generic-description.xml.i> + #include <include/interface/disable.xml.i> + #include <include/interface/mtu-68-16000.xml.i> + #include <include/vpp/vif.xml.i> + #include <include/vpp/iface_rx_mode.xml.i> + </children> + </tagNode> + </children> +</node> +</interfaceDefinition> diff --git a/op-mode-definitions/show_vpp_interfaces.xml.in b/op-mode-definitions/show_vpp_interfaces.xml.in new file mode 100644 index 000000000..75bb05240 --- /dev/null +++ b/op-mode-definitions/show_vpp_interfaces.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpp"> + <properties> + <help>Show VPP information</help> + </properties> + <children> + <node name="interfaces"> + <properties> + <help>Show VPP interface information</help> + </properties> + <children> + <node name="dataplane"> + <properties> + <help>Show VPP dataplane interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --dataplane</command> + </node> + <node name="kernel"> + <properties> + <help>Show VPP kernel interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --kernel</command> + </node> + <tagNode name="hardware"> + <properties> + <help>Show detailed statistics for specified VPP interface</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_vpp_interfaces.py</script> + </completionHelp> + </properties> + <standalone> + <help>Show VPP interfaces detailed statistics</help> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --hardware</command> + </standalone> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --hardware --intf-name="$5"</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_interfaces.py --summary</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show_vpp_nat44.xml.in b/op-mode-definitions/show_vpp_nat44.xml.in new file mode 100644 index 000000000..96d53fba9 --- /dev/null +++ b/op-mode-definitions/show_vpp_nat44.xml.in @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpp"> + <children> + <node name="nat44"> + <properties> + <help>Show VPP NAT44 information</help> + </properties> + <children> + <node name="static"> + <properties> + <help>Show VPP NAT44 static mapping</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_static</command> + </node> + <node name="sessions"> + <properties> + <help>Show VPP NAT44 sessions</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_sessions</command> + </node> + <node name="summary"> + <properties> + <help>Show VPP NAT44 summary</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_summary</command> + </node> + <node name="addresses"> + <properties> + <help>Show VPP NAT44 pool addresses</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_addresses</command> + </node> + <node name="interfaces"> + <properties> + <help>Show VPP NAT44 interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_vpp_nat44.py show_interfaces</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/vpp_acl.xml.in b/op-mode-definitions/vpp_acl.xml.in new file mode 100644 index 000000000..a901a537d --- /dev/null +++ b/op-mode-definitions/vpp_acl.xml.in @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpp"> + <children> + <node name="acl"> + <properties> + <help>Show VPP ACL information</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>Show VPP ACL by IPv4/IPv6</help> + </properties> + <children> + <tagNode name="tag-name"> + <properties> + <help>Show specified VPP ACL</help> + <completionHelp> + <path>vpp acl ip tag-name</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls --tag-name="$6"</command> + </tagNode> + <node name="interface"> + <properties> + <help>Show VPP ACL interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_interfaces</command> + </node> + </children> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_ip_acls</command> + </node> + <node name="macip"> + <properties> + <help>Show VPP ACL by macip</help> + </properties> + <children> + <tagNode name="tag-name"> + <properties> + <help>Show specified VPP ACL</help> + <completionHelp> + <path>vpp acl macip tag-name</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls --tag-name="$6"</command> + </tagNode> + <node name="interface"> + <properties> + <help>Show VPP ACL interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_interfaces</command> + </node> + </children> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_macip_acls</command> + </node> + </children> + <command>sudo ${vyos_op_scripts_dir}/vpp_acl.py show_all_acls</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/vpp_nat_cgnat.xml.in b/op-mode-definitions/vpp_nat_cgnat.xml.in new file mode 100644 index 000000000..8af10711f --- /dev/null +++ b/op-mode-definitions/vpp_nat_cgnat.xml.in @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpp"> + <children> + <node name="nat"> + <properties> + <help>Show VPP NAT information</help> + </properties> + <children> + <node name="cgnat"> + <properties> + <help>Show VPP CGNAT information</help> + </properties> + <children> + <node name="mappings"> + <properties> + <help>Show VPP CGNAT mappings</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_mappings</command> + </node> + <node name="sessions"> + <properties> + <help>Show VPP CGNAT sessions</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_sessions</command> + </node> + <node name="interfaces"> + <properties> + <help>Show VPP CGNAT interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py show_interfaces</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="clear"> + <children> + <node name="vpp"> + <properties> + <help>Terminate VPP NAT sessions</help> + </properties> + <children> + <node name="cgnat"> + <properties> + <help>Terminate VPP CGNAT sessions</help> + </properties> + <children> + <tagNode name="inside-address"> + <properties> + <help>Inside IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + <tagNode name="port"> + <properties> + <help>Port</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <children> + <tagNode name="external-address"> + <properties> + <help>External IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + <tagNode name="port"> + <properties> + <help>Port</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vpp_nat_cgnat.py clear_session --address $5 --port $7 --ext-address $9 --ext-port ${11}</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/vpp/__init__.py b/python/vyos/vpp/__init__.py new file mode 100644 index 000000000..a320ac7d1 --- /dev/null +++ b/python/vyos/vpp/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from .control_vpp import VPPControl + +__all__ = ['VPPControl'] diff --git a/python/vyos/vpp/acl/__init__.py b/python/vyos/vpp/acl/__init__.py new file mode 100644 index 000000000..6bd6cdb23 --- /dev/null +++ b/python/vyos/vpp/acl/__init__.py @@ -0,0 +1,3 @@ +from .acl import Acl + +__all__ = ['Acl'] diff --git a/python/vyos/vpp/acl/acl.py b/python/vyos/vpp/acl/acl.py new file mode 100644 index 000000000..9da241f1c --- /dev/null +++ b/python/vyos/vpp/acl/acl.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +NO_ACL_INDEX = 0xFFFFFFFF + + +class Acl: + def __init__(self): + self.vpp = VPPControl() + + def get_acl_index_by_tag(self, tag): + """Get ACL index by tag name + https://github.com/FDio/vpp/blob/21c641f9356da5137760cdc799127064c8c1fd31/src/plugins/acl/acl.api + """ + for acl in self.vpp.api.acl_dump(acl_index=NO_ACL_INDEX): + if acl.tag == tag: + return acl.acl_index + return NO_ACL_INDEX + + def add_replace_acl(self, tag, rules): + """Add new ACL or replace existing one""" + self.vpp.api.acl_add_replace( + tag=tag, + acl_index=self.get_acl_index_by_tag(tag), + count=len(rules), + r=rules, + ) + + def delete_acl(self, tag): + """Delete existing ACL""" + self.vpp.api.acl_del(acl_index=self.get_acl_index_by_tag(tag)) + + def add_acl_interface(self, interface, input_tags, output_tags): + """Add or replace ACLs on interface""" + acls = [] + for tag in input_tags: + acl_index = self.get_acl_index_by_tag(tag) + acls.append(acl_index) + for tag in output_tags: + acl_index = self.get_acl_index_by_tag(tag) + acls.append(acl_index) + self.vpp.api.acl_interface_set_acl_list( + sw_if_index=self.vpp.get_sw_if_index(interface), + count=len(acls), + n_input=len(input_tags), + acls=acls, + ) + + def delete_acl_interface(self, interface): + """Delete ACLs from interface""" + self.vpp.api.acl_interface_set_acl_list( + sw_if_index=self.vpp.get_sw_if_index(interface), + count=0, + ) + + def get_macip_acl_index_by_tag(self, tag): + """Get macip ACL by tag name""" + for acl in self.vpp.api.macip_acl_dump(): + if acl.tag == tag: + return acl.acl_index + return NO_ACL_INDEX + + def add_replace_acl_macip(self, tag, rules): + """Add or replace existing macip ACL""" + self.vpp.api.macip_acl_add_replace( + tag=tag, + acl_index=self.get_macip_acl_index_by_tag(tag), + count=len(rules), + r=rules, + ) + + def delete_acl_macip(self, tag): + """Delete existing macip ACL""" + self.vpp.api.macip_acl_del(acl_index=self.get_macip_acl_index_by_tag(tag)) + + def add_acl_macip_interface(self, interface, tag): + """Add or replace macip ACLs on interface""" + self.vpp.api.macip_acl_interface_add_del( + sw_if_index=self.vpp.get_sw_if_index(interface), + acl_index=self.get_macip_acl_index_by_tag(tag), + is_add=True, + ) + + def delete_acl_macip_interface(self, interface): + """Delete macip ACLs from interface""" + self.vpp.api.macip_acl_interface_add_del( + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=False, + ) diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py new file mode 100644 index 000000000..d826dedbb --- /dev/null +++ b/python/vyos/vpp/config_deps.py @@ -0,0 +1,76 @@ +# +# Copyright (C) 2023-2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +def deps_xconnect_dict(conf) -> dict[str, list[str]]: + """Get a dict of all xconnect interface members: + + keys: members + + values: xconnect interfaces + + Args: + conf (config): VyOS config object + + Returns: + dict[str, list[str]]: dict of members + """ + xconn_members_dict: dict[str, list[str]] = {} + config = conf.get_config_dict( + ['vpp', 'interfaces', 'xconnect'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + for xconn_name, xconn_config in config.items(): + for member_name in xconn_config.get('member', {}).get('interface', []): + xconn_ifaces_list = xconn_members_dict.get(xconn_name, []) + xconn_ifaces_list.append(xconn_name) + xconn_members_dict.update({member_name: xconn_ifaces_list}) + + return xconn_members_dict + + +def deps_bridge_dict(conf) -> dict[str, list[str]]: + """Get a dict of all bridge interface members: + + keys: members + + values: bridge interfaces + + Args: + conf (config): VyOS config object + + Returns: + dict[str, list[str]]: dict of members + """ + bridge_members_dict: dict[str, list[str]] = {} + config = conf.get_config_dict( + ['vpp', 'interfaces', 'bridge'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + for bridge_name, bridge_config in config.items(): + for member_name in bridge_config.get('member', {}).get('interface', []): + bridge_ifaces_list = bridge_members_dict.get(bridge_name, []) + bridge_ifaces_list.append(bridge_name) + bridge_members_dict.update({member_name: bridge_ifaces_list}) + + return bridge_members_dict diff --git a/python/vyos/vpp/config_filter.py b/python/vyos/vpp/config_filter.py new file mode 100644 index 000000000..4ab9ffda2 --- /dev/null +++ b/python/vyos/vpp/config_filter.py @@ -0,0 +1,58 @@ +# +# Copyright (C) 2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.config import Config + + +def iface_filter_eth(config: Config, iface: str) -> None: + """Filter out unsupported config nodes from Ethernet interface config + + Args: + config (Config): config object + iface (str): Ethernet interface name to filter + """ + allowed_nodes: list[str] = [ + 'address', + 'description', + 'dhcp-options', + 'dhcpv6-options', + 'disable', + 'eapol', + 'hw-id', + 'ip', + 'ipv6', + 'mtu', + 'redirect', + 'vif', + 'vif-s', + 'vrf', + ] + + # get list of config nides in a session configuration + iface_nodes = config._session_config.list_nodes(['interfaces', 'ethernet', iface]) + + # clean cached session config + if False in config._dict_cache: + del config._dict_cache[False] + + # remove unsupported config nodes + for cfg_node in iface_nodes: + if cfg_node not in allowed_nodes: + config._session_config.delete(['interfaces', 'ethernet', iface, cfg_node]) + print( + f'WARNING: {cfg_node} option in {iface} settings is not supported by VPP interfaces. It will be ignored.' + ) diff --git a/python/vyos/vpp/config_resource_checks/__init__.py b/python/vyos/vpp/config_resource_checks/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/__init__.py diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py new file mode 100644 index 000000000..ee0bace2d --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/cpu.py @@ -0,0 +1,76 @@ +# Used for validating estimated CPU/physical cores use +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.utils.cpu import get_available_cpus, get_core_count + +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map + + +# Get default value for reserved cpu cores +reserved_cpus = default_resource_map.get('reserved_cpu_cores') + + +def available_cores_count(cpu_settings: dict) -> int: + core_count = get_core_count() + + if cpu_settings.get('main_core'): + core_count -= 1 + + skip_cores = int(cpu_settings.get('skip_cores', 0)) + # The default settings assume that + # at least 2 CPU cores should remain reserved for system use + # (only in case of current runtime is not smoke test) + if skip_cores < reserved_cpus: + core_count -= reserved_cpus + else: + core_count -= skip_cores + + return core_count + + +def available_cores_list(skip_cores: int) -> list: + # Available cores are all CPU cores without first N skipped cores that will not be used + # Get all available physical cores - use set to filter out unique values + cpu_cores = set(map(lambda el: el['cpu'], get_available_cpus())) + cpu_cores = list(cpu_cores) + + return cpu_cores[skip_cores:] + + +def worker_cores_list(iface: str, worker_ranges: list) -> list: + all_core_numbers = [] + for worker_range in worker_ranges: + core_numbers = worker_range.split('-') + + if int(core_numbers[0]) > int(core_numbers[-1]): + raise ValueError( + f'Range for "{iface} workers {worker_range}" is not correct' + ) + + all_core_numbers.extend(range(int(core_numbers[0]), int(core_numbers[-1]) + 1)) + + # Check for duplicates + duplicates = set( + [str(x) for n, x in enumerate(all_core_numbers) if x in all_core_numbers[:n]] + ) + if duplicates: + raise ValueError( + f'Some workers in "{iface} workers" are duplicated: #{",".join(list(duplicates))}' + ) + + return all_core_numbers diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py new file mode 100644 index 000000000..f8ccf7d47 --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -0,0 +1,153 @@ +# Used for memory consumption calculations +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import re + +from vyos.utils.process import cmd +from vyos.vpp.utils import ( + human_memory_to_bytes, + human_page_memory_to_bytes, +) +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map + + +def get_hugepages_info() -> dict: + """ + Returns the information about HugePages for default hugepage size + retrieved from /proc/meminfo + """ + info = {} + with open('/proc/meminfo', 'r') as meminfo: + for line in meminfo: + if line.startswith('Huge'): + key, value, *_ = line.strip().split() + info[key.rstrip(':')] = int(value) + return info + + +def get_total_hugepages_memory() -> int: + """ + Returns the total amount of hugepage memory (in bytes) + """ + info = get_hugepages_info() + hugepages_total = info.get('HugePages_Total') + hugepage_size = info.get('Hugepagesize') * 1024 + + return hugepage_size * hugepages_total + + +def get_total_hugepages_count() -> int: + """ + Returns the total count of hugepages + """ + info = get_hugepages_info() + return info.get('HugePages_Total') + + +def get_numa_count(): + """ + Run `numactl --hardware` and parse the 'available:' line. + """ + out = cmd('numactl --hardware') + # e.g. "available: 2 nodes (0-1)" + m = re.search(r'available:\s*(\d+)\s+nodes', out) + return int(m.group(1)) if m else 0 + + +def buffer_size(settings: dict) -> int: + numa_count = get_numa_count() + buffers_per_numa = int( + settings.get('buffers', {}).get( + 'buffers_per_numa', default_resource_map.get('buffers_per_numa') + ) + ) + data_size = int( + settings.get('buffers', {}).get( + 'data_size', default_resource_map.get('data_size') + ) + ) + buffers_memory = buffers_per_numa * data_size * numa_count + return buffers_memory + + +def main_heap_page_size(settings: dict) -> int: + heap_page_size = settings.get('memory', {}).get( + 'main_heap_page_size', default_resource_map.get('main_heap_page_size') + ) + return human_page_memory_to_bytes(heap_page_size) + + +def memory_main_heap(settings: dict) -> int: + heap_size = settings.get('memory', {}).get( + 'main_heap_size', default_resource_map.get('main_heap_size') + ) + return human_memory_to_bytes(heap_size) + + +def ipv6_heap_size(settings: dict) -> int: + heap_size = settings.get('ipv6', {}).get( + 'heap_size', default_resource_map.get('ipv6_heap_size') + ) + return human_memory_to_bytes(heap_size) + + +def total_heap_size(heap_size: int, heap_page_size: int) -> int: + return (heap_size + heap_page_size - 1) & ~(heap_page_size - 1) + + +def statseg_size(settings: dict) -> int: + statseg_memory = settings.get('statseg', {}).get( + 'size', default_resource_map.get('statseg_heap_size') + ) + return human_memory_to_bytes(statseg_memory) + + +def statseg_page_size(settings: dict) -> int: + page_size = settings.get('statseg', {}).get('page_size', 'default') + return human_page_memory_to_bytes(page_size) + + +def total_statseg_size(_statseg_size: int, _statseg_page: int) -> int: + return (_statseg_size + _statseg_page - 1) & ~(_statseg_page - 1) + + +def total_memory_required(settings: dict) -> int: + mem_required = 0 + + mem_stats = { + 'memory_buffers': buffer_size(settings), + 'netlink_buffer_size': int( + settings.get('lcp', {}).get( + 'rx_buffer_size', default_resource_map.get('netlink_rx_buffer_size') + ) + ), + 'heap_size': total_heap_size( + heap_size=memory_main_heap(settings), + heap_page_size=main_heap_page_size(settings), + ), + 'statseg_size': total_statseg_size( + _statseg_size=statseg_size(settings), + _statseg_page=statseg_page_size(settings), + ), + 'ipv6_heap_size': ipv6_heap_size(settings), + } + + for stat in mem_stats: + mem_required += mem_stats[stat] + + return mem_required diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py new file mode 100644 index 000000000..828683456 --- /dev/null +++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py @@ -0,0 +1,43 @@ +# Default values for resource consumption checks +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +default_resource_map = { + # Default amount of buffers per NUMA (populated CPU socket) + 'buffers_per_numa': 16384, + # Default size of buffer (in bytes) + 'data_size': 2048, + # Default hugepage size for VPP + 'hugepage_size': '2M', + # Default amount of memory allocated for VPP exclusive usage + 'main_heap_size': '3G', + # Default main heap page size + 'main_heap_page_size': '2M', + # Default size of buffers transferred via netlink + 'netlink_rx_buffer_size': 212992, + # Default amount of memory allocated for VPP stats segment usage + 'statseg_heap_size': '96M', + # Minimal amount of memory required to start VPP + 'min_memory': '8G', + # Minimal number of physical CPU cores required to start VPP + 'min_cpus': 4, + # Reserve at least 2 physical cores + 'reserved_cpu_cores': 2, + # Default heap size for IPv6 + 'ipv6_heap_size': '32M', +} diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py new file mode 100644 index 000000000..0e21a5a8a --- /dev/null +++ b/python/vyos/vpp/config_verify.py @@ -0,0 +1,407 @@ +# Used for verifying configuration vpp interfaces +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import psutil + +from vyos import ConfigError +from vyos.base import Warning +from vyos.utils.cpu import get_core_count as total_core_count + +from vyos.vpp.control_host import get_eth_driver +from vyos.vpp.config_resource_checks import cpu as cpu_checks, memory as mem_checks +from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map +from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory + + +def verify_vpp_remove_kernel_interface(config: dict): + """Common verify for removed kernel-interfaces. + Verify that removed kernel interface are not used in 'vpp kernel-interfaces'. + + Example: + delete vpp interfaces gre|vxlan <tag>X kernel-interface vpp-tunX + set vpp kernel-interface vpp-tunX + """ + if ( + 'remove' in config + and 'kernel_interface_removed' in config + and 'vpp_kernel_interfaces' in config + ): + removed_interfaces = config['kernel_interface_removed'] + used_interfaces = config['vpp_kernel_interfaces'] + + for interface in removed_interfaces: + if interface in used_interfaces: + raise ConfigError( + f'"{interface}" is still in use within "vpp kernel-interfaces". ' + 'Please remove it before proceeding.' + ) + + +def verify_vpp_change_kernel_interface(config: dict): + """Common verify for changed kernel-interface + + Example: + set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunX' + commit + set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunY' + commit + + check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + """ + kernel_interface_removed = config.get('kernel_interface_removed', []) + vpp_kernel_interfaces = config.get('vpp_kernel_interfaces', {}) + + for interface in kernel_interface_removed: + if interface in vpp_kernel_interfaces: + raise ConfigError( + f'interface "{interface}" is still in use within "vpp kernel-interfaces". ' + f'Please remove it "vpp kernel-interface {interface}" before proceeding.' + ) + + +def verify_vpp_exists_kernel_interface(config: dict): + """Verify is a kernel-interface already created by another VPP LCP pair + + Example: + set vpp interfaces vxlan vxlan10 kernel-interface vpp-tun10' + commit + set vpp interfaces vxlan vxlan20 kernel-interface vpp-tun10' + commit + """ + kernel_interface = config.get('kernel_interface', '') + vpp_interface = config.get('ifname', '') + candidate_kernel_interfaces = config.get('candidate_kernel_interfaces', []) + + for candidate_kernel_iface in candidate_kernel_interfaces: + if ( + vpp_interface != candidate_kernel_iface[0] + and kernel_interface == candidate_kernel_iface[1] + ): + raise ConfigError( + f'Kernel interface "{kernel_interface}" is already configured for {candidate_kernel_iface[0]}. ' + 'Duplicates are not allowed.' + ) + + +def verify_vpp_remove_xconnect_interface(config: dict): + if not config.get('remove'): + return + for xconn_member, xconn_iface in config.get('xconn_members').items(): + if xconn_member == config.get('ifname'): + raise ConfigError( + f'interface "{xconn_member}" is still in use within "vpp interfaces xconnect". ' + f'Please remove it from "vpp interface xconnect {xconn_iface}" before proceeding.' + ) + + +def verify_vpp_tunnel_source_address(config: dict): + from vyos.utils.network import is_intf_addr_assigned + + address = config.get('source_address') + for iface in config.get('vpp_ether_vif_ifaces', []): + if is_intf_addr_assigned(iface, address): + return True + + raise ConfigError( + f'Source address "{address}" is not assigned on any Ethernet or VIF interface!' + ) + + +def verify_dev_driver(iface_name: str, driver_type: str) -> bool: + # Lists of drivers compatible with DPDK and XDP + drivers_dpdk: list[str] = [ + 'atlantic', + 'bnx2x', + 'e1000', + 'ena', + 'gve', + 'hv_netvsc', + 'i40e', + 'ice', + 'igc', + 'ixgbe', + 'liquidio', + 'mlx4_core', + 'mlx5_core', + 'qede', + 'sfc', + 'tap', + 'tun', + 'virtio_net', + 'vmxnet3', + ] + + drivers_xdp: list[str] = [ + 'atlantic', + 'ena', + 'gve', + 'hv_netvsc', + 'i40e', + 'ice', + 'igb', + 'igc', + 'ixgbe', + 'mlx4_core', + 'mlx5_core', + 'qede', + 'sfc', + 'tap', + 'tun', + 'virtio_net', + 'vmxnet3', + ] + + driver: str = get_eth_driver(iface_name) + + if driver_type == 'dpdk': + if driver in drivers_dpdk: + return True + elif driver_type == 'xdp': + if driver in drivers_xdp: + return True + else: + raise ConfigError(f'"Driver type {driver_type} is wrong') + + return False + + +def verify_vpp_minimum_cpus(): + """ + Verify that the host system has enough physical CPU cores + Current minimal requirement is 4 + """ + min_cpus = default_resource_map.get('min_cpus') + if total_core_count() < min_cpus: + raise ConfigError( + 'This system does not meet minimal requirements for VPP. ' + f'Minimum {min_cpus} CPU cores are required.' + ) + + +def verify_vpp_minimum_memory(): + """ + Verify that the host system has enough RAM + Calculate by retrieving the amount of physical memory + And the minimal requirement (currently 8 GB). Round before comparing - + To avoid situations like when a machine nominally has 8192 MB (8 giga/gibibytes) + But the OS sees only 7.75 GB, creating a fail condition for this check + """ + min_mem = default_resource_map.get('min_memory') + total_memory = round(psutil.virtual_memory().total / (1024**3)) + min_memory = round(human_memory_to_bytes(min_mem) / (1024**3)) + + if total_memory < min_memory: + raise ConfigError( + 'This system does not meet minimal requirements for VPP. ' + f'Minimum {min_memory} GB of RAM are required.' + ) + + +def verify_vpp_memory(config: dict): + main_heap_size = mem_checks.memory_main_heap(config['settings']) + main_heap_page_size = mem_checks.main_heap_page_size(config['settings']) + + if main_heap_size < 51 << 20: + raise ConfigError('The main heap size must be greater than or equal to 51M') + + readable_heap_page = bytes_to_human_memory(main_heap_page_size, 'K') + + if main_heap_page_size > main_heap_size: + raise ConfigError( + f'The main heap size must be greater than or equal to page-size ({readable_heap_page})' + ) + + available_memory = mem_checks.get_total_hugepages_memory() + memory_required = mem_checks.total_memory_required(config['settings']) + + if main_heap_size > available_memory: + available_memory_in_mb = bytes_to_human_memory(available_memory, 'M') + raise ConfigError( + f'"memory main-heap-size" must not be greater than hugepages memory. Reduce to {available_memory_in_mb} or less' + ) + + memory_required = round(memory_required / 1024**3, 1) + available_memory = round(available_memory / 1024**3, 1) + + # Allow 10% error margin + allowed_margin = memory_required * 0.1 + + # Compare HugePage memory with required memory for VPP + if memory_required > available_memory + allowed_margin: + raise ConfigError( + f'Not enough free hugepage memory to start VPP: ' + f'available: {available_memory} GB, required: {memory_required} GB. ' + 'Please add kernel memory options for HugePages ' + '"set system option kernel memory hugepage-size ..." and reboot' + ) + + +def verify_vpp_settings_cpu_skip_cores(skip_cores: int): + cpu_cores = total_core_count() + + # The number of skipped cores must not be greater than + # available CPU cores in the system - 1 for main thread + if skip_cores > (cpu_cores - 1): + raise ConfigError( + f'The system does not have enough available CPUs to skip ' + f'(reduce "cpu skip-cores" to {cpu_cores} or less)' + ) + + +def verify_vpp_settings_cpu_and_corelist_workers(settings: dict): + """ + `set vpp settings cpu workers` and `set vpp settings cpu corelist-workers` + are mutually exclusive! + """ + if ( + 'corelist_workers' in settings or 'workers' in settings + ) and 'main_core' not in settings: + raise ConfigError('"cpu main-core" is required but not set!') + + if 'corelist_workers' in settings and 'workers' in settings: + raise ConfigError( + '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!' + ) + + +def verify_vpp_cpu_main_core(cpu_settings: dict) -> None: + """Check that the main core is available""" + skip_cores = int(cpu_settings.get('skip_cores', 0)) + available_cores = cpu_checks.available_cores_list(skip_cores) + main_core = int(cpu_settings['main_core']) + + if main_core not in available_cores: + raise ConfigError( + 'Cannot set main core for VPP process: ' + f'CPU#{main_core} is not available.' + ) + + +def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int: + """ + Verify that the system has enough available CPU cores + to run a given amount of worker processes (1 worker/core) + """ + workers = int(cpu_settings.get('workers', 0)) + available_cores = cpu_checks.available_cores_count(cpu_settings) + + if workers > available_cores: + raise ConfigError( + f'Not enough free CPU cores for {workers} VPP workers ' + f'(reduce to {available_cores} or less)' + ) + + return workers + + +def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int: + """ + Verify that the CPU cores provided to the config are free and can be used by VPP + """ + workers = cpu_settings.get('corelist_workers') + main_core = int(cpu_settings.get('main_core')) + skip_cores = int(cpu_settings.get('skip_cores', 0)) + available_cores = cpu_checks.available_cores_list(skip_cores) + try: + all_core_nums = cpu_checks.worker_cores_list( + iface='cpu corelist', worker_ranges=workers + ) + except ValueError as e: + raise ConfigError(str(e)) + + error_msg = 'Cannot set VPP "cpu corelist-workers"' + + if main_core in all_core_nums: + raise ConfigError( + f'CPU#{main_core} is set as main core and should not ' + 'be included to the corelist-workers' + ) + + invalid_cores = [str(el) for el in all_core_nums if el not in available_cores] + if invalid_cores: + raise ConfigError( + f'{error_msg}: CPU# {",".join(invalid_cores)} are not available.' + ) + + if len(all_core_nums) > cpu_checks.available_cores_count(cpu_settings): + raise ConfigError(f'{error_msg}: Not enough free CPUs in the system.') + + return len(all_core_nums) + + +def verify_vpp_nat44_workers(workers: int, nat44_workers: list): + if workers < 1: + raise ConfigError( + '"nat44 workers" requires cpu workers or corelist-workers to be set!' + ) + try: + nat_workers = cpu_checks.worker_cores_list( + iface='nat44', worker_ranges=nat44_workers + ) + except ValueError as e: + raise ConfigError(str(e)) + + invalid_workers = [str(el) for el in nat_workers if el not in range(workers)] + if invalid_workers: + raise ConfigError( + f'Cannot set VPP "nat44 workers": worker(s) #{",".join(invalid_workers)} not available. ' + f'Available worker ids: {",".join(map(str, range(workers)))}' + ) + + +def verify_vpp_statseg_size(settings: dict): + statseg_size = mem_checks.statseg_size(settings) + + if 'size' in settings.get('statseg'): + if statseg_size < 1 << 20: + raise ConfigError('The statseg size must be greater than or equal to 1M') + + if 'page_size' in settings['statseg']: + statseg_page_size = mem_checks.statseg_page_size(settings) + if statseg_page_size > statseg_size: + readable_statseg_page = bytes_to_human_memory(statseg_page_size, 'K') + raise ConfigError( + f'The statseg size must be greater than or equal to page-size ({readable_statseg_page})' + ) + + +def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: int): + """ + Verify that VPP has enough workers to run the given amount of RX/TX queues + 1 queue per 1 worker is assumed as default + """ + + if num_queues > workers: + raise ConfigError( + f'The number of {qtype} queues cannot be greater than the number of configured VPP workers: ' + f'workers: {workers}, queues: {num_queues}' + ) + + +def verify_vpp_host_resources(config: dict): + max_map_count = int(config['settings']['host_resources']['max_map_count']) + + # Get HugePages total count + hugepages = mem_checks.get_total_hugepages_count() + + if max_map_count < 2 * hugepages: + Warning( + 'The max-map-count should be greater than or equal to (2 * HugePages_Total) ' + 'or VPP could work not properly. Please set up ' + f'"vpp settings host-resources max-map-count" to {2 * hugepages} or higher' + ) diff --git a/python/vyos/vpp/configdb.py b/python/vyos/vpp/configdb.py new file mode 100644 index 000000000..1830937ad --- /dev/null +++ b/python/vyos/vpp/configdb.py @@ -0,0 +1,206 @@ +# +# Copyright (C) 2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from json import loads as json_loads, dumps as json_dumps +from pathlib import Path +from typing import Any + +STORAGE_LOCATION = '/run/vpp' + + +class JSONStorage: + def __init__(self, name: str = '') -> None: + """Initiate a file storage + + Args: + name (str, optional): Unique storage name. Defaults to '' (generate a name). + + Raises: + err: In case a file for storage cannot be created + """ + # If a name is not provided, this is a temporary one-time storage + # this use case is strange, but let's allow this + if not name: + self.__temporary = True + self.__cache: dict[Any, Any] = {} + self.__locked = False + return + self.__temporary = False + + self.__storage = Path(f'{STORAGE_LOCATION}/{name}.json') + self.__lock_file = Path(f'{STORAGE_LOCATION}/{name}.lock') + + # prepare a folder + storage_dir = Path(STORAGE_LOCATION) + if not storage_dir.exists(): + storage_dir.mkdir(parents=True) + + # initialize lock status + self.__locked = False + if self.__storage_locked(): + raise FileExistsError(f'Cannot open locked storage: {self.__storage}') + self.__lock_file.touch() + + if not self.__storage.exists(): + try: + self.__storage.touch() + except Exception as err: + print(f'Unable to initiate storage: {err}') + raise err + # prepare an empty cache + self.__cache: dict[Any, Any] = {} + else: + # load a cache from file + self.__cache = self.__load_file() + + def __del__(self) -> None: + """Dump data to persistent storage and unlock it""" + if self.__temporary: + return + # dump a cache to storage + if self.__cache: + self.__dump_file() + # or remove a file + else: + self.__storage.unlink() + # unlock a storage + self.__lock_file.unlink() + + def __check_types(self, data: Any) -> None: + """Check if all the data have supported types + + Args: + data (Any): object to validate + + Raises: + TypeError: If a data type is not supported + """ + if isinstance(data, str | int | float | bool | None): + return + if isinstance(data, list): + for item in data: + self.__check_types(item) + return + if isinstance(data, dict): + for item in data.values(): + self.__check_types(item) + return + raise TypeError(f'Object type "{type(data)}" is not allowed') + + def __load_file(self) -> dict[Any, Any]: + """Read a file to a dictionary + + Returns: + dict[Any, Any]: loaded dict object + """ + data: bytes = self.__storage.read_bytes() + return json_loads(data) + + def __dump_file(self) -> None: + """Dump cache to a file""" + data: str = json_dumps(self.__cache) + self.__storage.write_text(data) + + def __lock(self) -> None: + """Lock storage + + Raises: + FileExistsError: Raised if a storage is already locked + """ + if self.__locked: + raise FileExistsError(f'Access is already locked: {self.__storage}') + self.__locked = True + + def __unlock(self) -> None: + """Unlock storage + + Raises: + FileNotFoundError: Raised if a storage is already unlocked + """ + if not self.__locked: + raise FileNotFoundError(f'Access is already unlocked: {self.__storage}') + self.__locked = False + + def __storage_locked(self) -> bool: + """Check if a storage is locked + + Returns: + bool: Lock status + """ + if self.__lock_file.exists(): + return True + return False + + def delete(self, key: Any = None) -> None: + """Delete data from a storage or a full storage + + Raises: + FileExistsError: Raised if a storage is locked + """ + if self.__locked: + raise FileExistsError( + f'Storage locked and delete operation cannot be performed: {self.__storage}' + ) + if key: + if key not in self.__cache: + raise ValueError( + f'Object {key} does not exist in storage {self.__storage}' + ) + del self.__cache[key] + else: + self.__cache = {} + + def write(self, key: Any, value: Any) -> None: + # Check types first + self.__check_types(key) + self.__check_types(value) + # check lock status + if self.__locked: + raise FileExistsError( + f'Storage is locked and cannot be written: {self.__storage}' + ) + # write a data to a cache + self.__lock() + self.__cache[key] = value + self.__unlock() + + def read(self, key: Any, default: Any = None) -> Any: + """Read data from a storage + + Args: + key (Any): key name + default (Any, optional): Value to return if a key does not exist. Defaults to None. + + Raises: + FileExistsError: Raised if a storage is locked + + Returns: + Any: Value to return + """ + # Check types first + self.__check_types(key) + # check lock status + if self.__locked: + raise FileExistsError( + f'Storage is locked and it is not safe to read: {self.__storage}' + ) + # read a data from cache + self.__lock() + data: Any | None = self.__cache.get(key, default) + self.__unlock() + + return data diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py new file mode 100644 index 000000000..1ac004fbb --- /dev/null +++ b/python/vyos/vpp/control_host.py @@ -0,0 +1,375 @@ +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +from pathlib import Path +from re import fullmatch as re_fullmatch +from subprocess import run +from time import sleep + +from pyroute2 import IPRoute + +from vyos.vpp.utils import EthtoolGDrvinfo + + +def pci_rescan(pci_addr: str = '') -> None: + """Rescan PCI device by removing it and rescan PCI bus + + If PCI address is not defined - just rescan PCI bus + + Args: + address (str, optional): PCI address of device. Defaults to ''. + """ + device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove') + if pci_addr: + if device_file.exists(): + device_file.write_text('1') + # wait 10 seconds max until device will be removed + attempts = 100 + while device_file.exists() and attempts: + attempts -= 1 + sleep(0.1) + if device_file.exists(): + raise TimeoutError( + f'Timeout was reached for removing PCI device {pci_addr}' + ) + else: + raise FileNotFoundError(f'PCI device {pci_addr} does not exist') + rescan_file = Path('/sys/bus/pci/rescan') + rescan_file.write_text('1') + if pci_addr: + # wait 10 seconds max until device will be installed + attempts = 100 + while not device_file.exists() and attempts: + attempts -= 1 + sleep(0.1) + if not device_file.exists(): + raise TimeoutError( + f'Timeout was reached for installing PCI device {pci_addr}' + ) + + +def unbind_driver(bus_id: str, device_id: str) -> bool: + """Unbind a driver from a device + + Args: + bus_id (str): bus ID (pci, vmbus, etc.) + device_id (str): device id on the bus (PCI address, VMBus UUID) + + Returns: + bool: True if a driver has been unbound, False otherwise + """ + device_resolved: str = ( + Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix() + ) + if not Path(f'{device_resolved}/driver').exists(): + return False + + Path(f'{device_resolved}/driver/unbind').write_text(device_id) + return True + + +def probe_driver(bus_id: str, device_id: str) -> None: + """Probe driver for a device on a bus + + Args: + bus_id (str): bus ID (pci, vmbus, etc.) + device_id (str): device id on the bus (PCI address, VMBus UUID) + """ + Path(f'/sys/bus/{bus_id}/drivers_probe').write_text(device_id) + + +def load_kernel_module(module_name: str) -> None: + """Load a kernel module + + Args: + module_name (str): module name + """ + # check if a module already loaded + if Path(f'/sys/module/{module_name}').exists(): + return + + # execute modprobe with the specified module name + run(['/usr/sbin/modprobe', '-q', module_name], check=True) + + +def override_driver(bus_id: str, device_id: str, driver_name: str = '') -> None: + """Override a driver for a device + + Args: + bus_id (str): bus ID (pci, vmbus, etc.) + device_id (str): device id on the bus (PCI address, VMBus UUID) + driver_name (str, optional): Kernel module (driver) name. Defaults to '' - clear an override. + + Raises: + FileNotFoundError: A device does not support driver override + ChildProcessError: Failed to override a driver + """ + device_resolved: str = ( + Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix() + ) + # check if a device supports driver override + if not Path(f'{device_resolved}/driver_override').exists(): + raise FileNotFoundError(f'{device_resolved} does not support driver override') + + if driver_name: + load_kernel_module(driver_name) + + unbind_driver(bus_id, device_id) + + # vfio-pci needs special approach + if driver_name == 'vfio-pci': + vendor: str = Path(f'{device_resolved}/vendor').read_text() + device: str = Path(f'{device_resolved}/device').read_text() + Path('/sys/module/vfio_pci/drivers/pci:vfio-pci/new_id').write_text( + f'{vendor} {device}' + ) + + # override a driver + Path(f'{device_resolved}/driver_override').write_text(f'{driver_name}\n') + + # probe a driver + probe_driver(bus_id, device_id) + + # check the result + if not Path(f'{device_resolved}/driver').exists(): + raise ChildProcessError( + f'Failed to override a driver to {driver_name} for {bus_id}, {device_id}' + ) + + +def get_bus_name(iface: str) -> str: + """Get bus name + Works for PCI, VMbus, maybe something else. + Does not work for Virtio and other virtual devices + (however, it does not seem we need this for such kind of devices). + + Args: + iface (str): interface name + + Returns: + str: bus name + """ + device_resolved: Path = Path(f'/sys/class/net/{iface}/device').resolve() + + # Iterate upwards until a `bus` directory is found + current_path: Path = device_resolved + while True: + # Check if a bus info is available + subsystem_path = Path(f'{current_path}/subsystem') + if subsystem_path.is_symlink(): + # Read the link to determine the bus type + bus_path = subsystem_path.resolve() + # Check if the parent directory is a 'bus' directory in '/sys/bus/' + if bus_path.parent.name == 'bus': + # Return only the last name of the path, e.g., 'pci' + return bus_path.name + + # Move up one directory level + current_path = current_path.parent + if current_path == Path('/sys'): + break # Stop if we reach the root of /sys without finding a bus type + + return '' # Return None if no bus type was found + + +def get_eth_name(dev_id: str) -> str: + """Find Ethernet interface name by PCI address or UUID + + Args: + dev_id (str): PCI address or UUID + + Raises: + FileNotFoundError: no Ethernet interface was found + + Returns: + str: Ethernet interface name + """ + # find all PCI devices with eth* names + net_devs: dict[str, str] = {} + net_devs_dir = Path('/sys/class/net') + regex_filter = r'^/sys/devices/pci[\w/:\.]+/(?P<pci_addr>\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?P<iface_name>eth\d+)$' + for dir in net_devs_dir.iterdir(): + # PCI devices + real_dir: str = dir.resolve().as_posix() + re_obj = re_fullmatch(regex_filter, real_dir) + if re_obj: + iface_name: str = re_obj.group('iface_name') + iface_addr: str = re_obj.group('pci_addr') + net_devs.update({iface_addr: iface_name}) + # UUID devices + else: + try: + bus_type: str = get_bus_name(dir.name) + iface_addr = EthtoolGDrvinfo(dir.name).bus_info_expand(bus_type) + net_devs.update({iface_addr: dir.name}) + except FileNotFoundError: + pass + + # match to provided PCI address or UUID and return a name if found + if dev_id in net_devs: + return net_devs[dev_id] + # raise error if device was not found + raise FileNotFoundError( + f'A device with ID {dev_id} not found in ethernet interfaces' + ) + + +def get_dev_id(iface: str) -> str: + """Get device ID by its interface name + + Args: + iface (str): interface name + + Raises: + FileNotFoundError: no Ethernet interface was found + + Returns: + str: device ID (PCI address or UUID) + """ + try: + # Try to get details via ethtool first + ethtool_info = EthtoolGDrvinfo(iface) + # For devices represented by UUID we need to expand them + # to their full representation + if ethtool_info.driver == 'hv_netvsc': + return ethtool_info.bus_info_expand('vmbus') + return ethtool_info.bus_info + except Exception: + # raise error if a device ID was not found + raise FileNotFoundError(f'Cannot find device ID for interface {iface}') + + +def get_eth_driver(iface: str) -> str: + """Find kernel module used for Ethernet interface + + Args: + iface (str): Ethernet interface name + + Raises: + FileNotFoundError: no Ethernet interface was found + + Returns: + str: kernel module name + """ + iface_driver: str = '' + driver_dir = Path(f'/sys/class/net/{iface}/device/driver/module') + if not driver_dir.exists(): + # raise error if device was not found + raise FileNotFoundError(f'PCI device {iface} not found in ethernet interfaces') + + iface_driver: str = driver_dir.resolve().name + return iface_driver + + +def unsafe_noiommu_mode(status: bool) -> None: + """Control unsafe_noiommu_mode parameter of vfio module + + Args: + status (bool): Target status + + Raises: + ChildProcessError: Raised if failed to set unsafe_noiommu_mode + """ + param_path = Path('/sys/module/vfio/parameters/enable_unsafe_noiommu_mode') + current_status: str = param_path.read_text().strip() + target_status: str = 'Y' if status else 'N' + if current_status != target_status: + param_path.write_text(target_status) + if param_path.read_text().strip() != target_status: + raise ChildProcessError('Failed to set unsafe_noiommu_mode') + + +def rename_iface(name_old: str, name_new: str) -> None: + """Rename interface + + Args: + name_old (str): old name + name_new (str): new name + """ + run(['ip', 'link', 'set', name_old, 'down']) + rename_cmd: list[str] = ['ip', 'link', 'set', name_old, 'name', name_new] + run(rename_cmd) + + +def set_promisc(iface_name: str, operation: str) -> None: + """Set promisc mode for interface + + Args: + iface_name (str): name of an interface + operation (str): operation (on, off) + """ + run(['ip', 'link', 'set', iface_name, 'promisc', operation]) + + +def set_mtu(iface_name: str, mtu: int) -> None: + """Set MTU for interface + + Args: + iface_name (str): name of an interface + mtu (int): MTU + """ + run(['ip', 'link', 'set', iface_name, 'mtu', str(mtu)]) + + +def get_eth_mac(iface_name: str) -> str: + """Get MAC address of an interface + + Args: + iface_name (str): name of an interface + + Raises: + FileNotFoundError: interface was not found + + Returns: + str: MAC address + """ + dev_addr_path = Path(f'/sys/class/net/{iface_name}/address') + if dev_addr_path.exists(): + return dev_addr_path.read_text().strip() + else: + # raise error if device was not found + raise FileNotFoundError(f'Interface {iface_name} not found') + + +def xdp_remove(iface_name: str) -> None: + """Remove XDP BPF program from an interfce + + Args: + iface_name (str): name of an interface + """ + run(['ip', 'link', 'set', iface_name, 'xdp', 'off']) + + +def set_status(iface_name: str, status: str) -> None: + """Set interface status + + Args: + iface_name (str): name of an interface + status (str): status - "up" or "down" + """ + run(['ip', 'link', 'set', iface_name, status]) + + +def flush_ip(iface_name: str) -> None: + """Flush IP addresses from an interface + + Args: + iface_name (str): name of an interface + """ + iproute = IPRoute() + iproute.flush_addr(label=iface_name) diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py new file mode 100644 index 000000000..5959b68b5 --- /dev/null +++ b/python/vyos/vpp/control_vpp.py @@ -0,0 +1,488 @@ +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from collections.abc import Callable +from functools import wraps +from re import search as re_search, MULTILINE as re_M +from systemd import journal +from time import sleep +from typing import TypeVar, ParamSpec, Literal + +from vpp_papi import VPPApiClient +from vpp_papi import VPPIOError, VPPValueError + +# define types for static type checkers +AnyType = TypeVar('AnyType') +AnyParam = ParamSpec('AnyParam') + + +class VPPControl: + """Control VPP network stack""" + + class _Decorators: + """Decorators for VPPControl""" + + @classmethod + def api_call( + cls, decorated_func: Callable[AnyParam, AnyType] + ) -> Callable[AnyParam, AnyType]: + """Check if API is connected before API call + + Args: + decorated_func: function to decorate + + Raises: + VPPIOError: Connection to API is not established + """ + + @wraps(decorated_func) + def api_safe_wrapper( + cls, *args: AnyParam.args, **kwargs: AnyParam.kwargs + ) -> AnyType: + if not cls.connected: + raise VPPIOError(2, 'VPP API is not connected') + return decorated_func(cls, *args, **kwargs) + + return api_safe_wrapper + + @classmethod + def check_retval( + cls, decorated_func: Callable[AnyParam, AnyType] + ) -> Callable[AnyParam, AnyType]: + """Check retval from API response + + Args: + decorated_func: function to decorate + + Raises: + VPPValueError: raised when retval is not 0 + """ + + @wraps(decorated_func) + def check_retval_wrapper( + cls, *args: AnyParam.args, **kwargs: AnyParam.kwargs + ) -> AnyType: + return_value = decorated_func(cls, *args, **kwargs) + if not return_value.retval == 0: + raise VPPValueError(f'VPP API call failed: {return_value.retval}') + return return_value + + return check_retval_wrapper + + def __init__(self, attempts: int = 5, interval: int = 1000) -> None: + """Create VPP API connection + + Args: + attempts (int, optional): attempts to connect. Defaults to 5. + interval (int, optional): interval between attempts in ms. Defaults to 1000. + + Raises: + VPPIOError: Connection to API cannot be established + """ + self.__vpp_api_client = VPPApiClient() + # connect with interval + while attempts: + try: + attempts -= 1 + self.__vpp_api_client.connect('vpp-vyos') + break + except (ConnectionRefusedError, FileNotFoundError) as err: + error_message = f'VPP API connection timeout: {err}' + journal.send(error_message, priority=journal.LOG_ERR) + sleep(interval / 1000) + # raise exception if connection was not successful in the end + if not self.__vpp_api_client.transport.connected: + raise VPPIOError(2, 'Cannot connect to VPP API') + + def __del__(self) -> None: + """Disconnect from VPP API (destructor)""" + self.disconnect() + + def disconnect(self) -> None: + """Disconnect from VPP API""" + if self.__vpp_api_client.transport.connected: + self.__vpp_api_client.disconnect() + + @_Decorators.check_retval + @_Decorators.api_call + def cli_cmd(self, command: str): + """Send raw CLI command + + Args: + command (str): command to send + + Returns: + vpp_papi.vpp_serializer.cli_inband_reply: CLI reply class + """ + return self.__vpp_api_client.api.cli_inband(cmd=command) + + @_Decorators.api_call + def get_mac(self, ifname: str) -> str: + """Find MAC address by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + str: MAC address + """ + for iface in self.__vpp_api_client.api.sw_interface_dump(): + if iface.interface_name == ifname: + return iface.l2_address.mac_string + return '' + + @_Decorators.api_call + def get_sw_if_index(self, ifname: str) -> int | None: + """Find interface index by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + int | None: Interface index or None (if was not fount) + """ + for iface in self.__vpp_api_client.api.sw_interface_dump(): + if iface.interface_name == ifname: + return iface.sw_if_index + return None + + @_Decorators.api_call + def get_interface_name(self, index: int) -> str | None: + """Find interface name by interface index in VPP + + Args: + index (int): interface index inside VPP + + Returns: + str | None: Interface name or None (if was not found) + """ + for iface in self.__vpp_api_client.api.sw_interface_dump(): + if iface.sw_if_index == index: + return iface.interface_name + return None + + @_Decorators.check_retval + @_Decorators.api_call + def lcp_pair_add( + self, + iface_name_vpp: str, + iface_name_kernel: str, + iface_type: Literal['tun', 'tap', ''] = '', + ) -> None: + """Create LCP interface pair between VPP and kernel + + Args: + iface_name_vpp (str): interface name in VPP + iface_name_kernel (str): interface name in kernel + iface_type (Literal['tun', 'tap', ''], optional): Use explicit interface type in kernel. Defaults to ''. + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + if iface_index: + api_call_args: dict[str, bool | int | str] = { + 'is_add': True, + 'sw_if_index': iface_index, + 'host_if_name': iface_name_kernel, + } + if iface_type: + iface_type_resolve = {'tun': 1, 'tap': 0} + api_call_args['host_if_type'] = iface_type_resolve[iface_type] + return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2(**api_call_args) + + @_Decorators.check_retval + @_Decorators.api_call + def lcp_pair_del(self, iface_name_vpp: str, iface_name_kernel: str) -> None: + """Delete LCP interface pair between VPP and kernel + + Args: + iface_name_vpp (str): interface name in VPP + iface_name_kernel (str): interface name in kernel + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + if iface_index: + return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2( + is_add=False, sw_if_index=iface_index, host_if_name=iface_name_kernel + ) + + @_Decorators.api_call + def lcp_pair_find( + self, + kernel_name: str = '', + vpp_index_hw: int | None = None, + vpp_index_kernel: int | None = None, + vpp_name_hw: str = '', + vpp_name_kernel: str = '', + ) -> dict[str, str | int] | None: + """Find LCP pair details + + Args: + kernel_name (str, optional): Interface name in the kernel. Defaults to ''. + vpp_index_hw (int | None, optional): Interface index in VPP (hardware). Defaults to None. + vpp_index_kernel (int | None, optional): Interface index in VPP (kernel). Defaults to None. + vpp_name_hw (str, optional): Interface name in VPP (hardware). Defaults to ''. + vpp_name_kernel (str, optional): Interface name in VPP (to kernel). Defaults to ''. + + Returns: + dict[str, str | int] | None: LCP pair details + """ + filter_dict = {} + for filter_name, filter_value in locals().items(): + if filter_value: + filter_dict[filter_name] = filter_value + + # Get list of pairs + lcp_pairs = self.lcp_pairs_list() + + # Check each pair + for pair in lcp_pairs: + pair_found = False + # For each item provided in function arguments + for filter_name, filter_value in filter_dict.items(): + # Stop if filter value is not as in a current pair + if filter_name in pair and pair[filter_name] != filter_value: + pair_found = False + break + # Set flag to True and check the next filter value + pair_found = True + + if pair_found: + return pair + + return None + + @_Decorators.api_call + def lcp_pairs_list(self) -> list[dict[str, str | int]]: + """List all LCP pairs + + Returns: + list[dict[str, str | int]]: LCP pairs details + """ + lcp_pairs_details = [] + + lcp_pairs = self.__vpp_api_client.api.lcp_itf_pair_get()[1] + vpp_ifaces = self.__vpp_api_client.api.sw_interface_dump() + for pair in lcp_pairs: + pair_details = { + 'kernel_name': pair.host_if_name, + 'vpp_index_hw': pair.phy_sw_if_index, + 'vpp_index_kernel': pair.host_sw_if_index, + } + for vpp_iface in vpp_ifaces: + if vpp_iface.sw_if_index == pair_details['vpp_index_hw']: + pair_details['vpp_name_hw'] = vpp_iface.interface_name + if vpp_iface.sw_if_index == pair_details['vpp_index_kernel']: + pair_details['vpp_name_kernel'] = vpp_iface.interface_name + + lcp_pairs_details.append(pair_details) + + return lcp_pairs_details + + @_Decorators.check_retval + @_Decorators.api_call + def lcp_resync(self) -> None: + """Resynchronize objects between kernel and VPP via Netlink + + This clears all routes in VPP configured by LCP and re-creates them + based on the current state of the kernel. + """ + return self.__vpp_api_client.api.lcp_nl_resync() + + @_Decorators.check_retval + @_Decorators.api_call + def iface_rxmode(self, iface_name: str, rx_mode: str) -> None: + """Set interface rx-mode in VPP + + Args: + iface_name (str): interface name in VPP + rx_mode (str): mode (polling, interrupt, adaptive) + """ + modes_dict: dict[str, int] = {'polling': 1, 'interrupt': 2, 'adaptive': 3} + if rx_mode not in modes_dict: + raise VPPValueError(f'Mode {rx_mode} is not known') + iface_index = self.get_sw_if_index(iface_name) + return self.__vpp_api_client.api.sw_interface_set_rx_mode( + sw_if_index=iface_index, mode=modes_dict[rx_mode] + ) + + @_Decorators.api_call + def get_pci_addr(self, ifname: str) -> str: + """Find PCI address of interface by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + str: PCI address + """ + hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply + + regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$' + re_obj = re_search(regex_filter, hw_info, re_M) + + # return empty string if no interface or no PCI info was found + if not hw_info or not re_obj: + return '' + + address = re_obj.groupdict().get('address', '') + + # we need to modify address to match kernel style + # for example: 0000:06:14.00 -> 0000:06:14.0 + address_chunks: list[str] = address.split('.') + address_normalized: str = f'{address_chunks[0]}.{int(address_chunks[1])}' + + return address_normalized + + @_Decorators.check_retval + @_Decorators.api_call + def xdp_iface_create( + self, + host_if: str, + name: str, + rxq_num: int = 0, + rxq_size: int = 0, + txq_size: int = 0, + mode: Literal['auto', 'copy', 'zero-copy'] = 'auto', + flags: Literal['no_syscall_lock', ''] = '', + ) -> None: + """Create XDP interface + + Args: + host_if (str): name of an interface in kernel + name (str): name of an interface in VPP + rxq_num (int, optional): Number of receive queues to connect to. Defaults to 0 (all). + rxq_size (int, optional): Size of receive queue. Defaults to 0. + txq_size (int, optional): Size of tranceive queue. Defaults to 0. + mode (Literal['auto', 'copy', 'zero-copy', optional): Zero-copy mode. Defaults to 'auto'. + flags (Literal['no_syscall_lock', ''], optional): Syscall lock mode. Defaults to ''. + """ + api_call_args: dict[str, int | str] = { + 'host_if': host_if, + 'name': name, + 'rxq_num': rxq_num, + 'rxq_size': rxq_size, + 'txq_size': txq_size, + } + if mode != 'auto': + mode_resolve: dict[str, int] = {'auto': 0, 'copy': 1, 'zero-copy': 2} + api_call_args['mode'] = mode_resolve[mode] + if flags == 'no_systcall_lock': + api_call_args['flags'] = 1 + return self.__vpp_api_client.api.af_xdp_create_v3(**api_call_args) + + @_Decorators.check_retval + @_Decorators.api_call + def xdp_iface_delete(self, iface_name_vpp: str) -> None: + """Delete XDP interface + + Args: + iface_name_vpp (str): Name of an interface in VPP + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + if iface_index: + api_call_args: dict[str, int] = {'sw_if_index': iface_index} + return self.__vpp_api_client.api.af_xdp_delete(**api_call_args) + + @_Decorators.check_retval + @_Decorators.api_call + def set_iface_mac(self, iface_name_vpp: str, mac_address: str) -> None: + """Set MAC address of an interface + + Args: + iface_name_vpp (str): Name of an interface in VPP + mac_address (str): MAC address + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + api_call_args: dict[str, str | int] = { + 'sw_if_index': iface_index, + 'mac_address': mac_address, + } + return self.__vpp_api_client.api.sw_interface_set_mac_address(**api_call_args) + + @_Decorators.check_retval + @_Decorators.api_call + def set_iface_mtu(self, iface_name_vpp: str, mtu: int) -> None: + """Set MTU for interface + + Args: + iface_name_vpp (str): Name of an interface in VPP + mtu (int): MTU + """ + iface_index = self.get_sw_if_index(iface_name_vpp) + api_call_args: dict[str, str | int] = {'sw_if_index': iface_index, 'mtu': mtu} + return self.__vpp_api_client.api.hw_interface_set_mtu(**api_call_args) + + @_Decorators.api_call + def get_sw_if_dev_type(self, ifname: str) -> int | None: + """Find interface device type by interface name in VPP + + Args: + ifname (str): interface name inside VPP + + Returns: + int | None: Interface device type or None (if was not fount) + """ + for iface in self.__vpp_api_client.api.sw_interface_dump(): + if iface.interface_name == ifname: + return iface.interface_dev_type + return None + + @_Decorators.api_call + def enable_disable_nat44_forwarding(self, enable: bool) -> None: + """Enable/disable NAT44 forwarding + + Args: + enable (bool): True if enable, False if disable + """ + self.__vpp_api_client.api.nat44_forwarding_enable_disable(enable=enable) + + @_Decorators.api_call + def set_nat44_session_limit(self, session_limit: int) -> None: + """Set NAT44 session limit + + Args: + session_limit (int): Maximum number of sessions per thread + """ + self.__vpp_api_client.api.nat44_set_session_limit( + session_limit=session_limit, + ) + + @_Decorators.api_call + def set_nat_workers(self, workers: int) -> None: + """Set NAT44 session limit + + Args: + workers (int): Bitmask of workers list + """ + self.__vpp_api_client.api.nat_set_workers(worker_mask=workers) + + @property + def connected(self) -> bool: + """Check if VPP API is connected + + Returns: + bool: True if connected, False if not + """ + return self.__vpp_api_client.transport.connected + + @property + @_Decorators.api_call + def api(self): + """Call API + + Returns: + Callable[AnyParam, AnyType]: API functions + """ + return self.__vpp_api_client.api diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py new file mode 100644 index 000000000..2048e07ca --- /dev/null +++ b/python/vyos/vpp/interface/__init__.py @@ -0,0 +1,42 @@ +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from .bond import BondInterface +from .bridge import BridgeInterface +from .ethernet import EthernetInterface +from .geneve import GeneveInterface +from .gre import GREInterface +from .interface import Interface +from .ipip import IPIPInterface +from .loopback import LoopbackInterface +from .vxlan import VXLANInterface +from .wireguard import WireguardInterface +from .xconnect import XconnectInterface + +__all__ = [ + 'BondInterface', + 'BridgeInterface', + 'EthernetInterface', + 'GeneveInterface', + 'GREInterface', + 'Interface', + 'IPIPInterface', + 'LoopbackInterface', + 'VXLANInterface', + 'WireguardInterface', + 'XconnectInterface', +] diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py new file mode 100644 index 000000000..6a96bce58 --- /dev/null +++ b/python/vyos/vpp/interface/bond.py @@ -0,0 +1,118 @@ +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp.control_host import set_promisc +from vyos.vpp.interface.interface import Interface + + +class BondInterface(Interface): + def __init__( + self, + ifname, + mode: str = '', + load_balance: int = 0, + mac: str = '', + kernel_interface: str = '', + state: str = 'up', + ): + super().__init__(ifname) + self.instance = int(ifname.removeprefix('bond')) + self.ifname = f'BondEthernet{self.instance}' + self.mode = mode + self.load_balance = load_balance + self.mac = mac + self.kernel_interface = kernel_interface + self.state = state + + def add(self): + """Create Bond interface + https://github.com/FDio/vpp/blob/stable/2306/src/vnet/bonding/bond.api + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0', mode=5) + a.add() + """ + # Create interface 'bondX' + create_args = { + 'id': self.instance, + 'mode': self.mode, + 'lb': self.load_balance, + } + if self.mac: + create_args.update({'use_custom_mac': True, 'mac_address': self.mac}) + self.vpp.api.bond_create2(**create_args) + if self.kernel_interface: + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + # Set interface state + self.set_state(self.state) + + def delete(self): + """Delete Bond interface + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0') + a.delete() + """ + bond_if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.bond_delete(sw_if_index=bond_if_index) + + def add_member(self, interface): + """Add member to Bond interface + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0') + a.add_member(interface='eth0') + """ + bond_if_index = self.vpp.get_sw_if_index(f'BondEthernet{self.instance}') + member_if_index = self.vpp.get_sw_if_index(interface) + member_if_type = self.vpp.get_sw_if_dev_type(interface) + self.vpp.api.bond_add_member( + bond_sw_if_index=bond_if_index, sw_if_index=member_if_index + ) + self.vpp.api.sw_interface_set_promisc( + sw_if_index=member_if_index, promisc_on=True + ) + if member_if_type == 'AF_XDP interface': + set_promisc(f'defunct_{interface}', 'on') + + def detach_member(self, interface): + """Detach member from Bond interface + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0') + a.detach_member(interface='eth0') + """ + member_if_index = self.vpp.get_sw_if_index(interface) + self.vpp.api.bond_detach_member(sw_if_index=member_if_index) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0', mode=5) + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import BondInterface + a = BondInterface(ifname='bond0', mode=5) + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py new file mode 100644 index 000000000..5ebe46d45 --- /dev/null +++ b/python/vyos/vpp/interface/bridge.py @@ -0,0 +1,128 @@ +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class BridgeInterface: + def __init__( + self, + ifname: str, + flood: bool = True, + forward: bool = True, + learn: bool = True, + uu_flood: bool = True, + arp_term: bool = False, + ): + self.ifname = ifname + self.interface_suffix = int(self.ifname.replace('br', '')) + self.flood = flood + self.forward = forward + self.learn = learn + self.uu_flood = uu_flood + self.arp_term = arp_term + self.vpp = VPPControl() + + def add(self): + """Create Bridge interface + https://github.com/FDio/vpp/blob/stable/2306/src/vnet/l2/l2.api + + Bridge-domain 0 is reserved for the default bridge-domain. + + Example: + from vyos.vpp.interface import BridgeInterface + a = BridgeInterface(ifname='br23') + a.add() + """ + self.vpp.api.bridge_domain_add_del_v2( + is_add=True, + bd_id=self.interface_suffix, + flood=self.flood, + forward=self.forward, + learn=self.learn, + uu_flood=self.uu_flood, + arp_term=self.arp_term, + ) + + def delete(self): + """Delete Bridge interface + + Bridge-members must be detached before deleting the bridge interface. + + Example: + from vyos.vpp.interface import BridgeInterface + a = BridgeInterface(ifname='br23') + a.delete() + """ + self.vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix) + + def add_member(self, member: str | int, port_type: int = 0): + """Add member to Bridge interface + + Attaches a VPP interface to the Bridge interface specified by `interface_suffix`. + The `member` parameter can be either the name (str) or the index (int) of the network + VPP interface to be added as a member to the bridge. + + Args: + member (str or int): The name or index of the VPP network interface + to be added as a member to the bridge. + port_type: 0 - Normal port, 1 - BVI port + + Example: + from vyos.vpp.interface import BridgeInterface + a = BridgeInterface(ifname='br23') + a.add_member(member='eth0') + """ + bridge_index = self.interface_suffix + # If the 'member' is an Integer or digit, assume it's an interface index + if isinstance(member, int): + member_if_index = member + elif member.isdigit(): + member_if_index = int(member) + else: + member_if_index = self.vpp.get_sw_if_index(member) + + return self.vpp.api.sw_interface_set_l2_bridge( + rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=port_type + ) + + def detach_member(self, member: str | int): + """Detach member from Bridge interface. + Bridge-domain 0 is reserved for the default bridge-domain. + The `member` parameter can be either the name (str) or the index (int) + of the network VPP interface + + Args: + member (str or int): The name or index of the VPP network interface + to be detached from the bridge. + + Example: + from vyos.vpp.interface import BridgeInterface + a = BridgeInterface(ifname='br23') + a.detach_member(member='eth0') + """ + # If the 'member' is an Integer or digit, assume it's an interface index + if isinstance(member, int): + member_if_index = member + elif member.isdigit(): + member_if_index = int(member) + else: + member_if_index = self.vpp.get_sw_if_index(member) + + return self.vpp.api.sw_interface_set_l2_bridge( + rx_sw_if_index=member_if_index, bd_id=0, port_type=0 + ) diff --git a/python/vyos/vpp/interface/ethernet.py b/python/vyos/vpp/interface/ethernet.py new file mode 100644 index 000000000..368d94c7f --- /dev/null +++ b/python/vyos/vpp/interface/ethernet.py @@ -0,0 +1,53 @@ +# VyOS implementation of VPP Ethernet interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class EthernetInterface: + """Interface Ethernet""" + + def __init__(self, ifname, kernel_interface: str = ''): + self.instance = int(ifname.removeprefix('eth')) + self.ifname = ifname + self.kernel_interface = kernel_interface + self.vpp = VPPControl() + + def add(self): + pass + + def delete(self): + pass + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import EthernetInterface + a = EthernetInterface(ifname='eth0') + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import EthernetInterface + a = EthernetInterface(ifname='eth0') + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py new file mode 100644 index 000000000..8260b1bfa --- /dev/null +++ b/python/vyos/vpp/interface/geneve.py @@ -0,0 +1,90 @@ +# VyOS implementation of Geneve interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +def show(): + """Show Geneve interface + Example: + from vyos.vpp.interface import geneve + geneve.show() + """ + vpp = VPPControl() + return vpp.api.geneve_tunnel_dump() + + +class GeneveInterface: + def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''): + self.instance = int(ifname.removeprefix('geneve')) + self.ifname = f'geneve_tunnel{self.instance}' + self.src_address = source_address + self.dst_address = remote + self.vni = vni + self.kernel_interface = kernel_interface + self.vpp = VPPControl() + + def add(self): + """Create Geneve interface + https://github.com/FDio/vpp/blob/stable/2306/src/plugins/geneve/geneve.api + + Example: + from vyos.vpp.interface import GeneveInterface + a = GeneveInterface(ifname='geneve25', source_address='192.0.2.1', remote='203.0.113.25', vni=25) + a.add() + """ + return self.vpp.api.geneve_add_del_tunnel2( + is_add=True, + local_address=self.src_address, + remote_address=self.dst_address, + vni=self.vni, + l3_mode=False, + ) + + def delete(self): + """Delete Geneve interface + Example: + from vyos.vpp.interface import GeneveInterface + a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25) + a.delete() + """ + return self.vpp.api.geneve_add_del_tunnel2( + is_add=False, + local_address=self.src_address, + remote_address=self.dst_address, + vni=self.vni, + l3_mode=False, + ) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import GeneveInterface + a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25) + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import GeneveInterface + a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25) + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py new file mode 100644 index 000000000..5bef280c6 --- /dev/null +++ b/python/vyos/vpp/interface/gre.py @@ -0,0 +1,140 @@ +# VyOS implementation of VPP GRE interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface + + +def show(): + """Show GRE interface + Example: + from vyos.vpp.interface import gre + gre.show() + """ + vpp = VPPControl() + return vpp.api.gre_tunnel_dump() + + +class GREInterface(Interface): + """ + Class representing a GRE (Generic Routing Encapsulation) interface. + + Attributes: + ifname (str): The interface name. + source_address (str): The source IP address for the GRE tunnel. + remote (str): The remote IP address for the GRE tunnel. + tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. + mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'. + kernel_interface (str): The associated kernel interface. Defaults to an empty string. + instance (int): The instance number derived from the interface name. + vpp (VPPControl): An instance of the VPPControl class for interacting with the VPP API. + """ + + # Mapping of tunnel types https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api#L25-L35 + TUNNEL_TYPE_MAP = { + 'l3': 0, + 'teb': 1, + 'erspan': 2, + } + + MODE_MAP = { + 'point-to-point': 0, + 'point-to-multipoint': 1, + } + + def __init__( + self, + ifname, + source_address, + remote, + tunnel_type: str = 'l3', + mode: str = 'point-to-point', + kernel_interface: str = '', + state: str = 'up', + ): + """ + Initialize a GREInterface instance. + + Args: + ifname (str): The interface name. + source_address (str): The source IP address for the GRE tunnel. + remote (str): The remote IP address for the GRE tunnel. + mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'. + tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'. + kernel_interface (str): The associated kernel interface. Defaults to an empty string. + state (str): The state of the interface. Defaults to 'up'. + """ + super().__init__(ifname) + self.instance = int(ifname.removeprefix('gre')) + self.ifname = ifname + self.src_address = source_address + self.dst_address = remote + self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type] + self.mode = self.MODE_MAP[mode] + self.kernel_interface = kernel_interface + self.initial_state = state + + def add(self): + """Create GRE interface + https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api + Example: + from vyos.vpp.interface import GREInterface + a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25', tunnel_type='l3') + a.add() + """ + self.vpp.api.gre_tunnel_add_del( + is_add=True, + tunnel={ + 'src': self.src_address, + 'dst': self.dst_address, + 'instance': self.instance, + 'mode': self.mode, + 'type': self.tunnel_type, + }, + ) + # Set interface state + self.set_state(self.initial_state) + + def delete(self): + """Delete GRE interface + Example: + from vyos.vpp.interface import GREInterface + a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25') + a.delete() + """ + return self.vpp.api.gre_tunnel_add_del( + is_add=False, tunnel={'src': self.src_address, 'dst': self.dst_address} + ) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import GREInterface + a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25') + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import GREInterface + a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25') + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py new file mode 100644 index 000000000..33dc779c2 --- /dev/null +++ b/python/vyos/vpp/interface/interface.py @@ -0,0 +1,49 @@ +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class Interface: + def __init__(self, ifname): + self.ifname = ifname + self.vpp = VPPControl() + + def set_state(self, state: str): + """Set interface state to UP or DOWN + Args: + state (str): The state of the interface. Options are 'up' and 'down'. + Example: + from vyos.vpp.interface import Interface + a = Interface(ifname='eth0') + a.set_state(state='up') + """ + if state not in ['up', 'down']: + raise ValueError(f"Invalid state: {state}") + state_flag = 1 if state == 'up' else 0 + if_index = self.vpp.get_sw_if_index(self.ifname) + self.vpp.api.sw_interface_set_flags(sw_if_index=if_index, flags=state_flag) + + def get_state(self): + """Get interface state + Example: + from vyos.vpp.interface import Interface + a = Interface(ifname='eth0') + a.get_state() + """ + if_index = self.vpp.get_sw_if_index(self.ifname) + return self.vpp.api.sw_interface_dump(sw_if_index=if_index)[0]['flags'] diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py new file mode 100644 index 000000000..4779a8292 --- /dev/null +++ b/python/vyos/vpp/interface/ipip.py @@ -0,0 +1,94 @@ +# VyOS implementation of VPP IPIP interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface + + +def show(): + """Show IPIP interface + Example: + from vyos.vpp.interface import ipip + ipip.show() + """ + vpp = VPPControl() + return vpp.api.ipip_tunnel_dump() + + +class IPIPInterface(Interface): + def __init__( + self, + ifname, + source_address, + remote, + kernel_interface: str = '', + state: str = 'up', + ): + super().__init__(ifname) + self.instance = int(ifname.removeprefix('ipip')) + self.ifname = ifname + self.src_address = source_address + self.dst_address = remote + self.kernel_interface = kernel_interface + self.initial_state = state + + def add(self): + """Create IPIP interface + https://github.com/FDio/vpp/blob/stable/2310/src/vnet/ipip/ipip.api + Example: + from vyos.vpp.interface import IPIPInterface + a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5') + a.add() + """ + self.vpp.api.ipip_add_tunnel( + tunnel={ + 'src': self.src_address, + 'dst': self.dst_address, + 'instance': self.instance, + }, + ) + # Set interface state + self.set_state(self.initial_state) + + def delete(self): + """Delete IPIP interface + Example: + from vyos.vpp.interface import IPIPInterface + a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5') + a.delete() + """ + ipip_if_index = self.vpp.get_sw_if_index(f'ipip{self.instance}') + return self.vpp.api.ipip_del_tunnel(sw_if_index=ipip_if_index) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import IPIPInterface + a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5') + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import IPIPInterface + a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5') + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py new file mode 100644 index 000000000..d417f627d --- /dev/null +++ b/python/vyos/vpp/interface/loopback.py @@ -0,0 +1,72 @@ +# VyOS implementation of VPP Loopback interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp.interface.interface import Interface + + +class LoopbackInterface(Interface): + """Interface Loopback""" + + def __init__(self, ifname, kernel_interface: str = '', state: str = 'up'): + super().__init__(ifname) + self.instance = int(ifname.removeprefix('lo')) + self.ifname = f'loop{self.instance}' + self.kernel_interface = kernel_interface + self.initial_state = state + + def add(self): + """Create Loopback interface + https://github.com/FDio/vpp/blob/stable/2306/src/vnet/interface.api + Example: + from vyos.vpp.interface import LoopbackInterface + a = LoopbackInterface(ifname='lo1') + a.add() + """ + self.vpp.api.create_loopback_instance( + is_specified=True, user_instance=self.instance + ) + # Set interface state + self.set_state(self.initial_state) + + def delete(self): + """Delete Loopback interface + Example: + from vyos.vpp.interface import LoopbackInterface + a = LoopbackInterface(ifname='lo1') + a.delete() + """ + loopback_if_index = self.vpp.get_sw_if_index(f'loop{self.instance}') + return self.vpp.api.delete_loopback(sw_if_index=loopback_if_index) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import LoopbackInterface + a = LoopbackInterface(ifname='lo1') + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import LoopbackInterface + a = LoopbackInterface(ifname='lo1') + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py new file mode 100644 index 000000000..eacc901dc --- /dev/null +++ b/python/vyos/vpp/interface/vxlan.py @@ -0,0 +1,106 @@ +# VyOS implementation of VPP VXLAN interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl +from vyos.vpp.interface.interface import Interface + + +def show(): + """Show VXLAN interface + Example: + from vyos.vpp.interface import vxlan + vxlan.show() + """ + vpp = VPPControl() + return vpp.api.vxlan_tunnel_dump() + + +class VXLANInterface(Interface): + """Interface VXLAN""" + + def __init__( + self, + ifname, + source_address, + remote, + vni, + kernel_interface: str = '', + state: str = 'up', + ): + super().__init__(ifname) + self.instance = int(ifname.removeprefix('vxlan')) + self.ifname = f'vxlan_tunnel{self.instance}' + self.src_address = source_address + self.dst_address = remote + self.vni = vni + self.kernel_interface = kernel_interface + self.initial_state = state + + def add(self): + """Create VXLAN interface + https://github.com/FDio/vpp/blob/stable/2306/src/plugins/vxlan/vxlan.api + + Example: + from vyos.vpp.interface import VXLANInterface + a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23) + a.add() + """ + self.vpp.api.vxlan_add_del_tunnel_v3( + is_add=True, + src_address=self.src_address, + dst_address=self.dst_address, + vni=self.vni, + instance=self.instance, + decap_next_index=1, + is_l3=False, + ) + # Set interface state + self.set_state(self.initial_state) + + def delete(self): + """Delete VXLAN interface + Example: + from vyos.vpp.interface import VXLANInterface + a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23) + a.delete() + """ + return self.vpp.api.vxlan_add_del_tunnel_v3( + is_add=False, + src_address=self.src_address, + dst_address=self.dst_address, + vni=self.vni, + is_l3=False, + ) + + def kernel_add(self): + """Add LCP pair + Example: + from vyos.vpp.interface import VXLANInterface + a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23, kernel_interface='vpptap10') + a.kernel_add() + """ + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface) + + def kernel_delete(self): + """Delete LCP pair + Example: + from vyos.vpp.interface import VXLANInterface + a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23) + a.kernel_delete() + """ + self.vpp.lcp_pair_del(self.ifname, self.kernel_interface) diff --git a/python/vyos/vpp/interface/wireguard.py b/python/vyos/vpp/interface/wireguard.py new file mode 100644 index 000000000..649ed5b05 --- /dev/null +++ b/python/vyos/vpp/interface/wireguard.py @@ -0,0 +1,55 @@ +# VyOS implementation of VPP Wireguard interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class WireguardInterface: + """Interface Wireguard""" + + def __init__(self, ifname, listen_port=51820, kernel_interface: str = ''): + self.instance = int(ifname.removeprefix('wg')) + self.ifname = ifname + self.listen_port = listen_port + self.kernel_interface = kernel_interface + self.vpp = VPPControl() + + def add(self): + """Create Wireguard interface + https://github.com/FDio/vpp/blob/stable/2306/src/plugins/wireguard/wireguard.api + Example: + from vyos.vpp.interface import WireguardInterface + a = WireguardInterface(ifname='wg5', listen_port=51820, generate_key=True) + a.add() + """ + self.vpp.api.wireguard_interface_create( + generate_key=True, + interface={'user_instance': self.instance, 'listen_port': self.listen_port}, + ) + if self.kernel_interface: + self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun') + + def delete(self): + """Delete Wireguard interface + Example: + from vyos.vpp.interface import WireguardInterface + a = WireguardInterface(ifname='wg5') + a.delete() + """ + wg_if_index = self.vpp.get_sw_if_index(f'wg{self.instance}') + return self.vpp.api.wireguard_interface_delete(sw_if_index=wg_if_index) diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py new file mode 100644 index 000000000..badece6e8 --- /dev/null +++ b/python/vyos/vpp/interface/xconnect.py @@ -0,0 +1,95 @@ +# VyOS implementation of VPP bridge interface +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl +from vyos.vpp.utils import iftunnel_transform + + +class XconnectInterface: + def __init__( + self, + ifname: str, + members: list = [], + state: str = 'up', + ): + self.ifname = ifname + self.members = members + self.vpp = VPPControl() + self.initial_state = state + + def add_l2_xconnect(self): + """Add l2 cross connect + Args: + members (list): The list of the xconnect members + Example: + from vyos.vpp.interface import XconnectInterface + a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0']) + a.add_l2_xconnect() + """ + interface_transform_filter = ('vxlan', 'gre') + first_member = self.members[0] + second_member = self.members[1] + # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX' + if first_member.startswith(interface_transform_filter): + first_member = iftunnel_transform(first_member) + if second_member.startswith(interface_transform_filter): + second_member = iftunnel_transform(second_member) + + member_first_if_index = self.vpp.get_sw_if_index(first_member) + member_second_if_index = self.vpp.get_sw_if_index(second_member) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_first_if_index, + tx_sw_if_index=member_second_if_index, + enable=True, + ) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_second_if_index, + tx_sw_if_index=member_first_if_index, + enable=True, + ) + + def del_l2_xconnect(self): + """Move l2 cross connect member to mode l3 (delte xconnect) + Args: + members (list): The list of the xconnect members + Example: + from vyos.vpp.interface import XconnectInterface + a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0']) + a.del_l2_xconnect() + """ + interface_transform_filter = ('vxlan', 'gre') + first_member = self.members[0] + second_member = self.members[1] + # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX' + if first_member.startswith(interface_transform_filter): + first_member = iftunnel_transform(first_member) + if second_member.startswith(interface_transform_filter): + second_member = iftunnel_transform(second_member) + + member_first_if_index = self.vpp.get_sw_if_index(first_member) + member_second_if_index = self.vpp.get_sw_if_index(second_member) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_first_if_index, + tx_sw_if_index=member_second_if_index, + enable=False, + ) + self.vpp.api.sw_interface_set_l2_xconnect( + rx_sw_if_index=member_second_if_index, + tx_sw_if_index=member_first_if_index, + enable=False, + ) diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py new file mode 100644 index 000000000..16685c9cc --- /dev/null +++ b/python/vyos/vpp/nat/__init__.py @@ -0,0 +1,4 @@ +from .nat44 import Nat44 +from .det44 import Det44 + +__all__ = ['Nat44', 'Det44'] diff --git a/python/vyos/vpp/nat/det44.py b/python/vyos/vpp/nat/det44.py new file mode 100644 index 000000000..1046d7cf9 --- /dev/null +++ b/python/vyos/vpp/nat/det44.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +class Det44: + def __init__(self): + self.vpp = VPPControl() + + def enable_det44_plugin(self): + """Enable DET44 plugin + Example: + from vyos.vpp.nat import Det44 + det44 = Det44() + det44.enable_det44_plugin() + https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/det44/det44.api + """ + self.vpp.api.det44_plugin_enable_disable(enable=True) + + def disable_det44_plugin(self): + """Disable DET44 plugin""" + self.vpp.api.det44_plugin_enable_disable(enable=False) + + def add_det44_interface_outside(self, interface_out): + """Add DET44 outside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_inside=False, + is_add=True, + ) + + def delete_det44_interface_outside(self, interface_out): + """Delete DET44 outside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_inside=False, + is_add=False, + ) + + def add_det44_interface_inside(self, interface_in): + """Add DET44 inside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_inside=True, + is_add=True, + ) + + def delete_det44_interface_inside(self, interface_in): + """Delete DET44 inside interface""" + self.vpp.api.det44_interface_add_del_feature( + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_inside=True, + is_add=False, + ) + + def add_det44_mapping(self, in_addr, in_plen, out_addr, out_plen): + """Add DET44 mapping""" + self.vpp.api.det44_add_del_map( + in_addr=in_addr, + in_plen=in_plen, + out_addr=out_addr, + out_plen=out_plen, + is_add=True, + ) + + def delete_det44_mapping(self, in_addr, in_plen, out_addr, out_plen): + """Delete DET44 mapping""" + self.vpp.api.det44_add_del_map( + in_addr=in_addr, + in_plen=in_plen, + out_addr=out_addr, + out_plen=out_plen, + is_add=False, + ) + + def set_det44_timeouts( + self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int + ): + """Set DET44 timeouts + Args: + tcp_established (int): TCP established timeout + tcp_transitory (int): TCP transitory timeout + udp (int): UDP timeout + icmp (int): ICMP timeout + """ + self.vpp.api.det44_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py new file mode 100644 index 000000000..6b02688aa --- /dev/null +++ b/python/vyos/vpp/nat/nat44.py @@ -0,0 +1,229 @@ +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos.vpp import VPPControl + + +# NAT44 flags +NAT_IS_NONE = 0x00 +NAT_IS_TWICE_NAT = 0x01 +NAT_IS_SELF_TWICE_NAT = 0x02 +NAT_IS_OUT2IN_ONLY = 0x04 +NAT_IS_ADDR_ONLY = 0x08 +NAT_IS_OUTSIDE = 0x10 +NAT_IS_INSIDE = 0x20 + +NO_INTERFACE = 0xFFFFFFFF + + +class Nat44: + def __init__(self): + self.vpp = VPPControl() + + def enable_nat44_ed(self): + """Enable NAT44 endpoint dependent plugin + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.enable_nat44_ed() + https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/nat44-ed/nat44_ed.api + """ + self.vpp.api.nat44_ed_plugin_enable_disable(enable=True) + + def disable_nat44_ed(self): + """Disable NAT44 endpoint dependent plugin""" + self.vpp.api.nat44_ed_plugin_enable_disable(enable=False) + + def enable_nat44_ei(self): + """Enable NAT44 endpoint independent plugin + Example: + from vyos.vpp.nat import Nat44 + nat44 = Nat44() + nat44.enable_nat44_ei() + """ + self.vpp.api.nat44_ei_plugin_enable_disable(enable=True) + + def add_nat44_interface_inside(self, interface_in): + """Add NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_add=True, + ) + + def delete_nat44_interface_inside(self, interface_in): + """Delete NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=NAT_IS_INSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_in), + is_add=False, + ) + + def add_nat44_interface_outside(self, interface_out): + """Add NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_add=True, + ) + + def delete_nat44_interface_outside(self, interface_out): + """Delete NAT44 interface""" + self.vpp.api.nat44_interface_add_del_feature( + flags=NAT_IS_OUTSIDE, + sw_if_index=self.vpp.get_sw_if_index(interface_out), + is_add=False, + ) + + def add_nat44_address_range(self, addresses, twice_nat): + """Add NAT44 address range""" + if '-' not in addresses: + first_ip_address = last_ip_address = addresses + else: + first_ip_address, last_ip_address = addresses.split('-') + self.vpp.api.nat44_add_del_address_range( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + first_ip_address=first_ip_address, + last_ip_address=last_ip_address, + is_add=True, + ) + + def delete_nat44_address_range(self, addresses, twice_nat): + """Delete NAT44 address range""" + if '-' not in addresses: + first_ip_address = last_ip_address = addresses + else: + first_ip_address, last_ip_address = addresses.split('-') + self.vpp.api.nat44_add_del_address_range( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + first_ip_address=first_ip_address, + last_ip_address=last_ip_address, + is_add=False, + ) + + def add_nat44_interface_address(self, interface, twice_nat): + """Add NAT44 interface address""" + self.vpp.api.nat44_add_del_interface_addr( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=True, + ) + + def delete_nat44_interface_address(self, interface, twice_nat): + """Delete NAT44 interface address""" + self.vpp.api.nat44_add_del_interface_addr( + flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE, + sw_if_index=self.vpp.get_sw_if_index(interface), + is_add=False, + ) + + def add_nat44_static_mapping( + self, + local_ip, + external_ip, + local_port, + external_port, + protocol, + twice_nat, + self_twice_nat, + out2in, + pool_ip, + ): + """Add NAT44 static mapping""" + flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE + flags |= NAT_IS_TWICE_NAT if twice_nat else 0 + flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0 + flags |= NAT_IS_OUT2IN_ONLY if out2in else 0 + self.vpp.api.nat44_add_del_static_mapping_v2( + local_ip_address=local_ip, + external_ip_address=external_ip, + protocol=protocol, + local_port=local_port, + external_port=external_port, + match_pool=True if pool_ip else False, + pool_ip_address=pool_ip if pool_ip else '', + flags=flags, + is_add=True, + ) + + def delete_nat44_static_mapping( + self, + local_ip, + external_ip, + local_port, + external_port, + protocol, + twice_nat, + self_twice_nat, + out2in, + pool_ip, + ): + """Delete NAT44 static mapping""" + flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE + flags |= NAT_IS_TWICE_NAT if twice_nat else 0 + flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0 + flags |= NAT_IS_OUT2IN_ONLY if out2in else 0 + self.vpp.api.nat44_add_del_static_mapping_v2( + local_ip_address=local_ip, + external_ip_address=external_ip, + protocol=protocol, + local_port=local_port, + external_port=external_port, + match_pool=True if pool_ip else False, + pool_ip_address=pool_ip if pool_ip else '', + flags=flags, + is_add=False, + ) + + def add_nat44_identity_mapping(self, ip_address, protocol, port, interface): + """Add NAT44 identity mapping""" + self.vpp.api.nat44_add_del_identity_mapping( + ip_address=ip_address, + protocol=protocol, + port=port, + sw_if_index=( + self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE + ), + flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE, + is_add=True, + ) + + def delete_nat44_identity_mapping(self, ip_address, protocol, port, interface): + """Delete NAT44 identity mapping""" + self.vpp.api.nat44_add_del_identity_mapping( + ip_address=ip_address, + protocol=protocol, + port=port, + sw_if_index=( + self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE + ), + flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE, + is_add=False, + ) + + def set_nat_timeouts(self, icmp, udp, tcp_established, tcp_transitory): + """Set NAT timeouts""" + self.vpp.api.nat_set_timeouts( + icmp=icmp, + udp=udp, + tcp_established=tcp_established, + tcp_transitory=tcp_transitory, + ) + + def enable_ipfix(self): + """Enable NAT44 IPFIX logging""" + self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True) diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py new file mode 100644 index 000000000..fe08a98b8 --- /dev/null +++ b/python/vyos/vpp/utils.py @@ -0,0 +1,412 @@ +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import ctypes +import os +import socket +from fcntl import ioctl +from pathlib import Path +from struct import pack + + +mem_shift = {'K': 10, 'k': 10, 'M': 20, 'm': 20, 'G': 30, 'g': 30} + + +def iftunnel_transform(iface: str) -> str: + """Transform interface name from `xxxNN` to `xxx_tunnelNN` + + Args: + iface (str): original interface name + + Raises: + ValueError: Raised if an interface name does not start with a alpha and ends with decimal digit + + Returns: + str: Transformed interface name + """ + # Check format + if not iface[0].isascii() or not iface[-1].isdecimal(): + raise ValueError(f'Wrong interface name format: {iface}') + # Transform + iface_type: str = iface.rstrip('0123456789') + iface_num: str = iface.removeprefix(iface_type) + # Return transformed + return f'{iface_type}_tunnel{iface_num}' + + +def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]: + """List of all VPP interfaces (CLI names) + + Args: + config_instance (VyOS Config): VyOS Config instance + mode (str, optional): `candidate` or `running`. Defaults to 'candidate'. + + Returns: + list[str]: list of interfaces + """ + + effective_mode: bool = True if mode == 'running' else False + + # Read a config + config = config_instance.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + effective=effective_mode, + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + vpp_ifaces: list[str] = [] + + # Get a list of Ethernet interfaces + for iface in config.get('settings', {}).get('interface', {}).keys(): + vpp_ifaces.append(iface) + + # Get a list of VPP interfaces + for iface_type in config.get('interfaces', {}).keys(): + for iface in config.get('interfaces', {}).get(iface_type, {}).keys(): + vpp_ifaces.append(iface) + + return vpp_ifaces + + +def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]: + """List of all VPP Ethernet interfaces with VIFs + + Args: + config_instance (VyOS Config): VyOS Config instance + + Returns: + list[str]: list of interfaces + """ + from vyos.configdict import get_interface_dict + + # Read a config + config = config_instance.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + ifaces: list[str] = [] + + # Get a list of Ethernet interfaces + for iface in config.get('settings', {}).get('interface', {}).keys(): + ifaces.append(iface) + + # Add Ethernet interfaces with VIFs + for iface in ifaces: + _, iface_config = get_interface_dict( + config_instance, ['interfaces', 'ethernet'], ifname=iface + ) + ifaces.extend([f'{iface}.{vif}' for vif in iface_config.get('vif', {})]) + ifaces.extend([f'{iface}.{vif_s}' for vif_s in iface_config.get('vif_s', {})]) + + return ifaces + + +def vpp_ifaces_list(vpp_api) -> list[dict]: + """List interfaces in VPP + + Args: + vpp_api (_type_): VPP API object + + Returns: + list[dict]: list of dictionaries with interfaces + """ + ifaces_list: list[dict] = [] + sw_ifaces_dump = vpp_api.sw_interface_dump() + while sw_ifaces_dump: + iface_details = sw_ifaces_dump.pop() + ifaces_list.append(iface_details._asdict()) + + return ifaces_list + + +def vpp_ip_addresses_by_index(vpp_api, index: str) -> list[str]: + """List of IP addresses for interface by its index in VPP + + Args: + vpp_api (_type_): VPP API object + index (str): interface index in vpp + + Returns: + list[str]: list of IP addresses + """ + ip_addresses_list: list[dict] = [] + ip_address_dump = vpp_api.ip_address_dump(sw_if_index=index) + while ip_address_dump: + ip_address_details = ip_address_dump.pop() + ip_addresses_list.append(str(ip_address_details._asdict().get('prefix'))) + return ip_addresses_list + + +def vpp_ifaces_stats( + iface_name: str = '', +) -> dict[str, dict[str, int | dict[str, int]]]: + from re import compile as re_compile + from vpp_papi import vpp_stats + + def total_value(val_list: vpp_stats.SimpleList) -> int | dict[str, int]: + """Helper for aggregation stats from multiple workers + + Args: + val_list (vpp_stats.SimpleList): list of stats for all workers + + Returns: + int | dict[str, int]: Summary stats + """ + # if all items are int return their sum + if all(isinstance(value, int) for value in val_list): + return sum(val_list) + # if all items are tuple + if all(isinstance(value, tuple) for value in val_list): + combined_stats = {} + # process individual workers + for worker_stats in val_list: + packets, octets = worker_stats + sum_packets = combined_stats.get('packets', 0) + packets + sum_octets = combined_stats.get('octets', 0) + octets + combined_stats = {'packets': sum_packets, 'bytes': sum_octets} + return combined_stats + + # items are something unknown, just return what we received + return val_list + + stats = vpp_stats.VPPStats() + + ifaces_stats: dict[str, dict[str, int | dict[str, int]]] = {} + + # prepare parser for stats output + regex_parser = re_compile(r'^/interfaces/(?P<iface>[^/]+)/(?P<param>[^/]+)') + # get list of available stats and dump them + stats_list: list[str] = stats.ls([f'^/interfaces/{iface_name}']) + stats_dump: list[dict[str, int]] = stats.dump(stats_list) + + # parse outputs and convert it to a dictionary + for stats_key, stats_value in stats_dump.items(): + parsed_key = regex_parser.search(stats_key).groupdict() + iface_name = parsed_key['iface'] + param = parsed_key['param'] + stats_item = {param: total_value(stats_value)} + if iface_name in ifaces_stats: + ifaces_stats[iface_name].update(stats_item) + else: + ifaces_stats[iface_name] = stats_item + + return ifaces_stats + + +def cli_ifaces_lcp_kernel_list( + config_instance, mode: str = 'candidate' +) -> list[tuple[str, str]]: + """List of all VPP kernel-interfaces (CLI names, attached VPP interfaces) + + Args: + config_instance (VyOS Config): VyOS Config instance + mode (str, optional): `candidate` or `running`. Defaults to 'candidate'. + + Returns: + list[tuple[str, str]]: list of interfaces ([(vpp_iface, kernel_iface)]) + """ + + effective_mode: bool = True if mode == 'running' else False + + # Read a config + config = config_instance.get_config_dict( + ['vpp'], + key_mangling=('-', '_'), + effective=effective_mode, + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + lcp_kernel_ifaces: list[tuple[str, str]] = [] + + # Get a list with kernel interfaces + for ifaces_list in config.get('interfaces', {}).values(): + for iface_name, iface_settings in ifaces_list.items(): + if 'kernel_interface' in iface_settings: + lcp_kernel_ifaces.append( + (iface_name, iface_settings['kernel_interface']) + ) + + return lcp_kernel_ifaces + + +def get_default_hugepage_size() -> int: + """ + Retrieve the system's default huge page size. + :return: The default huge page size in bytes. + """ + page_size = None + try: + # default huge page size + memfd = os.memfd_create('tmp', os.MFD_HUGETLB) + st = os.fstat(memfd) + page_size = st.st_blksize + os.close(memfd) + except OSError: + pass + + return page_size + + +def get_default_page_size() -> int: + """ + Retrieve the system's default page size. + :return: The default page size in bytes. + """ + return os.sysconf('SC_PAGESIZE') + + +def get_hugepage_sizes() -> list[int]: + """ + Retrieve all available huge page sizes from the system. + :return: A list of huge page sizes in bytes. + """ + huge_sizes = [] + path = '/sys/kernel/mm/hugepages/' + try: + entries = os.listdir(path) + for entry in entries: + if entry.startswith('hugepages-'): + try: + size_kb = int(entry.replace('hugepages-', '').replace('kB', '')) + huge_sizes.append(size_kb << 10) # Convert KB to bytes + except ValueError: + pass + except FileNotFoundError: + pass + + return huge_sizes + + +def human_memory_to_bytes(value: str) -> int: + """ + Convert a human-readable vpp memory format (K, M, G) to a byte value. + + :param value: The string memory size in vpp human-readable format. + :return: A int representing the value. + """ + try: + return int(value) + except ValueError: + return int(value[:-1]) << mem_shift[value[-1]] + + +def bytes_to_human_memory(value: int, unit: str) -> str | None: + """ + Convert a byte value to a human-readable format (K, M, G). + + :param value: The size in bytes. + :param unit: The unit to convert to ('K', 'M', 'G'). + :return: A string representing the value in the specified unit, or None if zero. + """ + val = value >> mem_shift[unit] + return f'{val}{unit}' if val else None + + +def human_page_memory_to_bytes(value: str) -> int: + """ + Convert a human-readable vpp page size format to a byte value. + + :param value: The string memory size in vpp human-readable format. + :return: A int representing the value. + """ + default = { + 'default': get_default_page_size, + 'default-hugepage': get_default_hugepage_size, + } + try: + return default[value]() + except KeyError: + return human_memory_to_bytes(value) + + +class EthtoolGDrvinfo: + """Return interface details like `ethtol -i` does""" + + # TODO + # this probably need to be replaced with a code generator + # like ctypeslib or C extension + class EthtoolDrvinfo(ctypes.Structure): + _fields_ = [ + ('cmd', ctypes.c_uint32), + ('driver', ctypes.c_char * 32), # Driver short name + ('version', ctypes.c_char * 32), # Driver version + ('fw_version', ctypes.c_char * 32), # Firmware version + # Be careful: bus info can be longer than 32 chars and thus truncated + ('bus_info', ctypes.c_char * 32), # Bus info. + ('erom_version', ctypes.c_char * 32), # Expansion ROM version + ('reserved2', ctypes.c_char * 12), # Reserved for future use + ('n_priv_flags', ctypes.c_uint32), # Number of private flags + ('n_stats', ctypes.c_uint32), # Number of U64 stats + ('testinfo_len', ctypes.c_uint32), # Test info length + ('eedump_len', ctypes.c_uint32), # EEPROM dump length + ('regdump_len', ctypes.c_uint32), # Register dump length + ] + + def __init__(self, iface: str): + # Constants for ethtool + SIOCETHTOOL = 0x8946 # pretend to be ethtool + ETHTOOL_GDRVINFO = 0x00000003 # Command to get driver info + + # Create a dummy socket + sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + # Prepare the request for getting driver info + drvinfo = self.EthtoolDrvinfo(cmd=ETHTOOL_GDRVINFO) + ifreq: bytes = pack('16sP', iface.encode('utf-8'), ctypes.addressof(drvinfo)) + + # Make an ioctl call to get the driver info + try: + ioctl(sockfd, SIOCETHTOOL, ifreq) + except OSError: + raise FileNotFoundError(f'There is no Ethernet device: {iface}') + + # Close the socket + sockfd.close() + + # save the information + self.driver: str = drvinfo.driver.decode('utf-8').strip('\x00') + self.version: str = drvinfo.version.decode('utf-8').strip('\x00') + self.fw_version: str = drvinfo.fw_version.decode('utf-8').strip('\x00') + self.bus_info: str = drvinfo.bus_info.decode('utf-8').strip('\x00') + self.erom_version: str = drvinfo.erom_version.decode('utf-8').strip('\x00') + self.reserved2: str = drvinfo.reserved2.decode('utf-8').strip('\x00') + self.n_priv_flags: int = drvinfo.n_priv_flags + self.testinfo_len: int = drvinfo.testinfo_len + self.eedump_len: int = drvinfo.eedump_len + self.regdump_len: int = drvinfo.regdump_len + + def bus_info_expand(self, bus_name: str) -> str: + bus_path = Path(f'/sys/bus/{bus_name}/devices').glob(f'{self.bus_info}*') + dev_ids = list(bus_path) + if not dev_ids: + raise FileNotFoundError( + f'No matching IDs on the bus: {self.bus_info} on {bus_name}' + ) + if len(dev_ids) > 1: + raise FileNotFoundError( + f'There are more than one matching IDs on the bus: {dev_ids} on {bus_name}' + ) + return dev_ids[0].name diff --git a/smoketest/config-tests/vpp b/smoketest/config-tests/vpp new file mode 100644 index 000000000..e42568965 --- /dev/null +++ b/smoketest/config-tests/vpp @@ -0,0 +1,49 @@ +set interfaces ethernet eth0 description 'WAN' +set interfaces ethernet eth0 offload gro +set interfaces ethernet eth0 offload gso +set interfaces ethernet eth0 offload sg +set interfaces ethernet eth0 offload tso +set interfaces ethernet eth1 address '192.0.2.1/30' +set interfaces ethernet eth1 address '192.0.2.21/30' +set interfaces ethernet eth1 description 'WAN' +set interfaces ethernet eth2 +set interfaces ethernet eth3 +set interfaces ethernet eth4 description 'Bonding' +set interfaces loopback lo +set system config-management commit-revisions '100' +set system console device ttyS0 speed '115200' +set system host-name 'r16' +set system login user vyos authentication encrypted-password '$6$rounds=656000$FZBlGpWsmSrV2Rvq$YPNAPtk4k6u99FAMxR6cw4DUPCgOomwCgRZRSO5rAoJK8RlMSCkVAFVF3ozL/3mZMfxcuCnwvd5HX6f9V5KzO.' +set system name-server '203.0.113.1' +set system option kernel memory hugepage-size 2M hugepage-count '2048' +set system option time-format '24-hour' +set system syslog local facility all level 'info' +set system syslog local facility local7 level 'debug' +set vpp interfaces bonding bond0 hash-policy 'layer2+3' +set vpp interfaces bonding bond0 member interface 'eth4' +set vpp interfaces bonding bond0 mode '802.3ad' +set vpp interfaces gre gre10 description 'Layer-3' +set vpp interfaces gre gre10 mode 'point-to-point' +set vpp interfaces gre gre10 remote '192.0.2.2' +set vpp interfaces gre gre10 source-address '192.0.2.1' +set vpp interfaces gre gre10 tunnel-type 'l3' +set vpp interfaces gre gre11 description 'L2-bridge-to-eth2' +set vpp interfaces gre gre11 remote '192.0.2.123' +set vpp interfaces gre gre11 source-address '192.0.2.1' +set vpp interfaces gre gre11 tunnel-type 'teb' +set vpp interfaces gre gre12 description 'Multipoint' +set vpp interfaces gre gre12 mode 'point-to-multipoint' +set vpp interfaces gre gre12 remote '0.0.0.0' +set vpp interfaces gre gre12 source-address '192.0.2.21' +set vpp interfaces gre gre12 tunnel-type 'l3' +set vpp interfaces ipip ipip20 remote '192.0.2.2' +set vpp interfaces ipip ipip20 source-address '192.0.2.1' +set vpp interfaces loopback lo12 description 'Loop12' +set vpp interfaces vxlan vxlan10 remote '192.0.2.2' +set vpp interfaces vxlan vxlan10 source-address '192.0.2.1' +set vpp interfaces vxlan vxlan10 vni '10' +set vpp settings interface eth1 driver 'dpdk' +set vpp settings interface eth2 driver 'dpdk' +set vpp settings interface eth3 driver 'dpdk' +set vpp settings interface eth4 driver 'dpdk' +set vpp settings unix poll-sleep-usec '12' diff --git a/smoketest/configs/vpp b/smoketest/configs/vpp new file mode 100644 index 000000000..472c2919b --- /dev/null +++ b/smoketest/configs/vpp @@ -0,0 +1,128 @@ +interfaces { + ethernet eth0 { + description "WAN" + offload { + gro + gso + sg + tso + } + } + ethernet eth1 { + address "192.0.2.1/30" + address "192.0.2.21/30" + description "WAN" + } + ethernet eth2 { + } + ethernet eth3 { + } + ethernet eth4 { + description "Bonding" + } + loopback lo { + } +} +system { + config-management { + commit-revisions "100" + } + console { + device ttyS0 { + speed "115200" + } + } + host-name "r16" + login { + user vyos { + authentication { + encrypted-password "$6$rounds=656000$FZBlGpWsmSrV2Rvq$YPNAPtk4k6u99FAMxR6cw4DUPCgOomwCgRZRSO5rAoJK8RlMSCkVAFVF3ozL/3mZMfxcuCnwvd5HX6f9V5KzO." + } + } + } + name-server "203.0.113.1" + option { + kernel { + memory { + hugepage-size 2M { + hugepage-count "2048" + } + } + } + time-format "24-hour" + } + syslog { + global { + facility all { + level "info" + } + facility local7 { + level "debug" + } + } + } +} +vpp { + interfaces { + bonding bond0 { + hash-policy "layer2+3" + member { + interface "eth4" + } + mode "802.3ad" + } + gre gre10 { + description "Layer-3" + mode "point-to-point" + remote "192.0.2.2" + source-address "192.0.2.1" + tunnel-type "l3" + } + gre gre11 { + description "L2-bridge-to-eth2" + remote "192.0.2.123" + source-address "192.0.2.1" + tunnel-type "teb" + } + gre gre12 { + description "Multipoint" + mode "point-to-multipoint" + remote "0.0.0.0" + source-address "192.0.2.21" + tunnel-type "l3" + } + ipip ipip20 { + remote "192.0.2.2" + source-address "192.0.2.1" + } + loopback lo12 { + description "Loop12" + } + vxlan vxlan10 { + remote "192.0.2.2" + source-address "192.0.2.1" + vni "10" + } + } + settings { + interface eth1 { + driver "dpdk" + } + interface eth2 { + driver "dpdk" + } + interface eth3 { + driver "dpdk" + } + interface eth4 { + driver "dpdk" + } + unix { + poll-sleep-usec "12" + } + } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@11:dhcpv6-server@6:dns-dynamic@4:dns-forwarding@4:firewall@18:flow-accounting@2:https@7:ids@1:interfaces@33:ipoe-server@4:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@2:nat@8:nat66@3:nhrp@1:ntp@3:openconnect@3:openvpn@4:ospf@2:pim@1:policy@8:pppoe-server@11:pptp@5:qos@3:quagga@12:reverse-proxy@2:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@28:vpp@1:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2" +// Release version: 1.5-rolling-202502030007 diff --git a/smoketest/scripts/cli/test_vpp.py b/smoketest/scripts/cli/test_vpp.py new file mode 100755 index 000000000..16302adea --- /dev/null +++ b/smoketest/scripts/cli/test_vpp.py @@ -0,0 +1,1422 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +import os +import re +import sys +import unittest +from collections import defaultdict + +from json import loads + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.process import process_named_running +from vyos.utils.file import read_file +from vyos.utils.process import rc_cmd +from vyos.vpp.utils import human_page_memory_to_bytes + +sys.path.append(os.getenv('vyos_completion_dir')) +from list_mem_page_size import list_mem_page_size + +PROCESS_NAME = 'vpp_main' +VPP_CONF = '/run/vpp/vpp.conf' +base_path = ['vpp'] +driver = 'dpdk' +interface = 'eth1' + + +def get_vpp_config(): + config = defaultdict(dict) + current_section = None + + with open(VPP_CONF, 'r') as f: + for line in f: + line = line.strip() + + if not line or line.startswith('#'): # Ignore empty lines and comments + continue + + section_match = re.match(r'([a-zA-Z0-9_-]+)\s*{', line) + if section_match: + current_section = section_match.group(1) + config[current_section] = {} + continue + + if line == '}': # End of section + current_section = None + continue + + key_value_match = re.match(r'([a-zA-Z0-9_-]+)\s+(.+)', line) + if key_value_match: + key, value = key_value_match.groups() + if current_section: + config[current_section][key] = value + else: + config[key] = value + + return config + + +def get_address(interface): + rc, data = rc_cmd(f'ip --json address show dev {interface}') + if rc == 0: + data = loads(data) + if isinstance(data, list) and len(data) > 0: + ip_address = data[0]['addr_info'][0]['local'] + return ip_address + + +class TestVPP(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestVPP, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def setUp(self): + self.cli_set(base_path + ['settings', 'interface', interface, 'driver', driver]) + self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '10']) + + def tearDown(self): + try: + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + finally: + # Ensure these cleanup operations always run + self.cli_delete(base_path) + self.cli_commit() + + # delete address for Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'address']) + self.cli_commit() + + self.assertFalse(os.path.exists(VPP_CONF)) + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_vpp_basic(self): + main_core = '0' + poll_sleep = '0' + + # Main core must be verified + # expect raise ConfigError + self.cli_set(base_path + ['settings', 'cpu', 'main-core', '99']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core]) + self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', poll_sleep]) + + # commit changes + self.cli_commit() + + config_entries = ( + f'poll-sleep-usec {poll_sleep}', + f'main-core {main_core}', + 'plugin default { disable }', + 'plugin dpdk_plugin.so { enable }', + 'plugin linux_cp_plugin.so { enable }', + 'dev 0000:00:00.0', + 'uio-bind-force', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # route-no-paths is not present in the output + # looks like vpp bug + _, out = rc_cmd('sudo vppctl show lcp') + required_str = 'lcp route-no-paths on' + self.assertIn(required_str, out) + + self.cli_set(base_path + ['settings', 'lcp', 'ignore-kernel-routes']) + self.cli_commit() + + # check disabled 'route no path' + _, out = rc_cmd('sudo vppctl show lcp') + required_str = 'lcp route-no-paths off' + self.assertIn(required_str, out) + + def test_02_vpp_vxlan(self): + vni = '23' + interface_vxlan = f'vxlan{vni}' + interface_kernel = f'vpptap{vni}' + new_interface_kernel = f'vpptap1{vni}' + source_address = '192.0.2.1' + new_source_address = '192.0.2.3' + remote_address = '192.0.2.254' + kernel_address = '203.0.113.1' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address] + ) + self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni]) + + # remote and source address must not be the same + # expect raise ConfigError + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'remote', source_address] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address] + ) + self.cli_set( + base_path + + [ + 'interfaces', + 'vxlan', + interface_vxlan, + 'kernel-interface', + interface_kernel, + ] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/24'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + current_address = get_address(interface_kernel) + self.assertEqual(kernel_address, current_address) + + # check vxlan interface + _, out = rc_cmd('sudo vppctl show vxlan tunnel') + required_str = f'[0] instance 23 src {source_address} dst {remote_address} src_port 4789 dst_port 4789 vni {vni}' + self.assertIn(required_str, out) + + # update vxlan interface + self.cli_set( + base_path + + [ + 'interfaces', + 'vxlan', + interface_vxlan, + 'source-address', + new_source_address, + ] + ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + [ + 'interfaces', + 'ethernet', + interface, + 'vif', + vni, + 'address', + f'{new_source_address}/24', + ] + ) + self.cli_commit() + + # check gre interface after update + _, out = rc_cmd('sudo vppctl show vxlan tunnel') + required_str = ( + f'[0] instance {vni} src {new_source_address} dst {remote_address}' + ) + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + self.assertEqual(kernel_address, current_address) + + # change vpp settings + self.cli_set(base_path + ['settings', 'unix', 'poll-sleep-usec', '5']) + self.cli_commit() + + config = read_file(VPP_CONF) + self.assertIn('poll-sleep-usec 5', config) + + # delete vxlan kernel-interface but do not delete 'vpp kernel-interface' + # expect raise ConfigError + self.cli_delete( + base_path + + [ + 'interfaces', + 'vxlan', + interface_vxlan, + 'kernel-interface', + interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # update vxlan kernel-interface but do not change 'vpp kernel-interface' + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'vxlan', + interface_vxlan, + 'kernel-interface', + new_interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete vpp kernel-interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete vxlan kernel-interface + self.cli_delete( + base_path + ['interfaces', 'vxlan', interface_vxlan, 'kernel-interface'] + ) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete vxlan interface + self.cli_delete(base_path + ['interfaces', 'vxlan', interface_vxlan]) + self.cli_commit() + + # delete vif Ethernet interface + self.cli_delete(['interfaces', 'ethernet', interface, 'vif']) + self.cli_commit() + + def test_03_vpp_gre(self): + interface_gre = 'gre12' + interface_kernel = 'vpptun12' + new_interface_kernel = 'vpptun123' + source_address = '192.0.2.1' + new_source_address = '192.0.2.2' + remote_address = '192.0.2.254' + kernel_address = '10.0.0.0' + + self.cli_set( + base_path + + ['interfaces', 'gre', interface_gre, 'source-address', source_address] + ) + self.cli_set( + base_path + ['interfaces', 'gre', interface_gre, 'remote', remote_address] + ) + self.cli_set( + base_path + + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31'] + ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{source_address}/24'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + current_address = get_address(interface_kernel) + self.assertEqual(kernel_address, current_address) + + # check gre interface + _, out = rc_cmd('sudo vppctl show gre tunnel') + required_str = f'[0] instance 12 src {source_address} dst {remote_address}' + self.assertIn(required_str, out) + + # update gre interface + self.cli_set( + base_path + + ['interfaces', 'gre', interface_gre, 'source-address', new_source_address] + ) + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) + self.cli_commit() + + # check gre interface after update + _, out = rc_cmd('sudo vppctl show gre tunnel') + required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}' + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + self.assertEqual(kernel_address, current_address) + + # delete gre kernel-interface but do not delete 'vpp kernel-interface' + # expect raise ConfigError + self.cli_delete( + base_path + + ['interfaces', 'gre', interface_gre, 'kernel-interface', interface_kernel] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # update gre kernel-interface but do not change 'vpp kernel-interface' + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'gre', + interface_gre, + 'kernel-interface', + new_interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete kernel interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete gre kernel-interface + self.cli_delete( + base_path + ['interfaces', 'gre', interface_gre, 'kernel-interface'] + ) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete gre interface + self.cli_delete(base_path + ['interfaces', 'gre', interface_gre]) + self.cli_commit() + + @unittest.skip('Skipping this test geneve index always is 0') + def test_04_vpp_geneve(self): + vni = '2' + # Must be 'geneve0' to pass smoketest + # As geneve interfaces cannot be named with "instance" suffix + interface_geneve = 'geneve0' + interface_kernel = f'vpptun{vni}' + new_interface_kernel = f'vpptun1{vni}' + source_address = '192.0.2.1' + new_source_address = '192.0.2.2' + remote_address = '203.0.113.10' + kernel_address = '10.0.0.1' + + self.cli_set( + base_path + + [ + 'interfaces', + 'geneve', + interface_geneve, + 'source-address', + source_address, + ] + ) + self.cli_set( + base_path + + ['interfaces', 'geneve', interface_geneve, 'remote', remote_address] + ) + self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve, 'vni', vni]) + self.cli_set( + base_path + + [ + 'interfaces', + 'geneve', + interface_geneve, + 'kernel-interface', + interface_kernel, + ] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + current_address = get_address(interface_kernel) + self.assertEqual(kernel_address, current_address) + + # check geneve interface + _, out = rc_cmd('sudo vppctl show geneve tunnel') + required_str = f'[0] lcl {source_address} rmt {remote_address} vni {vni}' + self.assertIn(required_str, out) + + # update geneve interface + self.cli_set( + base_path + + [ + 'interfaces', + 'geneve', + interface_geneve, + 'source-address', + new_source_address, + ] + ) + self.cli_commit() + + # check geneve interface after update + _, out = rc_cmd('sudo vppctl show geneve tunnel') + required_str = f'[0] lcl {new_source_address} rmt {remote_address} vni {vni}' + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + self.assertEqual(kernel_address, current_address) + + # delete geneve kernel-interface but do not delete 'vpp kernel-interface' + # expect raise ConfigError + self.cli_delete( + base_path + + [ + 'interfaces', + 'geneve', + interface_geneve, + 'kernel-interface', + interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # update gemeve kernel-interface but do not change 'vpp kernel-interface' + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'geneve', + interface_geneve, + 'kernel-interface', + new_interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete vpp kernel-interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete geneve kernel-interface + self.cli_delete( + base_path + ['interfaces', 'geneve', interface_geneve, 'kernel-interface'] + ) + self.cli_commit() + + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete geneve interface + self.cli_set(base_path + ['interfaces', 'geneve', interface_geneve]) + self.cli_commit() + + def test_05_vpp_loopback(self): + interface_loopback = 'lo11' + interface_kernel = 'vpptun11' + new_interface_kernel = 'vpptun12' + kernel_address = '192.0.2.54' + + self.cli_set(base_path + ['interfaces', 'loopback', interface_loopback]) + self.cli_set( + base_path + + [ + 'interfaces', + 'loopback', + interface_loopback, + 'kernel-interface', + interface_kernel, + ] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/25'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + current_address = get_address(interface_kernel) + self.assertEqual(kernel_address, current_address) + + # check loopback interface + _, out = rc_cmd('sudo vppctl show interface loop11') + required_str = 'loop11' + self.assertIn(required_str, out) + + # delete loopback kernel-interface but do not delete 'vpp kernel-interface' + # expect raise ConfigError + self.cli_delete( + base_path + + [ + 'interfaces', + 'loopback', + interface_loopback, + 'kernel-interface', + interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # update loopback kernel-interface but do not change 'vpp kernel-interface' + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'loopback', + interface_loopback, + 'kernel-interface', + new_interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete vpp kernel-interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete loopback kernel-interface + self.cli_delete( + base_path + + ['interfaces', 'loopback', interface_loopback, 'kernel-interface'] + ) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete loopback interface + self.cli_delete(base_path + ['interfaces', 'loopback', interface_loopback]) + self.cli_commit() + + @unittest.skip('Skipping temporary bonding, sometimes get recursion T7117') + def test_06_vpp_bonding(self): + interface_bond = 'bond23' + interface_kernel = 'vpptun23' + hash = 'layer3+4' + mode = '802.3ad' + description = 'Interface-Bonding' + vlans = ['123', '456'] + vlan_description = 'My-vlan-123' + + self.cli_set( + base_path + + [ + 'interfaces', + 'bonding', + interface_bond, + 'member', + 'interface', + interface, + ] + ) + self.cli_set( + base_path + ['interfaces', 'bonding', interface_bond, 'hash-policy', hash] + ) + self.cli_set( + base_path + ['interfaces', 'bonding', interface_bond, 'mode', mode] + ) + + # commit changes + self.cli_commit() + + # Check for interface state "BondEthernet23 up" + _, out = rc_cmd('sudo vppctl show interface') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + self.assertRegex( + normalized_out, + r'BondEthernet23\s+\d+\s+up', + "Interface BondEthernet23 is not in the expected state 'up'.", + ) + + # set kernel interface + self.cli_set( + base_path + + [ + 'interfaces', + 'bonding', + interface_bond, + 'kernel-interface', + interface_kernel, + ] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'description', description] + ) + for vlan in vlans: + self.cli_set( + base_path + + [ + 'kernel-interfaces', + interface_kernel, + 'vif', + vlan, + 'description', + vlan_description, + ] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}')) + + current_alias = read_file(f'/sys/class/net/{interface_kernel}/ifalias') + vlan_alias = read_file(f'/sys/class/net/{interface_kernel}.{vlan}/ifalias') + self.assertEqual(current_alias, description) + self.assertEqual(vlan_alias, vlan_description) + + # check bonding interface + _, out = rc_cmd('sudo vppctl show bond details') + required_enries = ( + 'BondEthernet23', + 'mode: lacp', + 'load balance: l34', + 'number of active members: 0', + 'number of members: 1', + f'{interface}', + 'device instance: 0', + 'interface id: 23', + ) + for entry in required_enries: + self.assertIn(entry, out) + + # check interface state + _, out = rc_cmd('sudo vppctl show interface') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + # Check for interface state "BondEthernet23 up" + self.assertRegex( + normalized_out, + r'BondEthernet23\s+\d+\s+up', + "Interface BondEthernet23 is not in the expected state 'up'.", + ) + + # delete vpp kernel-interface vlan + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel, 'vif']) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}.{vlan}')) + + # delete vpp kernel-interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete bonding kernel-interface + self.cli_delete( + base_path + ['interfaces', 'bonding', interface_bond, 'kernel-interface'] + ) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete bonding interface + self.cli_delete(base_path + ['interfaces', 'bonding']) + self.cli_commit() + + # check deleting bonding interface + _, out = rc_cmd('sudo vppctl show interface') + self.assertNotIn('BondEthernet23', out) + + def test_07_vpp_bridge(self): + fake_member = 'eth2' + members = [interface] + interface_bridge = 'br10' + vni = '23' + interface_vxlan = f'vxlan{vni}' + source_address = '192.0.2.1' + remote_address = '192.0.2.254' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + for member in members: + self.cli_set( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + member, + ] + ) + + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + # Perform assertions based on the normalized output + self.assertIn('BD-ID Index BSN Age(min)', normalized_out) + self.assertIn('10 1 0 off', normalized_out) + self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out) + self.assertIn('on on flood on', normalized_out) + self.assertIn('Interface If-idx ISN', normalized_out) + # Check Interface, If-idx, ISN + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + + # Set non exist member + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + fake_member, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + fake_member, + ] + ) + + # Add VXLAN to the bridge + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address] + ) + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address] + ) + self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni]) + self.cli_set( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + interface_vxlan, + ] + ) + + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + # Perform assertions based on the normalized output + self.assertIn('BD-ID Index BSN Age(min)', normalized_out) + self.assertIn('10 1 0 off', normalized_out) + self.assertIn('Learning U-Forwrd UU-Flood Flooding', normalized_out) + self.assertIn('on on flood on', normalized_out) + self.assertIn('Interface If-idx ISN', normalized_out) + # Check Interface, If-idx, ISN + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+') + + # Add check dependency ethernet => bridge + self.cli_set( + base_path + ['settings', 'interface', interface, 'dpdk-options', 'promisc'] + ) + self.cli_commit() + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + self.assertRegex(out, r'\s*eth1\s+\d+\s+\d+') + self.assertRegex(out, r'\s*vxlan_tunnel23\s+\d+\s+\d+') + + # Add Loopback BVI to the bridge + self.cli_set(base_path + ['interfaces', 'loopback', f'lo{vni}']) + self.cli_set( + base_path + + [ + 'interfaces', + 'bridge', + interface_bridge, + 'member', + 'interface', + f'lo{vni}', + 'bvi', + ] + ) + # commit changes + self.cli_commit() + + # check bridge interface + _, out = rc_cmd('sudo vppctl show bridge-domain 10 detail') + # Normalize the output for consistent whitespace + normalized_out = re.sub(r'\s+', ' ', out) + + self.assertIn('10 1 0 off', normalized_out) + self.assertRegex(out, r'\bloop23\s+\d+\s+\d+\s+\d+\s+\*\s+') + + def test_08_vpp_ipip(self): + interface_ipip = 'ipip12' + interface_kernel = 'vpptun12' + new_interface_kernel = 'vpptun123' + source_address = '192.0.2.1' + new_source_address = '192.0.2.2' + remote_address = '192.0.2.5' + kernel_address = '10.0.0.0' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set( + base_path + + ['interfaces', 'ipip', interface_ipip, 'source-address', source_address] + ) + self.cli_set( + base_path + ['interfaces', 'ipip', interface_ipip, 'remote', remote_address] + ) + self.cli_set( + base_path + + [ + 'interfaces', + 'ipip', + interface_ipip, + 'kernel-interface', + interface_kernel, + ] + ) + self.cli_set( + base_path + + ['kernel-interfaces', interface_kernel, 'address', f'{kernel_address}/31'] + ) + + # commit changes + self.cli_commit() + + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + current_address = get_address(interface_kernel) + self.assertEqual(kernel_address, current_address) + + # check ipip interface + _, out = rc_cmd('sudo vppctl show ipip tunnel') + required_str = f'[0] instance 12 src {source_address} dst {remote_address}' + self.assertIn(required_str, out) + + # update ipip interface + self.cli_set( + base_path + + [ + 'interfaces', + 'ipip', + interface_ipip, + 'source-address', + new_source_address, + ] + ) + + # source address of the tunnel interface should be configured + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + ['interfaces', 'ethernet', interface, 'address', f'{new_source_address}/24'] + ) + self.cli_commit() + + # check ipip interface after update + _, out = rc_cmd('sudo vppctl show ipip tunnel') + required_str = f'[0] instance 12 src {new_source_address} dst {remote_address}' + self.assertIn(required_str, out) + self.assertTrue(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + self.assertEqual(kernel_address, current_address) + + # delete ipip kernel-interface but do not delete 'vpp kernel-interface' + # expect raise ConfigError + self.cli_delete( + base_path + + [ + 'interfaces', + 'ipip', + interface_ipip, + 'kernel-interface', + interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # update ipip kernel-interface but do not change 'vpp kernel-interface' + # expect raise ConfigError + self.cli_set( + base_path + + [ + 'interfaces', + 'ipip', + interface_ipip, + 'kernel-interface', + new_interface_kernel, + ] + ) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete kernel interface + self.cli_delete(base_path + ['kernel-interfaces', interface_kernel]) + self.cli_commit() + + # delete ipip kernel-interface + self.cli_delete( + base_path + ['interfaces', 'ipip', interface_ipip, 'kernel-interface'] + ) + self.cli_commit() + self.assertFalse(os.path.isdir(f'/sys/class/net/{interface_kernel}')) + + # delete ipip interface + self.cli_delete(base_path + ['interfaces', 'ipip', interface_ipip]) + self.cli_commit() + + def test_09_vpp_xconnect(self): + vni = '23' + interface_vxlan = f'vxlan{vni}' + interface_xconnect = f'xcon{vni}' + source_address = '192.0.2.1' + remote_address = '192.0.2.254' + + self.cli_set(['interfaces', 'ethernet', interface, 'address', '192.0.2.1/24']) + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'source-address', source_address] + ) + self.cli_set( + base_path + + ['interfaces', 'vxlan', interface_vxlan, 'remote', remote_address] + ) + self.cli_set(base_path + ['interfaces', 'vxlan', interface_vxlan, 'vni', vni]) + + # Add xconneect + self.cli_set( + base_path + + [ + 'interfaces', + 'xconnect', + interface_xconnect, + 'member', + 'interface', + interface, + ] + ) + + # Cross connect interfaces require 2 interfaces + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + + [ + 'interfaces', + 'xconnect', + interface_xconnect, + 'member', + 'interface', + interface_vxlan, + ] + ) + + # commit changes + self.cli_commit() + + # check interface mode + _, out = rc_cmd('sudo vppctl show mode') + required_str_list = [ + f'l2 xconnect {interface} vxlan_tunnel{vni}', + f'l2 xconnect vxlan_tunnel{vni} {interface}', + ] + for required_string in required_str_list: + self.assertIn(required_string, out) + + # delete xconnect interface + self.cli_delete(base_path + ['interfaces', 'xconnect', interface_xconnect]) + self.cli_commit() + + # check delete xconnect interface + _, out = rc_cmd('sudo vppctl show mode') + for required_string in required_str_list: + self.assertNotIn(required_string, out) + + def test_10_vpp_driver_options(self): + dpdk_options = { + 'num-rx-desc': '512', + 'num-tx-desc': '512', + 'num-rx-queues': '1', + 'num-tx-queues': '1', + } + main_core = '0' + workers = '1' + + base_interface_path = base_path + ['settings', 'interface', interface] + + for option, value in dpdk_options.items(): + self.cli_set(base_interface_path + ['dpdk-options', option, value]) + + # rx/tx queue configuration expect VPP workers to be set + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core]) + self.cli_set(base_path + ['settings', 'cpu', 'workers', workers]) + + # DPDK driver expect only dpdk-options and not xdp-options to be set + # expect raise ConfigError + self.cli_set(base_interface_path + ['xdp-options', 'no-syscall-lock']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # delete xdp-options and apply commit + self.cli_delete(base_interface_path + ['xdp-options']) + self.cli_commit() + + # check dpdk options in config file + config = read_file(VPP_CONF) + + for option, value in dpdk_options.items(): + self.assertIn(f'{option} {value}', config) + + def test_11_vpp_cpu_settings(self): + main_core = '2' + workers = '1' + skip_cores = '1' + + self.cli_set(base_path + ['settings', 'cpu', 'workers', workers]) + + # "cpu workers" reqiures main-core to be set + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core]) + + self.cli_set(base_path + ['settings', 'cpu', 'skip-cores', '99']) + + # "cpu skip-cores" cannot be more than number of available CPUs - 1 + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'skip-cores', skip_cores]) + + self.cli_commit() + + config_entries = ( + f'skip-cores {skip_cores}', + f'main-core {main_core}', + f'workers {workers}', + 'dev 0000:00:00.0', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_12_vpp_cpu_corelist_workers(self): + main_core = '0' + corelist_workers = ['3'] + + for worker in corelist_workers: + self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', worker]) + + # "cpu corelist-workers" reqiures main-core to be set + # expect raise ConfigError + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['settings', 'cpu', 'main-core', main_core]) + + # corelist-workers and workers cannot be used at the same time + # expect raise ConfigError + self.cli_set(base_path + ['settings', 'cpu', 'workers', '2']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['settings', 'cpu', 'workers']) + + # verify corelist-workers are set not correctly + # expect raise ConfigError + self.cli_set(base_path + ['settings', 'cpu', 'corelist-workers', '99-101']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path + ['settings', 'cpu', 'corelist-workers', '99-101']) + + self.cli_commit() + + config_entries = ( + f'main-core {main_core}', + f'corelist-workers {",".join(corelist_workers)}', + 'dev 0000:00:00.0', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + def test_13_1_buffer_page_size(self): + sizes = ['default', 'default-hugepage'] + list_mem_page_size() + for size in sizes: + if human_page_memory_to_bytes(size) >= 1 << 30: + continue + self.cli_set(base_path + ['settings', 'buffers', 'page-size', size]) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['buffers']['page-size'], size) + + def test_13_2_statseg_page_size(self): + sizes = ['default', 'default-hugepage'] + list_mem_page_size() + for size in sizes: + if human_page_memory_to_bytes(size) >= 1 << 30: + continue + self.cli_set(base_path + ['settings', 'statseg', 'page-size', size]) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['statseg']['page-size'], size) + + def test_13_3_mem_page_size(self): + sizes = ['default', 'default-hugepage'] + list_mem_page_size() + for size in sizes: + if human_page_memory_to_bytes(size) >= 1 << 30: + continue + self.cli_set( + base_path + ['settings', 'memory', 'main-heap-page-size', size] + ) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['memory']['main-heap-page-size'], size) + + def test_14_mem_default_hugepage(self): + sizes = list_mem_page_size(hugepage_only=True) + for size in sizes: + if human_page_memory_to_bytes(size) >= 1 << 30: + continue + self.cli_set( + base_path + ['settings', 'memory', 'default-hugepage-size', size] + ) + self.cli_commit() + + conf = get_vpp_config() + self.assertEqual(conf['memory']['default-hugepage-size'], size) + + def test_15_vpp_ipsec_xfrm_nl(self): + base_ipsec = base_path + ['settings', 'ipsec'] + batch_delay = '250' + batch_size = '150' + rx_buffer_zise = '1024' + + self.cli_set(base_ipsec + ['netlink', 'batch-delay-ms', batch_delay]) + self.cli_set(base_ipsec + ['netlink', 'batch-size', batch_size]) + self.cli_set(base_ipsec + ['netlink', 'rx-buffer-size', rx_buffer_zise]) + self.cli_commit() + + config_entries = ( + 'linux-xfrm-nl', + 'enable-route-mode-ipsec', + 'interface ipsec', + f'nl-batch-delay-ms {batch_delay}', + f'nl-batch-size {batch_size}', + f'nl-rx-buffer-size {rx_buffer_zise}', + ) + + # Check configured options + config = read_file(VPP_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # set IPsec tunnel-type ipip + self.cli_set(base_ipsec + ['interface-type', 'ipip']) + self.cli_commit() + + config = read_file(VPP_CONF) + self.assertIn('interface ipip', config) + + def test_16_vpp_cgnat(self): + base_cgnat = base_path + ['nat', 'cgnat'] + iface_out = 'eth0' + iface_inside = 'eth1' + timeout_udp = '150' + timeout_icmp = '30' + timeout_tcp_est = '600' + timeout_tcp_trans = '120' + inside_prefix = '100.64.0.0/24' + outside_prefix = '192.0.2.1/32' + + self.cli_set(base_path + ['settings', 'interface', iface_out, 'driver', driver]) + self.cli_set(base_cgnat + ['interface', 'inside', iface_inside]) + self.cli_set(base_cgnat + ['interface', 'outside', iface_out]) + self.cli_set(base_cgnat + ['rule', '100', 'inside-prefix', inside_prefix]) + self.cli_set(base_cgnat + ['rule', '100', 'outside-prefix', outside_prefix]) + self.cli_set(base_cgnat + ['timeout', 'icmp', timeout_icmp]) + self.cli_set(base_cgnat + ['timeout', 'tcp-established', timeout_tcp_est]) + self.cli_set(base_cgnat + ['timeout', 'tcp-transitory', timeout_tcp_trans]) + self.cli_set(base_cgnat + ['timeout', 'udp', timeout_udp]) + self.cli_commit() + + # Check interfaces + _, out = rc_cmd('sudo vppctl show det44 interfaces') + self.assertIn(f'{iface_inside} in', out) + self.assertIn(f'{iface_out} out', out) + + # Check mappings + _, out = rc_cmd('sudo vppctl show det44 mappings') + self.assertIn(inside_prefix, out) + self.assertIn(outside_prefix, out) + + # Check timeouts + _, out = rc_cmd('sudo vppctl show det44 timeouts') + self.assertIn(f'udp timeout: {timeout_udp}sec', out) + self.assertIn(f'tcp established timeout: {timeout_tcp_est}sec', out) + self.assertIn(f'tcp transitory timeout: {timeout_tcp_trans}sec', out) + self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) + + def test_17_vpp_nat(self): + base_nat = base_path + ['nat44'] + base_nat_settings = base_path + ['settings', 'nat44'] + exclude_local_addr = '100.64.0.52' + exclude_local_port = '22' + iface_out = 'eth0' + iface_inside = 'eth1' + timeout_udp = '150' + timeout_icmp = '30' + timeout_tcp_est = '600' + timeout_tcp_trans = '120' + translation_pool = '192.0.2.1-192.0.2.2' + static_ext_addr = '192.0.2.55' + static_local_addr = '100.64.0.55' + sess_limit = '64000' + + self.cli_set(base_path + ['settings', 'interface', iface_out, 'driver', driver]) + self.cli_set(base_nat + ['interface', 'inside', iface_inside]) + self.cli_set(base_nat + ['interface', 'outside', iface_out]) + self.cli_set( + base_nat + ['address-pool', 'translation', 'address', translation_pool] + ) + self.cli_set( + base_nat + ['exclude', 'rule', '100', 'local-address', exclude_local_addr] + ) + self.cli_set( + base_nat + ['exclude', 'rule', '100', 'local-port', exclude_local_port] + ) + self.cli_set( + base_nat + ['static', 'rule', '100', 'external', 'address', static_ext_addr] + ) + self.cli_set( + base_nat + ['static', 'rule', '100', 'local', 'address', static_local_addr] + ) + + self.cli_set(base_nat_settings + ['no-forwarding']) + self.cli_set(base_nat_settings + ['session-limit', sess_limit]) + self.cli_set(base_nat_settings + ['timeout', 'icmp', timeout_icmp]) + self.cli_set( + base_nat_settings + ['timeout', 'tcp-established', timeout_tcp_est] + ) + self.cli_set( + base_nat_settings + ['timeout', 'tcp-transitory', timeout_tcp_trans] + ) + self.cli_set(base_nat_settings + ['timeout', 'udp', timeout_udp]) + self.cli_commit() + + # Check addresses + _, out = rc_cmd('sudo vppctl show nat44 addresses') + self.assertIn(translation_pool.split('-')[0], out) + self.assertIn(translation_pool.split('-')[1], out) + + # Check interfaces + _, out = rc_cmd('sudo vppctl show nat44 interfaces') + self.assertIn(f'{iface_inside} in', out) + self.assertIn(f'{iface_out} out', out) + + # Check mappings + _, out = rc_cmd('sudo vppctl show nat44 static mappings') + self.assertIn( + f'local {static_local_addr} external {static_ext_addr} vrf 0', out + ) + self.assertIn(f'{exclude_local_addr}:{exclude_local_port} vrf 0', out) + + # Check timeouts + _, out = rc_cmd('sudo vppctl show nat timeouts') + self.assertIn(f'udp timeout: {timeout_udp}sec', out) + self.assertIn(f'tcp-established timeout: {timeout_tcp_est}sec', out) + self.assertIn(f'tcp-transitory timeout: {timeout_tcp_trans}sec', out) + self.assertIn(f'icmp timeout: {timeout_icmp}sec', out) + + # Summary + _, out = rc_cmd('sudo vppctl show nat44 summary') + self.assertIn(f'max translations per thread: {sess_limit} fib 0', out) + + def test_18_vpp_sflow(self): + base_sflow = ['system', 'sflow'] + + self.cli_set(base_path + ['sflow', 'interface', interface]) + self.cli_set(base_sflow + ['interface', interface]) + self.cli_set(base_sflow + ['server', '127.0.0.1']) + self.cli_set(base_sflow + ['vpp']) + self.cli_commit() + + # Check sFlow + _, out = rc_cmd('sudo vppctl show sflow') + + expected_entries = ( + 'sflow sampling-direction ingress', + f'sflow enable {interface}', + 'interfaces enabled: 1', + ) + + for expected_entry in expected_entries: + self.assertIn(expected_entry, out) + + self.cli_delete(base_sflow) + self.cli_commit() + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/completion/list_mem_page_size.py b/src/completion/list_mem_page_size.py new file mode 100644 index 000000000..1ed81a15f --- /dev/null +++ b/src/completion/list_mem_page_size.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# 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/>. +import argparse +from vyos.vpp.utils import ( + get_hugepage_sizes, + get_default_hugepage_size, + get_default_page_size, + bytes_to_human_memory, +) + + +def get_default_page_sizes() -> list[int]: + """ + Retrieve the system's default page sizes, including huge pages. + :return: A list of page sizes in bytes. + """ + page_sizes = [] + # default system page size + page_size = get_default_page_size() + if page_size: + page_sizes.append(page_size) + + # default huge page size + page_size = get_default_hugepage_size() + if page_size: + page_sizes.append(page_size) + + return page_sizes + + +def list_mem_page_size(hugepage_only=None) -> list[str]: + result = [] + page_sizes = get_hugepage_sizes() + + if not hugepage_only: + page_sizes += get_default_page_sizes() + + page_sizes = set(page_sizes) + for unit in ['K', 'M', 'G']: + for size in page_sizes: + if val := bytes_to_human_memory(size, unit): + result.append(val) + + return result + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--hugepage_only', type=str, help='List only available hugepage sizes.' + ) + args = parser.parse_args() + + result = list_mem_page_size(args.hugepage_only) + print(' '.join(result)) diff --git a/src/completion/list_vpp_interfaces.py b/src/completion/list_vpp_interfaces.py new file mode 100644 index 000000000..ea6a77bed --- /dev/null +++ b/src/completion/list_vpp_interfaces.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS Inc. +# +# 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/>. + +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl +from vyos.vpp.utils import vpp_ifaces_list + + +def get_vpp_ifaces_names(): + config = ConfigTreeQuery() + if not config.exists('vpp settings interface'): + return [] + + vpp = VPPControl() + vpp_ifaces = vpp_ifaces_list(vpp.api) + ifaces_names = [iface['interface_name'] for iface in vpp_ifaces] + + return sorted(ifaces_names) + + +if __name__ == "__main__": + ifaces = [] + ifaces = get_vpp_ifaces_names() + print(" ".join(ifaces)) diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py new file mode 100755 index 000000000..0ea8d261b --- /dev/null +++ b/src/conf_mode/vpp.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from pathlib import Path + +from pyroute2 import IPRoute +from vpp_papi import VPPIOError, VPPValueError + +from vyos import ConfigError +from vyos import airbag +from vyos.base import Warning +from vyos.config import Config, config_dict_merge +from vyos.configdep import set_dependents, call_dependents +from vyos.configdict import node_changed, leaf_node_changed +from vyos.ifconfig import Section +from vyos.template import render +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.process import call +from vyos.utils.system import sysctl_read, sysctl_apply + +from vyos.vpp import VPPControl +from vyos.vpp import control_host +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_dev_driver, + verify_vpp_minimum_cpus, + verify_vpp_minimum_memory, + verify_vpp_settings_cpu_and_corelist_workers, + verify_vpp_settings_cpu_corelist_workers, + verify_vpp_cpu_main_core, + verify_vpp_settings_cpu_skip_cores, + verify_vpp_settings_cpu_workers, + verify_vpp_nat44_workers, + verify_vpp_memory, + verify_vpp_statseg_size, + verify_vpp_interfaces_dpdk_num_queues, + verify_vpp_host_resources, +) +from vyos.vpp.config_filter import iface_filter_eth +from vyos.vpp.utils import EthtoolGDrvinfo +from vyos.vpp.configdb import JSONStorage + +airbag.enable() + +service_name = 'vpp' +service_conf = Path(f'/run/vpp/{service_name}.conf') +systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' + +dependency_interface_type_map = { + 'vpp_interfaces_bonding': 'bonding', + 'vpp_interfaces_bridge': 'bridge', + 'vpp_interfaces_ethernet': 'ethernet', + 'vpp_interfaces_geneve': 'geneve', + 'vpp_interfaces_gre': 'gre', + 'vpp_interfaces_ipip': 'ipip', + 'vpp_interfaces_loopback': 'loopback', + 'vpp_interfaces_vxlan': 'vxlan', + 'vpp_interfaces_xconnect': 'xconnect', +} + +# dict of drivers that needs to be overrided +override_drivers: dict[str, str] = { + 'hv_netvsc': 'uio_hv_generic', + 'ena': 'vfio-pci', +} + +# drivers that does not use PCIe addresses +not_pci_drv: list[str] = ['hv_netvsc'] + +# drivers that support interrupt RX mode for DPDK and XDP +drivers_support_interrupt: dict[str, list] = { + 'atlantic': ['dpdk', 'xdp'], + 'bnx2x': ['dpdk'], + 'e1000': ['dpdk'], + 'ena': ['dpdk', 'xdp'], + 'i40e': ['dpdk', 'xdp'], + 'ice': ['dpdk', 'xdp'], + 'igb': ['xdp'], + 'igc': ['dpdk', 'xdp'], + 'ixgbe': ['dpdk', 'xdp'], + 'qede': ['dpdk', 'xdp'], + 'vmxnet3': ['xdp'], + 'virtio_net': ['xdp'], +} + + +def get_config(config=None): + # use persistent config to store interfaces data between executions + # this is required because some interfaces after they are connected + # to VPP is really hard or impossible to restore without knowing + # their original parameters (like IDs) + persist_config = JSONStorage('vpp_conf') + eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read( + 'eth_ifaces', {} + ) + + if config: + conf = config + else: + conf = Config() + + base = ['vpp'] + base_settings = ['vpp', 'settings'] + + # find interfaces removed from VPP + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + xconn_members = deps_xconnect_dict(conf) + + removed_ifaces = [] + tmp = node_changed(conf, base_settings + ['interface']) + if tmp: + for removed_iface in tmp: + to_append = { + 'iface_name': removed_iface, + 'driver': effective_config['settings']['interface'][removed_iface][ + 'driver' + ], + } + removed_ifaces.append(to_append) + # add an interface to a list of interfaces that need + # to be reinitialized after the commit + set_dependents('ethernet', conf, removed_iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # sFlow dependency + if conf.exists(['vpp', 'sflow']): + set_dependents('vpp_sflow', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + if not conf.exists(base): + return { + 'removed_ifaces': removed_ifaces, + 'xconn_members': xconn_members, + 'persist_config': eth_ifaces_persist, + } + + config = conf.get_config_dict( + base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + ) + + # Get default values which we need to conditionally update into the + # dictionary retrieved. + default_values = conf.get_config_defaults(**config.kwargs, recursive=True) + + # delete 'xdp-options' from defaults if driver is DPDK + for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): + if iface_config.get('driver') == 'dpdk': + del default_values['settings']['interface'][iface]['xdp_options'] + + config = config_dict_merge(default_values, config) + + # Ignore default XML values if config doesn't exists + if not conf.exists(base_settings + ['ipsec']): + del config['settings']['ipsec'] + + # add running config + if effective_config: + config['effective'] = effective_config + + if 'settings' in config: + if 'interface' in config['settings']: + for iface, iface_config in config['settings']['interface'].items(): + # Driver must be configured to continue + if 'driver' not in iface_config: + raise ConfigError( + f'"driver" must be configured for {iface} interface!' + ) + + old_driver = leaf_node_changed( + conf, base_settings + ['interface', iface, 'driver'] + ) + + # Get current kernel module, required for extra verification and + # logic for VMBus interfaces + config['settings']['interface'][iface]['kernel_module'] = ( + EthtoolGDrvinfo(iface).driver + ) + + # filter unsupported config nodes + iface_filter_eth(conf, iface) + set_dependents('ethernet', conf, iface) + # Interfaces with changed driver should be removed/readded + if old_driver and old_driver[0] == 'dpdk': + removed_ifaces.append( + { + 'iface_name': iface, + 'driver': 'dpdk', + } + ) + + # Get PCI address or device ID + if iface_config['driver'] == 'dpdk': + if 'dpdk_options' not in iface_config: + iface_config['dpdk_options'] = {} + # Check in a persistent config first + id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get( + 'dev_id' + ) + if id_from_persisten_conf: + iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf + else: + try: + iface_to_search = iface + if old_driver and old_driver[0] == 'xdp': + iface_to_search = f'defunct_{iface}' + iface_config['dpdk_options']['dev_id'] = ( + control_host.get_dev_id(iface_to_search) + ) + except Exception: + # Return empty address if all attempts failed + # We will catch this in verify() + iface_config['dpdk_options']['dev_id'] = '' + # prepare XDP interface parameters + if iface_config['driver'] == 'xdp': + xdp_api_params = { + 'rxq_size': int(iface_config['xdp_options']['rx_queue_size']), + 'txq_size': int(iface_config['xdp_options']['tx_queue_size']), + } + if iface_config['xdp_options']['num_rx_queues'] == 'all': + xdp_api_params['rxq_num'] = 0 + else: + xdp_api_params['rxq_num'] = int( + iface_config['xdp_options']['num_rx_queues'] + ) + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['mode'] = 'zero-copy' + if 'zero-copy' in iface_config['xdp_options']: + xdp_api_params['flags'] = 'no_syscall_lock' + iface_config['xdp_api_params'] = xdp_api_params + + if removed_ifaces: + config['removed_ifaces'] = removed_ifaces + config['xconn_members'] = xconn_members + + # Dependencies + for dependency, interface_type in dependency_interface_type_map.items(): + # if conf.exists(base + ['interfaces', interface_type]): + if effective_config.get('interfaces', {}).get(interface_type): + for iface, iface_config in ( + config.get('interfaces', {}).get(interface_type, {}).items() + ): + # filter unsupported config nodes + if interface_type == 'ethernet': + iface_filter_eth(conf, iface) + set_dependents(dependency, conf, iface) + + # Save important info about all interfaces that cannot be retrieved later + # Add new interfaces (only if they are first time seen in a config) + for iface, iface_config in config.get('settings', {}).get('interface', {}).items(): + if iface not in effective_config.get('settings', {}).get('interface', {}): + eth_ifaces_persist[iface] = { + 'original_driver': config['settings']['interface'][iface][ + 'kernel_module' + ], + } + eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface) + eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface) + + # Get kernel settings for hugepages + kernel_memory_settings = conf.get_config_dict( + ['system', 'option', 'kernel', 'memory'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + config['kernel_memory_settings'] = kernel_memory_settings + + # Return to config dictionary + config['persist_config'] = eth_ifaces_persist + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if not config or ('removed_ifaces' in config and 'settings' not in config): + return None + + if 'settings' not in config: + raise ConfigError('"settings interface" is required but not set!') + + if 'interface' not in config['settings']: + raise ConfigError('"settings interface" is required but not set!') + + # check if the system meets minimal requirements + verify_vpp_minimum_cpus() + verify_vpp_minimum_memory() + + # check if Ethernet interfaces exist + ethernet_ifaces = Section.interfaces('ethernet') + for iface in config['settings']['interface'].keys(): + if iface not in ethernet_ifaces: + raise ConfigError(f'Interface {iface} does not exist or is not Ethernet!') + + # Resource usage checks + workers = 0 + + if 'cpu' in config['settings']: + cpu_settings = config['settings']['cpu'] + + # Check if there are enough CPU cores to skip according to config + if 'skip_cores' in cpu_settings: + skip_cores = int(cpu_settings['skip_cores']) + verify_vpp_settings_cpu_skip_cores(skip_cores) + + # Check whether the workers and corelist-workers are configured properly + verify_vpp_settings_cpu_and_corelist_workers(cpu_settings) + + # Check if there are enough CPU cores to add workers + if 'workers' in cpu_settings: + workers = verify_vpp_settings_cpu_workers(cpu_settings) + + if 'main_core' in cpu_settings: + verify_vpp_cpu_main_core(cpu_settings) + + # Check the CPU main core not falling to the corelist-workers + if 'corelist_workers' in cpu_settings: + workers = verify_vpp_settings_cpu_corelist_workers(cpu_settings) + + if 'workers' in config['settings']['nat44']: + verify_vpp_nat44_workers( + workers=workers, nat44_workers=config['settings']['nat44']['workers'] + ) + + # Check if available memory is enough for current VPP config + verify_vpp_memory(config) + + if 'max_map_count' in config['settings'].get('host_resources', {}): + verify_vpp_host_resources(config) + + if 'statseg' in config['settings']: + verify_vpp_statseg_size(config['settings']) + + # ensure DPDK/XDP settings are properly configured + for iface, iface_config in config['settings']['interface'].items(): + # check if selected driver is supported, but only for new interfaces + if iface not in config.get('effective', {}).get('settings', {}).get( + 'interface', {} + ): + if not verify_dev_driver(iface, iface_config['driver']): + raise ConfigError( + f'Driver {iface_config["driver"]} is not compatible with interface {iface}!' + ) + if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config: + if iface_config['xdp_options']['num_rx_queues'] != 'all': + Warning(f'Not all RX queues will be connected to VPP for {iface}!') + + if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config: + raise ConfigError('DPDK options are not applicable for XDP driver!') + + if iface_config['driver'] == 'dpdk' and 'xdp_options' in iface_config: + raise ConfigError('XDP options are not applicable for DPDK driver!') + + if iface_config['driver'] == 'dpdk' and 'dpdk_options' in iface_config: + if 'num_rx_queues' in iface_config['dpdk_options']: + rx_queues = int(iface_config['dpdk_options']['num_rx_queues']) + verify_vpp_interfaces_dpdk_num_queues( + qtype='receive', num_queues=rx_queues, workers=workers + ) + + if 'num_tx_queues' in iface_config['dpdk_options']: + tx_queues = int(iface_config['dpdk_options']['num_tx_queues']) + verify_vpp_interfaces_dpdk_num_queues( + qtype='transmit', num_queues=tx_queues, workers=workers + ) + + # RX-mode verification + rx_mode = iface_config.get('rx_mode') + if rx_mode and rx_mode != 'polling': + # By default drivers operate in polling mode. Not all NIC drivers support + # RX mode interrupt and adaptive + driver = config.get('persist_config').get(iface).get('original_driver') + if ( + driver not in drivers_support_interrupt + or iface_config['driver'] not in drivers_support_interrupt[driver] + ): + raise ConfigError( + f'RX mode {rx_mode} is not supported for interface {iface}' + ) + + # check GRE tunnels as part of the bridge, only tunnel-type teb is allowed + # set vpp interfaces bridge br1 member interface gre1 + # set vpp interfaces gre gre1 tunnel-type teb + if 'interfaces' in config: + if 'bridge' in config['interfaces']: + for iface, iface_config in config['interfaces']['bridge'].items(): + if 'member' in iface_config: + for member in iface_config['member'].get('interface', []): + if member.startswith('gre'): + if ( + 'gre' in config['interfaces'] + and config['interfaces']['gre'] + .get(member, {}) + .get('tunnel_type') + != 'teb' + ): + raise ConfigError( + f'Only tunnel-type teb is allowed for GRE interfaces in bridge {iface}' + ) + + # Only one multipoint GRE tunnel is allowed from the same source address + # set vpp interfaces gre gre0 mode 'point-to-multipoint' + # set vpp interfaces gre gre0 remote '0.0.0.0' + # set vpp interfaces gre gre0 source-address '192.0.2.1' + # set vpp interfaces gre gre1 mode 'point-to-multipoint' + # set vpp interfaces gre gre1 remote '0.0.0.0' + # set vpp interfaces gre gre1 source-address '192.0.2.1' + if 'gre' in config['interfaces']: + for iface, iface_config in config['interfaces']['gre'].items(): + if iface_config['mode'] == 'point-to-multipoint': + for other_iface, other_iface_config in config['interfaces'][ + 'gre' + ].items(): + if ( + other_iface_config['mode'] == 'point-to-multipoint' + and other_iface_config['source_address'] + == iface_config['source_address'] + and iface != other_iface + ): + raise ConfigError( + 'Only one multipoint GRE tunnel is allowed from the same source address' + ) + + # Check if deleted interfaces are not xconnect memebrs + for iface_config in config.get('removed_ifaces', []): + if iface_config['iface_name'] in config.get('xconn_members', {}): + raise ConfigError( + f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed' + ) + + +def generate(config): + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + render(service_conf, 'vpp/startup.conf.j2', config['settings']) + render(systemd_override, 'vpp/override.conf.j2', config) + + # apply sysctl values + # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf + # vm.nr_hugepages are now configured in section + # 'set system option kernel memory hugepage-size 2M hugepage-count <count>' + sysctl_config: dict[str, str] = { + 'vm.max_map_count': config['settings']['host_resources']['max_map_count'], + 'vm.hugetlb_shm_group': '0', + 'kernel.shmmax': config['settings']['host_resources']['shmmax'], + } + # we do not want to lower current values + for sysctl_key, sysctl_value in sysctl_config.items(): + # perform check only for quantitative params + if sysctl_key == 'vm.hugetlb_shm_group': + pass + current_value = sysctl_read(sysctl_key) + if int(current_value) > int(sysctl_value): + sysctl_config[sysctl_key] = current_value + + if not sysctl_apply(sysctl_config): + raise ConfigError('Cannot configure sysctl parameters for VPP') + + return None + + +def initialize_interface(iface, driver, iface_config) -> None: + # DPDK - rescan PCI to use a proper driver + if driver == 'dpdk' and iface_config['original_driver'] not in not_pci_drv: + control_host.pci_rescan(iface_config['dev_id']) + # rename to the proper name + iface_new_name: str = control_host.get_eth_name(iface_config['dev_id']) + control_host.rename_iface(iface_new_name, iface) + + # XDP - rename an interface, disable promisc and XDP + if driver == 'xdp': + control_host.set_promisc(f'defunct_{iface}', 'off') + control_host.rename_iface(f'defunct_{iface}', iface) + control_host.xdp_remove(iface) + + # Rename Mellanox NIC to a normal name + try: + if control_host.get_eth_driver(f'defunct_{iface}') == 'mlx5_core': + control_host.rename_iface(f'defunct_{iface}', iface) + except FileNotFoundError: + pass + + # Replace a driver with original for VMBus interfaces and rename it + if driver == 'dpdk' and iface_config['original_driver'] in override_drivers: + control_host.override_driver(iface_config['bus_id'], iface_config['dev_id']) + iface_new_name: str = control_host.get_eth_name(iface_config['dev_id']) + control_host.rename_iface(iface_new_name, iface) + + +def apply(config): + # Open persistent config + # It is required for operations with interfaces + persist_config = JSONStorage('vpp_conf') + if not config or ('removed_ifaces' in config and 'settings' not in config): + # Cleanup persistent config + persist_config.delete() + # And stop the service + call(f'systemctl stop {service_name}.service') + else: + # Some interfaces required extra preparation before VPP can be started + if 'settings' in config and 'interface' in config.get('settings'): + for iface, iface_config in config['settings']['interface'].items(): + if iface_config['driver'] == 'dpdk': + # ena interfaces require noiommu mode + if iface_config['kernel_module'] == 'ena': + control_host.unsafe_noiommu_mode(True) + + if iface_config['kernel_module'] in override_drivers: + control_host.override_driver( + config['persist_config'][iface]['bus_id'], + config['persist_config'][iface]['dev_id'], + override_drivers[iface_config['kernel_module']], + ) + + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + + # Initialize interfaces removed from VPP + for iface in config.get('removed_ifaces', []): + initialize_interface( + iface['iface_name'], + iface['driver'], + config['persist_config'][iface['iface_name']], + ) + + # Remove what is not in the config anymore + if iface['iface_name'] not in config.get('settings', {}).get('interface', {}): + del config['persist_config'][iface['iface_name']] + + if 'settings' in config and 'interface' in config.get('settings'): + # connect to VPP + # must be performed multiple attempts because API is not available + # immediately after the service restart + try: + vpp_control = VPPControl(attempts=20, interval=500) + + # preconfigure LCP plugin + if 'ignore_kernel_routes' in config.get('settings', {}).get('lcp', {}): + vpp_control.cli_cmd('lcp param route-no-paths off') + else: + vpp_control.cli_cmd('lcp param route-no-paths on') + # add interfaces + iproute = IPRoute() + for iface, iface_config in config['settings']['interface'].items(): + # promisc option for DPDK interfaces + if iface_config['driver'] == 'dpdk': + if 'promisc' in iface_config['dpdk_options']: + if_index = vpp_control.get_sw_if_index(iface) + vpp_control.api.sw_interface_set_promisc( + sw_if_index=if_index, promisc_on=True + ) + # add XDP interfaces + if iface_config['driver'] == 'xdp': + control_host.rename_iface(iface, f'defunct_{iface}') + vpp_control.xdp_iface_create( + host_if=f'defunct_{iface}', + name=iface, + **iface_config['xdp_api_params'], + ) + # replicate MAC address of a real interface + real_mac = control_host.get_eth_mac(f'defunct_{iface}') + vpp_control.set_iface_mac(iface, real_mac) + if 'promisc' in iface_config['xdp_options']: + control_host.set_promisc(f'defunct_{iface}', 'on') + control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') + # Rename Mellanox interfaces to hide them and create LCP properly + if ( + iface in Section.interfaces() + and control_host.get_eth_driver(iface) == 'mlx5_core' + ): + control_host.rename_iface(iface, f'defunct_{iface}') + control_host.set_status(f'defunct_{iface}', 'up') + control_host.flush_ip(f'defunct_{iface}') + # Create lcp + if iface not in Section.interfaces(): + vpp_control.lcp_pair_add(iface, iface) + + # For unknown reasons, if multiple interfaces later try to be + # initialized by configuration scripts, some of them may stuck + # in an endless UP/DOWN loop + # We found two workarounds - pause initialization (requires + # main code modifications). + # And this one + dev_index = iproute.link_lookup(ifname=iface)[0] + iproute.link('set', index=dev_index, state='up') + + # Set rx-mode. Should be configured after interface state set to UP + rx_mode = iface_config.get('rx_mode') + if rx_mode: + # to hardware side + vpp_control.iface_rxmode(iface, rx_mode) + # to kernel side + lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get( + 'vpp_name_kernel' + ) + vpp_control.iface_rxmode(lcp_name, rx_mode) + + # Syncronize routes via LCP + vpp_control.lcp_resync() + + # NAT44 settings + nat44_settings = config['settings'].get('nat44', {}) + + enable_forwarding = True + if 'no_forwarding' in nat44_settings: + enable_forwarding = False + vpp_control.enable_disable_nat44_forwarding(enable_forwarding) + + vpp_control.set_nat44_session_limit( + int(nat44_settings.get('session_limit')) + ) + + if nat44_settings.get('workers'): + bitmask = 0 + for worker_range in nat44_settings['workers']: + worker_numbers = worker_range.split('-') + for wid in range( + int(worker_numbers[0]), int(worker_numbers[-1]) + 1 + ): + bitmask |= 1 << wid + vpp_control.set_nat_workers(bitmask) + + except (VPPIOError, VPPValueError) as e: + # if cannot connect to VPP or an error occurred then + # we need to stop vpp service and initialize interfaces + call(f'systemctl stop {service_name}.service') + for iface, iface_config in config['settings']['interface'].items(): + initialize_interface( + iface, iface_config['driver'], config['persist_config'][iface] + ) + + raise ConfigError( + f'An error occurred: {e}. ' + 'VPP service will be restarted with the previous configuration' + ) + + # Save persistent config + if 'persist_config' in config and config['persist_config']: + persist_config.write('eth_ifaces', config['persist_config']) + + # reinitialize interfaces, but not during the first boot + if boot_configuration_complete(): + call_dependents() + + +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/vpp_acl.py b/src/conf_mode/vpp_acl.py new file mode 100644 index 000000000..d6a9150d1 --- /dev/null +++ b/src/conf_mode/vpp_acl.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import ipaddress + +from vyos import ConfigError + +from vyos.configdiff import Diff +from vyos.configdict import node_changed +from vyos.config import Config +from vyos.utils.network import get_protocol_by_name + +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp.acl import Acl + + +# TCP flag names to bit values +TCP_FLAGS = { + 'FIN': 0x01, + 'SYN': 0x02, + 'RST': 0x04, + 'PSH': 0x08, + 'ACK': 0x10, + 'URG': 0x20, + 'ECN': 0x40, + 'CWR': 0x80, +} + +# ACL action flags +action_map = { + 'deny': 0, + 'permit': 1, + 'permit-reflect': 2, +} + + +def get_tcp_mask_value(set_flags, unset_flags): + mask = 0 + value = 0 + + for flag in set_flags + unset_flags: + bit = TCP_FLAGS.get(flag.upper()) + mask |= bit + if flag in set_flags: + value |= bit + + return mask, value + + +def get_port_first_last(port_range, protocol): + first_port = 0 + last_port = 65535 + if not port_range: + if protocol in ['icmp', 'ipv6-icmp']: + last_port = 255 + elif '-' not in port_range: + first_port = last_port = port_range + else: + first_port, last_port = port_range.split('-') + return int(first_port), int(last_port) + + +def create_ip_rules_list(rules): + rules_list = [] + for rule in rules.values(): + r = { + 'is_permit': action_map[rule.get('action')], + 'src_prefix': rule.get('source', {}).get('prefix', ''), + 'dst_prefix': rule.get('destination', {}).get('prefix', ''), + 'proto': ( + int(get_protocol_by_name(rule.get('protocol'))) + if rule.get('protocol') != 'all' + else 0 + ), + } + + tcp_flags = rule.get('tcp_flags', {}) + set_flags = [flag for flag in tcp_flags if flag != 'not'] + unet_flags = list(tcp_flags.get('not', {}).keys()) + tcp_mask, tcp_value = get_tcp_mask_value(set_flags, unet_flags) + r['tcp_flags_mask'] = tcp_mask + r['tcp_flags_value'] = tcp_value + + src_ports = rule.get('source', {}).get('port') + src_first_port, src_last_port = get_port_first_last( + src_ports, rule.get('protocol') + ) + r['srcport_or_icmptype_first'] = src_first_port + r['srcport_or_icmptype_last'] = src_last_port + + dst_ports = rule.get('destination', {}).get('port') + dst_first_port, dst_last_port = get_port_first_last( + dst_ports, rule.get('protocol') + ) + r['dstport_or_icmpcode_first'] = dst_first_port + r['dstport_or_icmpcode_last'] = dst_last_port + + rules_list.append(r) + + return rules_list + + +def create_macip_rules_list(rules): + rules_list = [] + for rule in rules.values(): + r = { + 'is_permit': action_map[rule.get('action')], + 'src_prefix': rule.get('prefix', ''), + 'src_mac': rule.get('mac_address', ''), + 'src_mac_mask': rule.get('mac_mask', ''), + } + rules_list.append(r) + + return rules_list + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'acl'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + changed_ip_ifaces = node_changed( + conf, + base + ['ip', 'interface'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_macip_ifaces = node_changed( + conf, + base + ['macip', 'interface'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + config.update( + { + 'changed_ip_ifaces': changed_ip_ifaces, + 'changed_macip_ifaces': changed_macip_ifaces, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + for acl_type in ['ip', 'macip']: + if acl_type in config: + acl = config.get(acl_type) + if 'tag_name' not in acl: + raise ConfigError(f'"tag-name" is required for "acl {acl_type}"') + + for acl_name, acl_config in acl.get('tag_name').items(): + if 'rule' not in acl_config: + raise ConfigError(f'Rules must be configured for ACL {acl_name}') + + for rule, rule_config in acl_config.get('rule').items(): + err_msg = f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:' + if 'action' not in rule_config: + raise ConfigError(f'{err_msg} action must be defined') + + for iface, iface_config in acl.get('interface', {}).items(): + if iface not in config.get('vpp_ifaces'): + raise ConfigError( + f'{iface} must be a VPP interface for ACL interface' + ) + + if 'ip' in config: + acl = config.get('ip') + for acl_name, acl_config in acl.get('tag_name').items(): + for rule, rule_config in acl_config.get('rule').items(): + err_msg = ( + f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:' + ) + + # verify IPv4 and IPv6 address family + src_prefix = rule_config.get('source', {}).get('prefix') + dst_prefix = rule_config.get('destination', {}).get('prefix') + src = ipaddress.ip_network(src_prefix) if src_prefix else None + dst = ipaddress.ip_network(dst_prefix) if dst_prefix else None + + if src and dst: + if src.version != dst.version: + raise ConfigError( + f'{err_msg} source and destination prefixes must be from the same IP family' + ) + elif src or dst: + family = src.version if src else dst.version + if family == 6: + raise ConfigError( + f'{err_msg} both source and destination prefixes must be defined for IPv6' + ) + + # verify protocol + protocol = rule_config.get('protocol') + if protocol != 'all': + proto = get_protocol_by_name(protocol) + if not isinstance(proto, int) and ( + not proto.isdigit() or int(proto) > 147 + ): + raise ConfigError( + f'{err_msg} protocol name {protocol} is not valid' + ) + + # verify TCP flags + if 'tcp_flags' in rule_config: + if rule_config.get('protocol') != 'tcp': + raise ConfigError( + f'{err_msg} protocol must be tcp when specifying tcp flags' + ) + + not_flags = rule_config.get('tcp_flags').get('not', []) + if not_flags: + duplicates = [ + flag + for flag in rule_config.get('tcp_flags') + if flag in not_flags + ] + if duplicates: + raise ConfigError( + f'{err_msg} cannot match a tcp flag as set and not set: {duplicates}' + ) + + for iface, iface_config in acl.get('interface', {}).items(): + if not any(key in iface_config for key in ('input', 'output')): + raise ConfigError( + f'Please specify direction input/output for interface {iface}' + ) + + for direction in ['input', 'output']: + if direction in iface_config: + iface_acl = iface_config.get(direction) + if 'acl_tag' not in iface_acl: + raise ConfigError( + f'"acl-tag" is required for {direction} interface {iface}' + ) + + used_names = [] + for tag, tag_conf in iface_acl.get('acl_tag').items(): + if 'tag_name' not in tag_conf: + raise ConfigError( + f'"tag-name" is required for {direction} interface {iface} with acl-tag {tag}' + ) + name = tag_conf.get('tag_name') + if name not in acl.get('tag_name').keys(): + raise ConfigError( + f'ACL with tag-name {name} does not exist. ' + f'Cannot use it for {direction} interface {iface}' + ) + if name in used_names: + raise ConfigError( + f'ACL with tag-name {name} is already used for {direction} interface {iface}' + ) + used_names.append(name) + + if 'macip' in config: + acl = config.get('macip') + for iface, iface_config in acl.get('interface', {}).items(): + if 'tag_name' not in iface_config: + raise ConfigError(f'"tag-name" is required for interface {iface}') + name = iface_config.get('tag_name') + if name not in acl.get('tag_name').keys(): + raise ConfigError( + f'ACL with tag-name {name} does not exist. Cannot use it for interface {iface}' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + acl = Acl() + + if 'effective' in config: + # Delete ACL ip + if 'ip' in config.get('effective'): + remove_config_ip = config.get('effective').get('ip') + + # Delete ACL interfaces + for interface in config.get('changed_ip_ifaces'): + acl.delete_acl_interface(interface) + + # Delete ACLs + for acl_name in remove_config_ip.get('tag_name'): + if acl_name not in config.get('ip', {}).get('tag_name', {}): + acl.delete_acl(acl_name) + + # Delete ACL macip + if 'macip' in config.get('effective'): + remove_config_macip = config.get('effective').get('macip') + + # Delete ACL interfaces + for interface in config.get('changed_macip_ifaces'): + acl.delete_acl_macip_interface(interface) + + # Delete ACL macip + for acl_name in remove_config_macip.get('tag_name'): + if acl_name not in config.get('macip', {}).get('tag_name', {}): + acl.delete_acl_macip(acl_name) + + if 'remove' in config: + return None + + # Add or replace ACL ip + config_ip = config.get('ip', {}) + for acl_name in config_ip.get('tag_name', {}): + rules = create_ip_rules_list( + config_ip.get('tag_name').get(acl_name).get('rule') + ) + acl.add_replace_acl(acl_name, rules) + + for iface, iface_config in config_ip.get('interface', {}).items(): + input_tags = [ + v['tag_name'] + for v in iface_config.get('input', {}).get('acl_tag', {}).values() + ] + output_tags = [ + v['tag_name'] + for v in iface_config.get('output', {}).get('acl_tag', {}).values() + ] + acl.add_acl_interface(iface, input_tags, output_tags) + + # Add or replace ACL macip + config_macip = config.get('macip', {}) + for acl_name in config_macip.get('tag_name', {}): + rules = create_macip_rules_list( + config_macip.get('tag_name').get(acl_name).get('rule') + ) + acl.add_replace_acl_macip(acl_name, rules) + + for iface, iface_config in config_macip.get('interface', {}).items(): + acl.add_acl_macip_interface(iface, iface_config.get('tag_name')) + + +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/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py new file mode 100644 index 000000000..c9aa6276c --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bonding.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos import ConfigError + +from vyos.vpp.interface import BondInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_xconnect_interface, + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_list, cli_ifaces_lcp_kernel_list + + +def _get_bond_mode(mode_name: str) -> int: + """Convert VyOS CLI name bonding mode to VPP compatible""" + mode_mapping = { + 'round-robin': 1, + 'active-backup': 2, + 'xor-hash': 3, + 'broadcast': 4, + '802.3ad': 5, + } + + return mode_mapping.get(mode_name, 5) + + +def _get_bond_lb(lb_name: str) -> int: + """Convert VyOS CLI name bonding load balance to VPP compatible""" + lb_mapping = { + 'layer2': 0, + 'layer2+3': 2, + 'layer3+4': 1, + } + + return lb_mapping.get(lb_name, 5) + + +def get_config(config=None) -> dict: + """Get Bonding interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bonding interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bonding'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # convert values to VPP compatible + if 'mode' in config: + config['mode'] = _get_bond_mode(config['mode']) + if 'hash_policy' in config: + config['hash_policy'] = _get_bond_lb(config['hash_policy']) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if conf.exists(base + [ifname, 'kernel-interface']): + if effective_config.get('kernel_interface') or __name__ != '__main__': + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove_vpp' in config: + return None + + verify_vpp_remove_kernel_interface(config) + verify_vpp_remove_xconnect_interface(config) + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for bonding') + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # remove old members + if 'effective' in config: + members = config['effective'].get('member', {}).get('interface', []) + + kernel_interface = config['effective'].get('kernel_interface', '') + i = BondInterface(ifname, kernel_interface=kernel_interface) + for member in members: + i.detach_member(interface=member) + + if 'kernel_interface' in config['effective']: + i.kernel_delete() + # Delete bonding interface + i.delete() + + if 'remove' in config: + return None + + # Create a new one + mode = config.get('mode') + lb = config.get('hash_policy') + members = config.get('member', {}).get('interface', []) + mac = config.get('mac', '') + kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' + + i = BondInterface(ifname, mode, lb, mac, kernel_interface, state) + i.add() + # Add members to bond + if members: + for member in members: + i.add_member(interface=member) + + call_dependents() + + 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/vpp_interfaces_bridge.py b/src/conf_mode/vpp_interfaces_bridge.py new file mode 100644 index 000000000..4272db616 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_bridge.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos import ConfigError +from vyos.vpp.interface import BridgeInterface +from vyos.vpp.utils import iftunnel_transform + + +def get_config(config=None) -> dict: + """Get Bridge interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'bridge'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # determine which members have been removed + interfaces_removed = node_changed(conf, base + [ifname, 'member', 'interface']) + if interfaces_removed: + config['members_removed'] = interfaces_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Check if interface exists in vpp before adding to bridge-domain + + allowed_prefixes = ('bond', 'gre', 'geneve', 'lo', 'vxlan') + + if 'member' in config: + bvi_exists = False + for member, member_config in ( + config.get('member', {}).get('interface', {}).items() + ): + # Check if the interface exists in VPP settings or starts with allowed prefixes + if not ( + member in config.get('vpp_interfaces', {}) + or member.startswith(allowed_prefixes) + ): + raise ConfigError( + f"Interface '{member}' not found in 'vpp settings interface' or does not start with allowed prefixes {allowed_prefixes}" + ) + + # Check if BVI is already defined, only one BVI per bridge domain is allowed + if 'bvi' in member_config: + if bvi_exists: + raise ConfigError("Only one BVI per bridge domain is allowed") + if not member.startswith('lo'): + raise ConfigError("BVI can only be defined on loopback interface") + bvi_exists = True + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # vxlan10 in the vpp is vxlan_tunnel10 + interface_transform_filter = ('geneve', 'vxlan') + # update members + if 'members_removed' in config: + i = BridgeInterface(ifname) + for member in config.get('members_removed'): + if member.startswith(interface_transform_filter): + member = iftunnel_transform(member) + if member.startswith('lo'): + # interface name in VPP is loopX + member = member.replace('lo', 'loop') + elif member.startswith('bond'): + # interface name in VPP is BondEthernetX + member = member.replace('bond', 'BondEthernet') + i.detach_member(member=member) + + # Delete bridge domain + if 'effective' in config: + ifname = config.get('ifname') + i = BridgeInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add bridge domain + members = config.get('member', {}).get('interface', '') + i = BridgeInterface(ifname) + i.add() + # Add members to bridge + if members: + br = BridgeInterface(ifname) + port_type = 0 + for member, member_config in members.items(): + if member.startswith(interface_transform_filter): + member = iftunnel_transform(member) + if member.startswith('lo'): + # interface name in VPP is loopX + member = member.replace('lo', 'loop') + if 'bvi' in member_config: + port_type = 1 + elif member.startswith('bond'): + # interface name in VPP is BondEthernetX + member = member.replace('bond', 'BondEthernet') + + br.add_member(member=member, port_type=port_type) + # set default port type 0 (not BVI) + port_type = 0 + + 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/vpp_interfaces_ethernet.py b/src/conf_mode/vpp_interfaces_ethernet.py new file mode 100644 index 000000000..f56cadc3e --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ethernet.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdep import set_dependents +from vyos import ConfigError +from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface + + +def get_config(config=None) -> dict: + """Get Ethernet interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Ethernet interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ethernet'] + base_interfaces_xconnect = ['vpp', 'interfaces', 'xconnect'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not conf.exists(base + [ifname]): + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + config.update({'remove': effective_config}) + else: + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get global 'vpp interfaces xconnect' + config['vpp_interfaces_xconnect'] = conf.get_config_dict( + base_interfaces_xconnect, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # Dependency + for xcon_name, xcon_config in config.get('vpp_interfaces_xconnect').items(): + for member_name in xcon_config.get('member', {}).get('interface', []): + if member_name == ifname: + set_dependents('vpp_interfaces_xconnect', conf, xcon_name) + + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + verify_vpp_remove_xconnect_interface(config) + if 'remove' in config: + return None + + +def generate(config): + pass + + +def apply(config): + # Delete interface + if 'remove' in config: + pass + # remove_config = config.get('remove') + # ifname = config.get('ifname') + # src_addr = remove_config.get('source_address') + # dst_addr = remove_config.get('remote') + # vni = int(remove_config.get('vni')) + # v = VXLANInterface(ifname, src_addr, dst_addr, vni) + # v.delete() + else: + pass + # ifname = config.get('ifname') + # kernel_interface = config.get('kernel_interface', '') + # v = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + # v.add() + 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/vpp_interfaces_geneve.py b/src/conf_mode/vpp_interfaces_geneve.py new file mode 100644 index 000000000..12fac83fb --- /dev/null +++ b/src/conf_mode/vpp_interfaces_geneve.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GeneveInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None) -> dict: + """Get Geneve interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + recursive_defaults (bool, optional): Include recursive defaults + Returns: + dict: Geneve interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'geneve'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + config['ifname'] = ifname + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces geneve geneveX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # Change 'vpp interfaces geneve geneveX kernel-interface vpp-tunX' + # => 'vpp interfaces geneve geneveX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = GeneveInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + i = GeneveInterface(ifname, src_addr, dst_addr, vni, kernel_interface) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + 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/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py new file mode 100644 index 000000000..5ec6feeb0 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_gre.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import GREInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get GRE interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: GRE interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'gre'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + config['ifname'] = ifname + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces gre greX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote', 'mode', 'tunnel_type'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + + # check multipoint mode + if config.get('mode') == 'point-to-multipoint': + # For multipoint mode, remote IP must be 0.0.0.0 + if config.get('remote') != '0.0.0.0': + raise ConfigError('For point-to-multipoint mode, remote must be 0.0.0.0') + + # Change 'vpp interfaces gre greX kernel-interface vpp-tunX' + # => 'vpp interfaces gre greX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = GREInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + mode = config.get('mode') + tunnel_type = config.get('tunnel_type') + state = 'up' if 'disable' not in config else 'down' + i = GREInterface( + ifname, src_addr, dst_addr, tunnel_type, mode, kernel_interface, state + ) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + 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/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py new file mode 100644 index 000000000..430b7b334 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_ipip.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents + +from vyos.vpp.interface import IPIPInterface +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get IPIP interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: IPIP interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'ipip'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + if config.get('effective', {}).get('kernel_interface'): + iface = config.get('kernel_interface') + if iface: + if iface in config.get('vpp_kernel_interfaces'): + set_dependents('vpp_kernel_interface', conf, iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + config['ifname'] = ifname + + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces ipip ipipX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + # config removed + if 'remove' in config: + return None + + # source-address and remote are mandatory options + required_keys = {'source_address', 'remote'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + + # Change 'vpp interfaces ipip ipipX kernel-interface vpp-tunX' + # => 'vpp interfaces ipip ipipX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + i = IPIPInterface(ifname, src_addr, dst_addr) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' + i = IPIPInterface(ifname, src_addr, dst_addr, kernel_interface, state) + i.add() + + if 'kernel_interface' in config: + i.kernel_add() + + call_dependents() + + 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/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py new file mode 100644 index 000000000..92ba110f1 --- /dev/null +++ b/src/conf_mode/vpp_interfaces_loopback.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import LoopbackInterface +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_exists_kernel_interface, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list + + +def get_config(config=None) -> dict: + """Get Loopback interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Loopback interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'loopback'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # Dependency + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + config['ifname'] = ifname + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + verify_vpp_remove_kernel_interface(config) + + if 'remove' in config: + return None + + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + i = LoopbackInterface(ifname) + i.delete() + + if 'remove' in config: + return None + + # Add interface + kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' + i = LoopbackInterface(ifname, kernel_interface, state) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + 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/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py new file mode 100644 index 000000000..c4068818e --- /dev/null +++ b/src/conf_mode/vpp_interfaces_vxlan.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos import ConfigError + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdep import set_dependents, call_dependents +from vyos.template import is_interface + +from vyos.vpp.interface import VXLANInterface +from vyos.vpp.config_deps import deps_bridge_dict +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.config_verify import ( + verify_vpp_remove_kernel_interface, + verify_vpp_change_kernel_interface, + verify_vpp_remove_xconnect_interface, + verify_vpp_exists_kernel_interface, + verify_vpp_tunnel_source_address, +) +from vyos.vpp.utils import cli_ifaces_lcp_kernel_list, cli_ethernet_with_vifs_ifaces + + +def get_config(config=None) -> dict: + """Get VXLAN interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: VXLAN interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'vxlan'] + base_kernel_interfaces = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if effective_config: + config.update({'effective': effective_config}) + + if not conf.exists(base + [ifname]): + config['remove'] = True + + # Get global 'vpp kernel-interfaces' for verify + config['vpp_kernel_interfaces'] = conf.get_config_dict( + base_kernel_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + tmp = leaf_node_changed(conf, base + [ifname, 'kernel-interface']) + if tmp: + config['kernel_interface_removed'] = tmp + + # list of all kernel interfaces `vpp interface xxx kernel-interface xxx` + config['candidate_kernel_interfaces'] = cli_ifaces_lcp_kernel_list(conf) + + # list of all Ethernet interfaces with vifs + config['vpp_ether_vif_ifaces'] = cli_ethernet_with_vifs_ifaces(conf) + + # Dependency + config['xconn_members'] = deps_xconnect_dict(conf) + if ifname in config['xconn_members']: + for xconn_iface in config['xconn_members'][ifname]: + set_dependents('vpp_interfaces_xconnect', conf, xconn_iface) + + config['bridge_members'] = deps_bridge_dict(conf) + if ifname in config['bridge_members']: + for bridge_iface in config['bridge_members'][ifname]: + set_dependents('vpp_interfaces_bridge', conf, bridge_iface) + + if effective_config.get('kernel_interface'): + if conf.exists(base + [ifname, 'kernel-interface']): + iface = config.get('kernel_interface') + if conf.exists(['vpp', 'kernel-interfaces', iface]): + set_dependents('vpp_kernel_interface', conf, iface) + + # NAT dependency + if conf.exists(['vpp', 'nat44']): + set_dependents('vpp_nat', conf) + if conf.exists(['vpp', 'nat', 'cgnat']): + set_dependents('vpp_nat_cgnat', conf) + + # ACL dependency + if conf.exists(['vpp', 'acl']): + set_dependents('vpp_acl', conf) + + config['ifname'] = ifname + return config + + +def verify(config): + # No need to verify anything if vpp is removed + if 'remove_vpp' in config: + return None + + # Verify that removed kernel interface is not used in 'vpp kernel-interfaces'. + # vpp interfaces vxlan vxlanX kernel-interface vpp-tunX + # vpp kernel-interface vpp-tunX + verify_vpp_remove_kernel_interface(config) + + verify_vpp_remove_xconnect_interface(config) + + if 'remove' in config: + return None + + required_keys = {'source_address', 'remote', 'vni'} + if not all(key in config for key in required_keys): + missing_keys = required_keys - set(config.keys()) + raise ConfigError( + f"Required options are missing: {', '.join(missing_keys).replace('_', '-')}" + ) + + # verify source address and remote address + verify_vpp_tunnel_source_address(config) + if config.get('source_address') == config.get('remote'): + raise ConfigError('Remote address must not be the same as source address') + + # Change 'vpp interfaces vxlan greX kernel-interface vpp-tunX' + # => 'vpp interfaces gre vxlanX kernel-interface vpp-tunY' + # check if we have kernel interface config 'vpp kernel-interface vpp-tunX' + verify_vpp_change_kernel_interface(config) + verify_vpp_exists_kernel_interface(config) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + # Delete interface + if 'effective' in config: + remove_config = config.get('effective') + src_addr = remove_config.get('source_address') + dst_addr = remove_config.get('remote') + vni = int(remove_config.get('vni')) + i = VXLANInterface(ifname, src_addr, dst_addr, vni) + i.delete() + + if 'remove' in config: + return None + + # Add interface + src_addr = config.get('source_address') + dst_addr = config.get('remote') + vni = int(config.get('vni')) + kernel_interface = config.get('kernel_interface', '') + state = 'up' if 'disable' not in config else 'down' + i = VXLANInterface(ifname, src_addr, dst_addr, vni, kernel_interface, state) + i.add() + + # Add kernel-interface (LCP) if interface is not exist + if 'kernel_interface' in config and not is_interface(kernel_interface): + i.kernel_add() + + call_dependents() + + 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/vpp_interfaces_xconnect.py b/src/conf_mode/vpp_interfaces_xconnect.py new file mode 100644 index 000000000..220c1d44e --- /dev/null +++ b/src/conf_mode/vpp_interfaces_xconnect.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos import ConfigError +from vyos.vpp.config_deps import deps_xconnect_dict +from vyos.vpp.interface import XconnectInterface +from vyos.vpp.utils import cli_ifaces_list + + +def get_config(config=None) -> dict: + """Get Xconnect interface configuration + + Args: + config (vyos.config.Config, optional): The VyOS configuration dictionary + Returns: + dict: Bridge interface configuration + """ + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'interfaces', 'xconnect'] + vpp_interfaces = ['vpp', 'settings', 'interface'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + if effective_config: + config.update({'effective': effective_config}) + + # Get global vpp interfaces for verify + config['vpp_interfaces'] = conf.get_config_dict( + vpp_interfaces, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + config['xconn_members'] = deps_xconnect_dict(conf) + config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate') + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Xconnect requires 2 members + if len(config.get('member', {}).get('interface')) != 2: + raise ConfigError('Cross connect requires 2 members') + + # Member must belong to VPP + for iface in config.get('member', {}).get('interface', []): + if iface not in config['vpp_ifaces']: + raise ConfigError(f'{iface} must be a VPP interface for xconnect') + + # Each interface can belong only to one xconnect + for xconn_member, xconn_ifaces in config['xconn_members'].items(): + if len(xconn_ifaces) > 1: + raise ConfigError( + f'Interface {xconn_member} added to more than one xconnect: {xconn_ifaces}' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + + # Delete xconnect + if 'effective' in config: + remove_config = config.get('effective') + members = remove_config.get('member', {}).get('interface') + i = XconnectInterface(ifname, members=members) + i.del_l2_xconnect() + + if 'remove' in config: + return None + + # Add xconnect + members = config.get('member', {}).get('interface') + state = 'up' if 'disable' not in config else 'down' + i = XconnectInterface(ifname, members=members, state=state) + i.add_l2_xconnect() + + 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/vpp_kernel-interfaces.py b/src/conf_mode/vpp_kernel-interfaces.py new file mode 100644 index 000000000..f9354c964 --- /dev/null +++ b/src/conf_mode/vpp_kernel-interfaces.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.configdict import node_changed +from vyos import ConfigError +from vyos.ifconfig import Interface +from vyos.utils.network import interface_exists +from vyos.utils.process import call +from vyos.vpp import VPPControl + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'kernel-interfaces'] + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # Get config_dict with default values + config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dicitonary per interface delete + if __name__ == '__main__': + effective_config = conf.get_config_dict( + base + [ifname], + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + # if a file was started as dependency, we are starting from empty config + else: + effective_config = {} + + if effective_config: + config.update({'effective': effective_config}) + + address_removed = leaf_node_changed(conf, base + [ifname, 'address']) + if address_removed: + config['address_removed'] = address_removed + + description_removed = leaf_node_changed(conf, base + [ifname, 'description']) + if description_removed: + config['description_removed'] = {} + + vlans_removed = node_changed(conf, base + [ifname, 'vif']) + if vlans_removed: + config['vlans_removed'] = vlans_removed + + config['ifname'] = ifname + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + # Interface must exists before it is configured + if not interface_exists(config['ifname']): + raise ConfigError( + f'Interface {config["ifname"]} must be created before using in configuration' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + ifname = config.get('ifname') + i = Interface(ifname) + # update/remove addresses + if 'address_removed' in config: + for address in config['address_removed']: + i.del_addr(address) + # remove description + if 'description_removed' in config: + i.set_alias('') + + # remove VLANs + if 'vlans_removed' in config: + for vlan in config['vlans_removed']: + call(f'ip link del dev {ifname}.{vlan}') + + # Delete + if 'remove' in config: + pass + else: + # Add address + if 'address' in config: + for address in config['address']: + i.add_addr(address) + # Set MTU + if 'mtu' in config: + i.set_mtu(config.get('mtu')) + # Set description + if 'description' in config: + i.set_alias(config.get('description')) + # Admin state down + if 'disable' in config: + i.set_admin_state('down') + else: + i.set_admin_state('up') + + for vlan, vlan_config in config.get('vif', {}).items(): + if vlan not in config.get('effective', {}).get('vif', {}).keys(): + call( + f'ip link add link {ifname} name {ifname}.{vlan} type vlan id {vlan}' + ) + call(f'ip link set dev {ifname}.{vlan} up') + v = Interface(f'{ifname}.{vlan}') + + # VLAN address + addresses_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('address', []) + ) + addresses_candidate = vlan_config.get('address', []) + + for ipaddr in addresses_effective: + if ipaddr not in addresses_candidate: + v.del_addr(ipaddr) + for ipaddr in addresses_candidate: + if ipaddr not in addresses_effective: + v.add_addr(ipaddr) + + # VLAN description + description_effective = ( + config.get('effective', {}) + .get('vif', {}) + .get(vlan, {}) + .get('description', '') + ) + description_candidate = vlan_config.get('description', '') + + if description_candidate: + v.set_alias(description_candidate) + elif description_effective and not description_candidate: + v.set_alias('') + + v.set_admin_state('up') + + # Set rx-mode + rx_mode = config.get('rx_mode') + if rx_mode: + vpp_control = VPPControl() + lcp_name = vpp_control.lcp_pair_find(kernel_name=ifname).get('vpp_name_kernel') + vpp_control.iface_rxmode(lcp_name, rx_mode) + + 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/vpp_nat.py b/src/conf_mode/vpp_nat.py new file mode 100644 index 000000000..2c0298902 --- /dev/null +++ b/src/conf_mode/vpp_nat.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import ipaddress + +from vyos import ConfigError + +from vyos.configdiff import Diff +from vyos.configdict import node_changed +from vyos.config import Config +from vyos.utils.network import get_interface_address + +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp.nat.nat44 import Nat44 + + +protocol_map = { + 'all': 0, + 'icmp': 1, + 'tcp': 6, + 'udp': 17, +} + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat44'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + return config + + config_changed = node_changed( + conf, + base, + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_static_rules = node_changed( + conf, + base + ['static', 'rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_exclude_rules = node_changed( + conf, + base + ['exclude', 'rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + if not config_changed: + changed_static_rules = list(config.get('static', {}).get('rule', {}).keys()) + changed_exclude_rules = list(config.get('exclude', {}).get('rule', {}).keys()) + + config.update( + { + 'changed_static_rules': changed_static_rules, + 'changed_exclude_rules': changed_exclude_rules, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if conf.exists(['vpp', 'settings', 'nat44', 'timeout']): + timeouts = conf.get_config_dict( + ['vpp', 'settings', 'nat44', 'timeout'], + key_mangling=('-', '_'), + with_defaults=True, + ) + config.update(timeouts) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def convert_range_to_list_ips(address_range) -> list: + """Converts IP range to a list of IPs . + + Example: + % ip = IPOperations('192.0.0.1-192.0.2.5') + % ip.convert_prefix_to_list_ips() + ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5'] + """ + if '-' in address_range: + start_ip, end_ip = address_range.split('-') + start_ip = ipaddress.ip_address(start_ip) + end_ip = ipaddress.ip_address(end_ip) + return [ + str(ipaddress.ip_address(ip)) + for ip in range(int(start_ip), int(end_ip) + 1) + ] + else: + return [address_range] + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + if 'interface' not in config: + raise ConfigError('Interfaces must be configured for NAT44') + + required_keys = {'inside', 'outside'} + missing_keys = required_keys - set(config['interface'].keys()) + if missing_keys: + raise ConfigError( + f'Both inside and outside interfaces must be configured. Please add: {", ".join(missing_keys)}' + ) + + for interface in config['interface']['inside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for inside NAT interface' + ) + for interface in config['interface']['outside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for outside NAT interface' + ) + + if not config.get('address_pool', {}).get('translation') and not config.get( + 'static', {} + ).get('rule'): + raise ConfigError('"address-pool translation" or "static rule" is required') + + addresses_translation = [] + addresses_twice_nat = [] + if 'address_pool' in config: + address_pool = config.get('address_pool') + if 'translation' in address_pool: + if not address_pool['translation'].get('address') and not address_pool[ + 'translation' + ].get('interface'): + raise ConfigError( + '"address-pool translation" requires address or interface' + ) + + for address_range in address_pool['translation'].get('address', []): + addresses = convert_range_to_list_ips(address_range) + for address in addresses: + if address in addresses_translation: + raise ConfigError( + f'Address {address} is already in use in "address-pool translation address"' + ) + addresses_translation.append(address) + + for interface in address_pool['translation'].get('interface', []): + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for "address-pool translation interface"' + ) + iface_address = ( + get_interface_address(interface) + .get('addr_info', [])[0] + .get('local') + ) + addresses_translation.append(iface_address) + + if 'twice_nat' in address_pool: + if not address_pool['twice_nat'].get('address') and not address_pool[ + 'twice_nat' + ].get('interface'): + raise ConfigError( + '"address-pool twice-nat" requires address or interface' + ) + + for address_range in address_pool['twice_nat'].get('address', []): + addresses = convert_range_to_list_ips(address_range) + for address in addresses: + if address in addresses_twice_nat: + raise ConfigError( + f'Address {address} is already in use in "address-pool twice-nat address"' + ) + addresses_twice_nat.append(address) + + for interface in address_pool['twice_nat'].get('interface', []): + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for "address-pool twice-nat interface"' + ) + iface_address = ( + get_interface_address(interface) + .get('addr_info', [])[0] + .get('local') + ) + addresses_twice_nat.append(iface_address) + + if 'static' in config: + addresses_with_ports = set() + addresses_without_ports = set() + local_addresses = set() + + for rule, rule_config in config['static']['rule'].items(): + error_msg = f'Configuration error in static rule {rule}:' + + if not rule_config.get('local', {}).get('address'): + raise ConfigError(f'{error_msg} local settings require address') + + if not rule_config.get('external', {}).get('address'): + raise ConfigError(f'{error_msg} external settings require address') + + has_local_port = 'port' in rule_config.get('local', {}) + has_external_port = 'port' in rule_config.get('external', {}) + + if not has_external_port == has_local_port: + raise ConfigError( + f'{error_msg} source and destination ports must either ' + 'both be specified, or neither must be specified' + ) + + ext_address = rule_config['external']['address'] + port = rule_config['external'].get('port') + local_address = rule_config['local']['address'] + + if port: + pair = (ext_address, port) + if ( + pair in addresses_with_ports + or ext_address in addresses_without_ports + ): + raise ConfigError( + f'{error_msg} external address/port is already in use!' + ) + addresses_with_ports.add(pair) + if ext_address not in addresses_translation: + raise ConfigError( + f'{error_msg} external address {ext_address} is not in "address-pool translation"' + ) + + else: + if ext_address in addresses_without_ports or any( + addr == ext_address for addr, _ in addresses_with_ports + ): + raise ConfigError( + f'{error_msg} external address is already in use!' + ) + addresses_without_ports.add(ext_address) + + if local_address in local_addresses: + raise ConfigError( + f'{error_msg} local address {local_address} is already in use' + ) + local_addresses.add(local_address) + + options = rule_config.get('options', {}) + if all(key in options for key in ('twice_nat', 'self_twice_nat')): + raise ConfigError( + f'{error_msg} cannot set both options "twice-nat" and "self-twice-nat"' + ) + if any(key in options for key in ('twice_nat', 'self_twice_nat')): + if not has_local_port or rule_config['protocol'] == 'all': + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat options require port and protocol to be set' + ) + if not config.get('address_pool', {}).get('twice_nat'): + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat options require "address-pool twice-nat" to be set' + ) + if 'twice_nat_address' in options: + if not any(key in options for key in ('twice_nat', 'self_twice_nat')): + raise ConfigError( + f'{error_msg} twice-nat/self-twice-nat option required when twice-nat-address is set' + ) + tn_address = options['twice_nat_address'] + if tn_address not in addresses_twice_nat: + raise ConfigError( + f'{error_msg} twice-nat-address {tn_address} is not in "address-pool twice-nat"' + ) + + if 'exclude' in config: + for rule, rule_config in config['exclude']['rule'].items(): + keys = {'local_address', 'external_interface'} + if not any(key in rule_config for key in keys): + raise ConfigError( + f'Local-address or external-interface must be specified for exclude rule {rule}' + ) + if all(key in rule_config for key in keys): + raise ConfigError( + f'Cannot set both address and interface for exclude rule {rule}' + ) + if ( + 'external_interface' in rule_config + and rule_config.get('external_interface') not in config['vpp_ifaces'] + ): + raise ConfigError( + f'{rule_config["external_interface"]} must be a VPP interface for exclude rule {rule}' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + n = Nat44() + + if 'remove' in config: + n.disable_nat44_ed() + return None + + if 'effective' in config: + remove_config = config.get('effective') + # Delete inside interfaces + for interface in remove_config['interface']['inside']: + if interface not in config.get('interface', {}).get('inside', []): + n.delete_nat44_interface_inside(interface) + # Delete outside interfaces + for interface in remove_config['interface']['outside']: + if interface not in config.get('interface', {}).get('outside', []): + n.delete_nat44_interface_outside(interface) + # Delete address pool + address_pool = config.get('address_pool', {}) + for address in ( + remove_config.get('address_pool', {}) + .get('translation', {}) + .get('address', []) + ): + if address not in address_pool.get('translation', {}).get('address', []): + n.delete_nat44_address_range(address, twice_nat=False) + for interface in ( + remove_config.get('address_pool', {}) + .get('translation', {}) + .get('interface', []) + ): + if interface not in address_pool.get('translation', {}).get( + 'interface', [] + ): + n.delete_nat44_interface_address(interface, twice_nat=False) + for address in ( + remove_config.get('address_pool', {}) + .get('twice_nat', {}) + .get('address', []) + ): + if address not in address_pool.get('twice_nat', {}).get('address', []): + n.delete_nat44_address_range(address, twice_nat=True) + for interface in ( + remove_config.get('address_pool', {}) + .get('twice_nat', {}) + .get('interface', []) + ): + if interface not in address_pool.get('twice_nat', {}).get('interface', []): + n.delete_nat44_interface_address(interface, twice_nat=True) + # Delete NAT static mapping rules + for rule in config['changed_static_rules']: + if rule in remove_config.get('static', {}).get('rule', {}): + rule_config = remove_config['static']['rule'][rule] + n.delete_nat44_static_mapping( + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + twice_nat='twice_nat' in rule_config.get('options', {}), + self_twice_nat='self_twice_nat' in rule_config.get('options', {}), + out2in='out_to_in_only' in rule_config.get('options', {}), + pool_ip=rule_config.get('options', {}).get('twice_nat_address'), + ) + # Delete NAT exclude rules + for rule in config['changed_exclude_rules']: + if rule in remove_config.get('exclude', {}).get('rule', {}): + rule_config = remove_config['exclude']['rule'][rule] + n.delete_nat44_identity_mapping( + ip_address=rule_config.get('local_address'), + protocol=protocol_map[rule_config.get('protocol', 'all')], + port=int(rule_config.get('local_port', 0)), + interface=rule_config.get('external_interface'), + ) + + # Add NAT44 + n.enable_nat44_ed() + # Add inside interfaces + for interface in config['interface']['inside']: + n.add_nat44_interface_inside(interface) + # Add outside interfaces + for interface in config['interface']['outside']: + n.add_nat44_interface_outside(interface) + # Add translation pool + for address in ( + config.get('address_pool', {}).get('translation', {}).get('address', []) + ): + n.add_nat44_address_range(address, twice_nat=False) + for interface in ( + config.get('address_pool', {}).get('translation', {}).get('interface', []) + ): + n.add_nat44_interface_address(interface, twice_nat=False) + for address in ( + config.get('address_pool', {}).get('twice_nat', {}).get('address', []) + ): + n.add_nat44_address_range(address, twice_nat=True) + for interface in ( + config.get('address_pool', {}).get('twice_nat', {}).get('interface', []) + ): + n.add_nat44_interface_address(interface, twice_nat=True) + # Add NAT static mapping rules + for rule in config['changed_static_rules']: + if rule in config.get('static', {}).get('rule', {}): + rule_config = config['static']['rule'][rule] + n.add_nat44_static_mapping( + local_ip=rule_config.get('local').get('address'), + external_ip=rule_config.get('external', {}).get('address', ''), + local_port=int(rule_config.get('local', {}).get('port', 0)), + external_port=int(rule_config.get('external', {}).get('port', 0)), + protocol=protocol_map[rule_config.get('protocol', 'all')], + twice_nat='twice_nat' in rule_config.get('options', {}), + self_twice_nat='self_twice_nat' in rule_config.get('options', {}), + out2in='out_to_in_only' in rule_config.get('options', {}), + pool_ip=rule_config.get('options', {}).get('twice_nat_address'), + ) + # Add NAT exclude rules + for rule in config['changed_exclude_rules']: + if rule in config.get('exclude', {}).get('rule', {}): + rule_config = config['exclude']['rule'][rule] + n.add_nat44_identity_mapping( + ip_address=rule_config.get('local_address'), + protocol=protocol_map[rule_config.get('protocol', 'all')], + port=int(rule_config.get('local_port', 0)), + interface=rule_config.get('external_interface'), + ) + if 'timeout' in config: + n.set_nat_timeouts( + icmp=int(config.get('timeout').get('icmp')), + udp=int(config.get('timeout').get('udp')), + tcp_established=int(config.get('timeout').get('tcp_established')), + tcp_transitory=int(config.get('timeout').get('tcp_transitory')), + ) + + +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/vpp_nat_cgnat.py b/src/conf_mode/vpp_nat_cgnat.py new file mode 100644 index 000000000..68695b78e --- /dev/null +++ b/src/conf_mode/vpp_nat_cgnat.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from vyos import ConfigError +from vyos.config import Config, config_dict_merge +from vyos.configdict import node_changed +from vyos.configdiff import Diff +from vyos.vpp.utils import cli_ifaces_list + +from vyos.vpp.nat.det44 import Det44 + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'nat', 'cgnat'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not conf.exists(['vpp']): + config['remove_vpp'] = True + return config + + # Get effective config as we need full dictionary to delete + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if not config: + config['remove'] = True + + # Get default values which we need to conditionally update into the + # dictionary retrieved. + default_values = conf.get_config_defaults(**config.kwargs, recursive=True) + config = config_dict_merge(default_values, config) + + config_changed = node_changed( + conf, + base, + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + changed_rules = node_changed( + conf, + base + ['rule'], + key_mangling=('-', '_'), + recursive=True, + expand_nodes=Diff.DELETE | Diff.ADD, + ) + + if not config_changed: + changed_rules = list(config.get('rule', {}).keys()) + + config.update( + { + 'changed_rules': changed_rules, + 'vpp_ifaces': cli_ifaces_list(conf), + } + ) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config or 'remove_vpp' in config: + return None + + if 'interface' not in config: + raise ConfigError('Interfaces must be configured for CGNAT') + if 'rule' not in config: + raise ConfigError('Rules must be configured for CGNAT') + + required_keys = {'inside', 'outside'} + missing_keys = required_keys - set(config['interface'].keys()) + if missing_keys: + raise ConfigError( + f'Both inside and outside interfaces must be configured. ' + f'Please add: {", ".join(missing_keys)}' + ) + + conflict_ifaces = set(config['interface']['inside']).intersection( + set(config['interface']['outside']) + ) + if conflict_ifaces: + raise ConfigError( + f'Interface cannot be both inside and outside. ' + f'Please choose a side for: {", ".join(conflict_ifaces)} ' + ) + + for interface in config['interface']['inside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for inside CGNAT interface' + ) + for interface in config['interface']['outside']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for outside CGNAT interface' + ) + + required_keys = {'outside_prefix', 'inside_prefix'} + for rule in config['rule']: + missing_keys = required_keys - set(config['rule'][rule].keys()) + if missing_keys: + raise ConfigError( + f'Both inside-prefix and outside-prefix must be configured in rule {rule}. ' + f'Please add: {", ".join(missing_keys).replace("_", "-")}' + ) + + +def generate(config): + pass + + +def apply(config): + if 'remove_vpp' in config: + return None + + cgnat = Det44() + + if 'remove' in config: + cgnat.disable_det44_plugin() + return None + + if 'effective' in config: + remove_config = config.get('effective') + # Delete inside interfaces + for interface in remove_config['interface']['inside']: + if interface not in config.get('interface', {}).get('inside', []): + cgnat.delete_det44_interface_inside(interface) + # Delete outside interfaces + for interface in remove_config['interface']['outside']: + if interface not in config.get('interface', {}).get('outside', []): + cgnat.delete_det44_interface_outside(interface) + # Delete CGNAT rules + for rule in config['changed_rules']: + if rule in remove_config.get('rule', {}): + rule_config = remove_config['rule'][rule] + in_addr, in_plen = rule_config['inside_prefix'].split('/') + out_addr, out_plen = rule_config['outside_prefix'].split('/') + cgnat.delete_det44_mapping( + in_addr=in_addr, + in_plen=int(in_plen), + out_addr=out_addr, + out_plen=int(out_plen), + ) + + # Add DET44 + cgnat.enable_det44_plugin() + # Add inside interfaces + for interface in config['interface']['inside']: + cgnat.add_det44_interface_inside(interface) + # Add outside interfaces + for interface in config['interface']['outside']: + cgnat.add_det44_interface_outside(interface) + # Add CGNAT rules + for rule in config['changed_rules']: + if rule in config.get('rule', {}): + rule_config = config['rule'][rule] + in_addr, in_plen = rule_config['inside_prefix'].split('/') + out_addr, out_plen = rule_config['outside_prefix'].split('/') + cgnat.add_det44_mapping( + in_addr=in_addr, + in_plen=int(in_plen), + out_addr=out_addr, + out_plen=int(out_plen), + ) + # Set CGNAT timeouts + cgnat.set_det44_timeouts( + icmp=int(config['timeout']['icmp']), + udp=int(config['timeout']['udp']), + tcp_established=int(config['timeout']['tcp_established']), + tcp_transitory=int(config['timeout']['tcp_transitory']), + ) + + +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/vpp_sflow.py b/src/conf_mode/vpp_sflow.py new file mode 100644 index 000000000..ebe91617b --- /dev/null +++ b/src/conf_mode/vpp_sflow.py @@ -0,0 +1,145 @@ +# Copyright (C) 2025 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 vyos import ConfigError +from vyos.config import Config +from vyos.vpp.utils import cli_ifaces_list +from vyos.vpp import VPPControl + + +def get_config(config=None) -> dict: + if config: + conf = config + else: + conf = Config() + + base = ['vpp', 'sflow'] + + # Get config_dict with default values + config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_defaults=True, + with_recursive_defaults=True, + ) + + # Get effective config as we need full dictionary for deletion + effective_config = conf.get_config_dict( + base, + key_mangling=('-', '_'), + effective=True, + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + # Get system sflow configuration to check for server + system_sflow = conf.get_config_dict( + ['system', 'sflow'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + + if system_sflow: + config['system_sflow'] = system_sflow + + if not config: + config['remove'] = True + return config + + # Add list of VPP interfaces to the config + config.update({'vpp_ifaces': cli_ifaces_list(conf)}) + + if effective_config: + config.update({'effective': effective_config}) + + return config + + +def verify(config): + if 'remove' in config: + return None + + # Check if interface section exists + if 'interface' not in config: + return None + + # Verify that all interfaces specified exist in VPP + for interface in config['interface']: + if interface not in config['vpp_ifaces']: + raise ConfigError( + f'{interface} must be a VPP interface for sFlow monitoring' + ) + + # Verify sample rate is a positive integer + if 'sample_rate' in config: + try: + sample_rate = int(config['sample_rate']) + if sample_rate <= 0: + raise ConfigError('sFlow sample rate must be a positive integer') + except ValueError: + raise ConfigError('sFlow sample rate must be a valid integer') + + # Verify that system sflow has enable-vpp defined + if 'system_sflow' not in config or 'vpp' not in config.get('system_sflow', {}): + raise ConfigError( + 'sFlow enable-vpp must be defined under system sflow configuration' + ) + + +def generate(config): + # No templates to render for sFlow + pass + + +def apply(config): + # Initialize VPP control API + vpp = VPPControl(attempts=20, interval=500) + + if 'remove' in config: + # Disable sFlow on all interfaces + for interface in config.get('effective', {}).get('interface', []): + vpp.cli_cmd(f'sflow enable-disable {interface} disable') + return None + + # Configure sample rate if specified + if 'sample_rate' in config: + vpp.cli_cmd(f'sflow sampling-rate {config["sample_rate"]}') + + # Configure interfaces + if 'interface' in config: + # Enable sFlow on specified interfaces + for interface in config['interface']: + vpp.cli_cmd(f'sflow enable-disable {interface}') + + # Disable sFlow on interfaces that were removed from config + effective_interfaces = config.get('effective', {}).get('interface', []) + if effective_interfaces: + for interface in effective_interfaces: + if interface not in config['interface']: + vpp.cli_cmd(f'sflow enable-disable {interface} disable') + + +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/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py new file mode 100755 index 000000000..b7c42eb58 --- /dev/null +++ b/src/op_mode/show_vpp_interfaces.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import argparse +import json +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.utils.process import rc_cmd + +from vyos.vpp import VPPControl +from vyos.vpp.utils import ( + vpp_ifaces_list, + vpp_ip_addresses_by_index, + vpp_ifaces_stats, +) + + +def get_iproute_address_list(interface: str) -> list: + """Get data from the Linux command 'ip --json address list dev {interface}' and return a list info + for the given interface. + + Args: + interface (str): Interface name. + + Returns: + list: A dictionary containing the JSON data from the 'ip --json address list' command for the specified interface. + """ + rc, out = rc_cmd(f'ip --json address list dev {interface}') + if rc: + return [] + return json.loads(out) + + +def get_iproute_link_list(interface): + """Get data from the Linux command 'ip --json link show dev {interface}' and return a list info + for the given interface. + + Args: + interface (str): Interface name. + + Returns: + list: A dictionary containing the JSON data from the 'ip --json link show' command for the specified interface. + """ + rc, out = rc_cmd(f'ip --json link list dev {interface}') + if rc: + return [] + return json.loads(out) + + +def merge_dicts(*dicts) -> dict: + """Merge dictionaries into a new dictionary. + + Args: + *dicts: Any number of dictionaries. + + Returns: + dict: A new dictionary containing all the key-value pairs from the given dictionaries. + """ + merged = {} + for dictionary in dicts: + merged.update(dictionary) + return merged + + +def show_interfaces(interfaces_list: list) -> str: + """Get JSON info from linux and represent it in a table format + Use tabulate to generate table + + Interface IP Address Mtu S/L Description + --------- ---------- --- --- ----------- + dum0 203.0.113.1/32 1500 u/u + 100.64.1.1/24 + eth0 192.168.122.14/24 1500 u/u WAN + + :return: + """ + table = [] + for interface in interfaces_list: + # Get the data for the interface + ip_address_data = get_iproute_address_list(interface) + link_data = get_iproute_link_list(interface) + + # Skip this interface if data is not available + if not link_data: + continue + interface_data = merge_dicts(ip_address_data[0], link_data[0]) + + # Get the interface name + interface_name = interface_data['ifname'] + + # Get the IP addresses and their corresponding prefixes + ip_info = [ + (address['local'], address.get('prefixlen', '')) + for address in interface_data['addr_info'] + ] + + # Format the IP addresses with prefixes and line breaks + ip_addresses = '\n'.join(f'{ip}/{prefix}' for ip, prefix in ip_info) + + # Get the MAC address + mac = interface_data.get('address', 'n/a') + + # Get the MTU + mtu = interface_data.get('mtu') + + # Get the state of the interface + state = interface_data['operstate'].lower() + + # Get the description of the interface + description = interface_data.get('ifalias', '') + + # Create the list of values for the table + values = [interface_name, ip_addresses, mac, mtu, state, description] + + # Append the list of values to the table + table.append(values) + + # Print the table with IP addresses listed on separate lines + headers = ['Interface', 'IP Address', 'MAC', 'MTU', 'State', 'Description'] + return tabulate(table, headers=headers, tablefmt='simple') + + +def show_interfaces_dataplane(interfaces_list: list, filter_type: str = 'all') -> str: + table = [] + interface_dp_filter = ('tun', 'tap') + lcp_pair_list = vpp.lcp_pairs_list() + vpp_name_kernel_to_kernel_name = { + entry['vpp_name_kernel']: entry['kernel_name'] for entry in lcp_pair_list + } + for interface in interfaces_list: + interface_name = interface.get('interface_name') + if filter_type == 'no_tun_tap' and interface_name.startswith( + interface_dp_filter + ): + continue + if filter_type == 'only_tun_tap' and not interface_name.startswith( + interface_dp_filter + ): + continue + kernel_name = vpp_name_kernel_to_kernel_name.get(interface_name, '') + + dp_ip_addresses = vpp_ip_addresses_by_index( + vpp.api, interface.get('sw_if_index') + ) + ip_addresses = '\n'.join(dp_ip_addresses) + + mac = str(interface.get('l2_address', 'n/a')) + mtu = interface.get('mtu', [])[0] + # state + flags = interface.get('flags') + state = 'up' if flags == 3 else 'down' + + iftype = interface.get('interface_dev_type').split()[0] + + values = [kernel_name, interface_name, iftype, ip_addresses, mac, mtu, state] + table.append(values) + headers = [ + 'Kernel', + 'Dataplane', + 'Type', + 'IP Address', + 'MAC', + 'MTU', + 'State', + ] + table = sorted(table) + return tabulate(table, headers=headers, tablefmt='simple') + + +def show_interfaces_hardware(intf_name) -> str: + if not intf_name: + intf_name = '' + + statistics = vpp_ifaces_stats(intf_name) + for intf, stats in sorted(statistics.items()): + print(f'\n---------------------------------\nInterface {intf}:\n') + table = [] + for k, v in stats.items(): + if isinstance(v, dict): + for i, j in v.items(): + table.append([f"{k} {i}", j]) + else: + table.append([k, v]) + print(tabulate(table, tablefmt="presto")) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Show VPP interfaces') + parser.add_argument( + '--summary', + action='store_true', + help='Show summary of VPP interfaces (ethernet and kernel tun)', + ) + parser.add_argument( + '--dataplane', action='store_true', help='Show VPP ethernet interfaces' + ) + parser.add_argument( + '--kernel', action='store_true', help='Show VPP kernel interfaces' + ) + parser.add_argument( + '--iproute', action='store_true', help='Show interfaces (iproute2)' + ) + parser.add_argument( + '--hardware', + action='store_true', + help='Show more detailed statistics for VPP interfaces', + ) + parser.add_argument('--intf-name', action='store', help='Kernel interface name') + + args = parser.parse_args() + + config = ConfigTreeQuery() + + if not config.exists('vpp settings interface'): + print('VPP interfaces not configured') + exit(0) + + vpp = VPPControl() + dp_ifaces_list = vpp_ifaces_list(vpp.api) + + if args.summary: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='all')) + + if args.dataplane: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='no_tun_tap')) + exit(0) + + if args.kernel: + print(show_interfaces_dataplane(dp_ifaces_list, filter_type='only_tun_tap')) + + if args.iproute: + vpp_interfaces = [] + vpp_ethernet = config.list_nodes('vpp settings interface') + vpp_interfaces.extend(vpp_ethernet) + if config.exists('vpp kernel-interfaces'): + vpp_kernel_interfaces = config.list_nodes('vpp kernel-interfaces') + vpp_interfaces.extend(vpp_kernel_interfaces) + print(show_interfaces(interfaces_list=vpp_interfaces)) + + if args.hardware: + show_interfaces_hardware(intf_name=args.intf_name) diff --git a/src/op_mode/show_vpp_nat44.py b/src/op_mode/show_vpp_nat44.py new file mode 100644 index 000000000..d0569ac49 --- /dev/null +++ b/src/op_mode/show_vpp_nat44.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import json +import sys +from tabulate import tabulate + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +protocol_map = { + 0: 'all', + 1: 'icmp', + 6: 'tcp', + 17: 'udp', +} + +# NAT flags +flags_map = { + 'twice-nat': 0x01, + 'self-twice-nat': 0x02, + 'out2in-only': 0x04, + 'out': 0x10, + 'in': 0x20, +} + + +def _verify(func): + """Decorator checks if config for VPP NAT44 exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + base = 'vpp nat44' + if not config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured') + + return func(*args, **kwargs) + + return _wrapper + + +def decode_bitmask(bitmask: int) -> list: + """Decode a bitmask into a list of flag names""" + return [name for name, value in flags_map.items() if bitmask & value] + + +def _get_raw_output(data_dump): + data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump] + return data + + +def _get_raw_output_sessions(vpp_api): + users: list[dict] = vpp_api.nat44_user_dump() + sessions_list: list[dict] = [] + for user in users: + ip_address = str(user._asdict().get('ip_address')) + user_sessions_dump = vpp_api.nat44_user_session_v3_dump(ip_address=ip_address) + user_sessions = [ + json.loads(json.dumps(session._asdict(), default=str)) + for session in user_sessions_dump + ] + sessions_list.extend(user_sessions) + return sorted(sessions_list, key=lambda x: x["inside_ip_address"]) + + +def _get_formatted_output_sessions(sessions_list): + print('NAT44 ED sessions:') + print(f'--------------- {len(sessions_list)} sessions ---------------') + for session in sessions_list: + in_ip_addr = session.get('inside_ip_address') + in_port = session.get('inside_port') + out_ip_addr = session.get('outside_ip_address') + out_port = session.get('outside_port') + protocol = protocol_map[session.get('protocol')].upper() + last_heard = session.get('last_heard') + time_since_last_heard = session.get('time_since_last_heard') + total_bytes = session.get('total_bytes') + total_pkts = session.get('total_pkts') + ext_host_address = session.get('ext_host_address') + ext_host_port = session.get('ext_host_port') + is_timed_out = session.get('is_timed_out') + + print(f' i2o {in_ip_addr} proto {protocol} port {in_port}') + print(f' o2i {out_ip_addr} proto {protocol} port {out_port}') + print(f' external host {ext_host_address}:{ext_host_port}') + print( + f' i2o flow: match: saddr {in_ip_addr} sport {in_port} daddr {ext_host_address} dport {ext_host_port} proto {protocol} rewrite: saddr {out_ip_addr}' + + ( + f' sport {out_port}' + if protocol != 'ICMP' + else f' daddr {ext_host_address} icmp-id {ext_host_port}' + ) + ) + print( + f' o2i flow: match: saddr {ext_host_address} sport {ext_host_port} daddr {out_ip_addr} dport {out_port} proto {protocol} rewrite: ' + + ( + f'daddr {in_ip_addr} dport {in_port}' + if protocol != 'ICMP' + else f' saddr {ext_host_address} daddr {in_ip_addr} icmp-id {ext_host_port}' + ) + ) + print(f' last heard {last_heard}') + print(f' time since last heard {time_since_last_heard}') + print(f' total packets {total_pkts}, total bytes {total_bytes}') + if is_timed_out: + print(' session timed out') + print('\n') + + +def _get_formatted_output_addresses(addresses): + twice_nat_address = [] + translation_address = [] + for address_info in addresses: + address = address_info.get('ip_address') + if address_info.get('flags') & flags_map['twice-nat']: + twice_nat_address.append(address) + else: + translation_address.append(address) + + print('NAT44 pool addresses:') + for addr in translation_address: + print(f' {addr}') + print('NAT44 twice-nat pool addresses:') + for addr in twice_nat_address: + print(f' {addr}') + + +def _get_formatted_output_interfaces(vpp, interfaces): + print('NAT44 interfaces:') + for interface in interfaces: + name = vpp.get_interface_name(interface['sw_if_index']) + iface_type = decode_bitmask(interface['flags']) + print(f' {name} {" ".join(iface_type)}') + + +def _get_formatted_output_rules(rules_list): + data_entries = [] + for rule in rules_list: + external_address = rule.get('external_ip_address') + external_port = rule.get('external_port') or '' + local_address = rule.get('local_ip_address') + local_port = rule.get('local_port') or '' + protocol = protocol_map[rule.get('protocol', 0)] + options = ' '.join(decode_bitmask(rule.get('flags'))) + + values = [ + external_address, + external_port, + local_address, + local_port, + protocol, + options, + ] + data_entries.append(values) + headers = [ + 'External address', + 'External port', + 'Local address', + 'Local port', + 'Protocol', + 'Options', + ] + out = sorted(data_entries, key=lambda x: x[2]) + return tabulate(out, headers=headers, tablefmt='simple') + + +@_verify +def show_sessions(raw: bool): + vpp = VPPControl() + sessions_list: list[dict] = _get_raw_output_sessions(vpp.api) + + if raw: + return sessions_list + + else: + return _get_formatted_output_sessions(sessions_list) + + +@_verify +def show_summary(raw: bool): + vpp = VPPControl() + return vpp.cli_cmd('show nat44 summary').reply + + +@_verify +def show_static(raw: bool): + vpp = VPPControl() + nat_static_dump = vpp.api.nat44_static_mapping_dump() + rules_list: list[dict] = _get_raw_output(nat_static_dump) + + if raw: + return rules_list + + else: + return _get_formatted_output_rules(rules_list) + + +@_verify +def show_addresses(raw: bool): + vpp = VPPControl() + addresses_dump = vpp.api.nat44_address_dump() + addresses: list[dict] = _get_raw_output(addresses_dump) + + if raw: + return addresses + + else: + return _get_formatted_output_addresses(addresses) + + +@_verify +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.nat44_interface_dump() + interfaces: list[dict] = _get_raw_output(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/vpp_acl.py b/src/op_mode/vpp_acl.py new file mode 100644 index 000000000..7afe96433 --- /dev/null +++ b/src/op_mode/vpp_acl.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import json +import sys +import typing +from tabulate import tabulate + +import vyos.opmode +from vyos.config import Config +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +NO_ACL_INDEX = 0xFFFFFFFF + +# ACL action flags +action_map = { + 0: 'deny', + 1: 'permit', + 2: 'permit-reflect', +} + +# TCP flag names to bit values +TCP_FLAGS = { + 'FIN': 0x01, + 'SYN': 0x02, + 'RST': 0x04, + 'PSH': 0x08, + 'ACK': 0x10, + 'URG': 0x20, + 'ECN': 0x40, + 'CWR': 0x80, +} + + +def _verify(target): + """Decorator checks if config for VPP NAT CGNAT exists""" + from functools import wraps + + if target not in ['ip', 'macip', 'no_target']: + raise ValueError('Invalid target') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + path = 'vpp acl' + if target == 'ip': + path += ' ip' + elif target == 'macip': + path += ' macip' + if not config.exists(path): + raise vyos.opmode.UnconfiguredSubsystem(f'"{path}" is not configured') + return func(*args, **kwargs) + + return _wrapper + + return _verify_target + + +def _get_acl_tag_by_index(vpp, acl_index): + acl = vpp.api.acl_dump(acl_index=acl_index) + if acl: + return acl[0].tag + + return None + + +def _get_macip_acl_tag_by_index(vpp, acl_index): + acl = vpp.api.macip_acl_dump(acl_index=acl_index) + if acl: + return acl[0].tag + + return None + + +def _get_tcp_flag_states(value, mask): + set_flags = [] + unset_flags = [] + for flag, bit in TCP_FLAGS.items(): + if mask & bit: # This flag is being checked + if value & bit: + set_flags.append(flag) + else: + unset_flags.append(flag) + return sorted(set_flags), sorted(unset_flags) + + +def _get_raw_output_acls(data_dump): + out = [] + for data in data_dump: + rules = [json.loads(json.dumps(d._asdict(), default=str)) for d in data.r] + out.append( + { + 'acl_index': data.acl_index, + 'tag': data.tag, + 'count': data.count, + 'r': rules, + } + ) + return out + + +def _get_raw_output_interfaces(data_dump): + ifaces_list = [] + for iface in data_dump: + if iface.count != 0: + ifaces_list.append(json.loads(json.dumps(iface._asdict(), default=str))) + return ifaces_list + + +def _get_formatted_output_interfaces(vpp, interfaces): + data_entries = [] + for interface in interfaces: + name = vpp.get_interface_name(interface.get('sw_if_index')) + input_acls = [] + for acl_index in interface.get('acls')[: interface.get('n_input')]: + input_acls.append(_get_acl_tag_by_index(vpp, int(acl_index))) + output_acls = [] + for acl_index in interface.get('acls')[interface.get('n_input') :]: + output_acls.append(_get_acl_tag_by_index(vpp, int(acl_index))) + values = [ + name, + '\n'.join(input_acls), + '\n'.join(output_acls), + ] + data_entries.append(values) + + headers = ['Interface', 'Input ACLs', 'Output ACLs'] + return tabulate(data_entries, headers=headers, tablefmt='simple') + + +def _get_formatted_output_macip_interfaces(vpp, interfaces): + data_entries = [] + for interface in interfaces: + name = vpp.get_interface_name(interface.get('sw_if_index')) + acl = _get_macip_acl_tag_by_index(vpp, int(interface.get('acls')[0])) + data_entries.append([name, acl]) + + headers = ['Interface', 'ACL'] + return tabulate(data_entries, headers=headers, tablefmt='simple') + + +def _get_formatted_output_acls(acls_list): + conf = Config() + + for acl in acls_list: + acl_index = acl.get('acl_index') + tag = acl.get('tag') + rules = acl.get('r') + print( + '\n---------------------------------\n' + f'IP ACL "tag-name {tag}" acl_index {acl_index}\n' + ) + + path = ['vpp', 'acl', 'ip', 'tag-name', tag, 'rule'] + conf_rules = conf.list_nodes(path) + data_entries = [] + for rule_index, rule in enumerate(rules): + srcport_first = str(rule.get('srcport_or_icmptype_first')) + srcport_last = str(rule.get('srcport_or_icmptype_last')) + dstport_first = str(rule.get('dstport_or_icmpcode_first')) + dstport_last = str(rule.get('dstport_or_icmpcode_last')) + set_flags, unset_flags = _get_tcp_flag_states( + rule.get('tcp_flags_value'), rule.get('tcp_flags_mask') + ) + + values = [ + conf_rules[rule_index], + action_map.get(rule.get('is_permit')), + rule.get('src_prefix'), + ( + f'{srcport_first}-{srcport_last}' + if srcport_first != srcport_last + else srcport_first + ), + rule.get('dst_prefix'), + ( + f'{dstport_first}-{dstport_last}' + if dstport_first != dstport_last + else dstport_first + ), + rule.get('proto'), + '\n'.join(set_flags), + '\n'.join(unset_flags), + ] + data_entries.append(values) + + headers = [ + 'Rule', + 'Action', + 'Src prefix', + 'Src port', + 'Dst prefix', + 'Dst port', + 'Proto', + 'TCP flags set', + 'TCP flags not set', + ] + print(tabulate(data_entries, headers=headers, tablefmt='simple')) + print('\n') + + +def _get_formatted_output_macip_acls(acls_list): + conf = Config() + + for acl in acls_list: + acl_index = acl.get('acl_index') + tag = acl.get('tag') + rules = acl.get('r') + print( + '\n---------------------------------\n' + f'MACIP ACL "tag-name {tag}" acl_index {acl_index}\n' + ) + + path = ['vpp', 'acl', 'macip', 'tag-name', tag, 'rule'] + conf_rules = conf.list_nodes(path) + data_entries = [] + for rule_index, rule in enumerate(rules): + values = [ + conf_rules[rule_index], + action_map.get(rule.get('is_permit')), + rule.get('src_prefix'), + rule.get('src_mac'), + rule.get('src_mac_mask'), + ] + data_entries.append(values) + + headers = [ + 'Rule', + 'Action', + 'IP prefix', + 'MAC address', + 'MAC mask', + ] + print(tabulate(data_entries, headers=headers, tablefmt='simple')) + print('\n') + + +def _find_acl_by_tag(acls, tag_name): + return [acl for acl in acls if acl['tag'] == tag_name] + + +@_verify('ip') +def show_ip_acls(raw: bool, tag_name: typing.Optional[str]): + vpp = VPPControl() + acls_dump = vpp.api.acl_dump(acl_index=NO_ACL_INDEX) + acls: list[dict] = _get_raw_output_acls(acls_dump) + + if tag_name: + acls = _find_acl_by_tag(acls, tag_name) + + if raw: + return acls + + else: + return _get_formatted_output_acls(acls) + + +@_verify('macip') +def show_macip_acls(raw: bool, tag_name: typing.Optional[str]): + vpp = VPPControl() + acls_dump = vpp.api.macip_acl_dump(acl_index=NO_ACL_INDEX) + acls: list[dict] = _get_raw_output_acls(acls_dump) + + if tag_name: + acls = _find_acl_by_tag(acls, tag_name) + + if raw: + return acls + + else: + return _get_formatted_output_macip_acls(acls) + + +@_verify('ip') +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.acl_interface_list_dump() + interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + +@_verify('macip') +def show_macip_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.macip_acl_interface_list_dump() + interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_macip_interfaces(vpp, interfaces) + + +@_verify('no_target') +def show_all_acls(raw: bool): + conf = Config() + acls_all = {} + path = ['vpp', 'acl'] + if conf.exists(path + ['ip']): + ip_acls = show_ip_acls(raw, tag_name=None) + acls_all['ip'] = ip_acls + if conf.exists(path + ['macip']): + macip_acls = show_macip_acls(raw, tag_name=None) + acls_all['macip'] = macip_acls + + if raw: + return acls_all + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/vpp_nat_cgnat.py b/src/op_mode/vpp_nat_cgnat.py new file mode 100644 index 000000000..5112c70be --- /dev/null +++ b/src/op_mode/vpp_nat_cgnat.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import json +import sys +from tabulate import tabulate + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery + +from vyos.vpp import VPPControl + + +def _verify(func): + """Decorator checks if config for VPP NAT CGNAT exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + base = 'vpp nat cgnat' + if not config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured') + + return func(*args, **kwargs) + + return _wrapper + + +def _get_raw_output(data_dump): + data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump] + return data + + +def _get_formatted_output_interfaces(vpp, interfaces): + print('CGNAT interfaces:') + for interface in interfaces: + name = vpp.get_interface_name(interface['sw_if_index']) + iface_type = 'in' if interface['is_inside'] else 'out' + print(f' {name} {iface_type}') + + +def _get_formatted_output_mappings(rules_list): + data_entries = [] + for rule in rules_list: + in_addr = rule.get('in_addr') + in_plen = str(rule.get('in_plen')) + out_addr = rule.get('out_addr') + out_plen = str(rule.get('out_plen')) + sharing_ratio = rule.get('sharing_ratio') + ports_per_host = rule.get('ports_per_host') + ses_num = rule.get('ses_num') + + values = [ + f'{in_addr}/{in_plen}', + f'{out_addr}/{out_plen}', + sharing_ratio, + ports_per_host, + ses_num, + ] + data_entries.append(values) + headers = [ + 'Inside', + 'Outside', + 'Sharing ratio', + 'Ports per host', + 'Sessions', + ] + out = sorted(data_entries, key=lambda x: x[0]) + return tabulate(out, headers=headers, tablefmt='simple') + + +@_verify +def show_sessions(raw: bool): + vpp = VPPControl() + out = vpp.cli_cmd('show det44 sessions').reply + out = out.replace('NAT44 deterministic', 'CGNAT') + return out + + +@_verify +def show_mappings(raw: bool): + vpp = VPPControl() + nat_static_dump = vpp.api.det44_map_dump() + rules_list: list[dict] = _get_raw_output(nat_static_dump) + + if raw: + return rules_list + + else: + return _get_formatted_output_mappings(rules_list) + + +@_verify +def show_interfaces(raw: bool): + vpp = VPPControl() + interfaces_dump = vpp.api.det44_interface_dump() + interfaces: list[dict] = _get_raw_output(interfaces_dump) + + if raw: + return interfaces + + else: + return _get_formatted_output_interfaces(vpp, interfaces) + + +@_verify +def clear_session(address: str, port: str, ext_address: str, ext_port: str): + vpp = VPPControl() + vpp.api.det44_close_session_in( + in_addr=address, + in_port=int(port), + ext_addr=ext_address, + ext_port=int(ext_port), + ) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/systemd/vpp-failure-handler.service b/src/systemd/vpp-failure-handler.service new file mode 100644 index 000000000..3a75f9519 --- /dev/null +++ b/src/systemd/vpp-failure-handler.service @@ -0,0 +1,9 @@ +[Unit] +Description=Restart VPP on failure + +[Service] +Type=oneshot +User=root +Group=vyattacfg +UMask=0002 +ExecStart=/usr/bin/python3 /usr/libexec/vyos/reset_section.py vpp --reload |
