1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Copyright (c) 2023 PANTHEON.tech s.r.o.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 """Common IP utilities library."""
19 from enum import IntEnum
21 from ipaddress import ip_address, ip_network
23 from resources.libraries.python.Constants import Constants
24 from resources.libraries.python.IncrementUtil import ObjIncrement
25 from resources.libraries.python.InterfaceUtil import InterfaceUtil
26 from resources.libraries.python.IPAddress import IPAddress
27 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
28 from resources.libraries.python.ssh import exec_cmd_no_error, exec_cmd
29 from resources.libraries.python.topology import Topology
30 from resources.libraries.python.Namespaces import Namespaces
33 # from vpp/src/vnet/vnet/mpls/mpls_types.h
34 MPLS_IETF_MAX_LABEL = 0xfffff
35 MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1
38 class FibPathType(IntEnum):
40 FIB_PATH_TYPE_NORMAL = 0
41 FIB_PATH_TYPE_LOCAL = 1
42 FIB_PATH_TYPE_DROP = 2
43 FIB_PATH_TYPE_UDP_ENCAP = 3
44 FIB_PATH_TYPE_BIER_IMP = 4
45 FIB_PATH_TYPE_ICMP_UNREACH = 5
46 FIB_PATH_TYPE_ICMP_PROHIBIT = 6
47 FIB_PATH_TYPE_SOURCE_LOOKUP = 7
49 FIB_PATH_TYPE_INTERFACE_RX = 9
50 FIB_PATH_TYPE_CLASSIFY = 10
53 class FibPathFlags(IntEnum):
55 FIB_PATH_FLAG_NONE = 0
56 FIB_PATH_FLAG_RESOLVE_VIA_ATTACHED = 1
57 FIB_PATH_FLAG_RESOLVE_VIA_HOST = 2
60 class FibPathNhProto(IntEnum):
61 """FIB path next-hop protocol."""
62 FIB_PATH_NH_PROTO_IP4 = 0
63 FIB_PATH_NH_PROTO_IP6 = 1
64 FIB_PATH_NH_PROTO_MPLS = 2
65 FIB_PATH_NH_PROTO_ETHERNET = 3
66 FIB_PATH_NH_PROTO_BIER = 4
69 class IpDscp(IntEnum):
70 """DSCP code points."""
94 class NetworkIncrement(ObjIncrement):
96 An iterator object which accepts an IPv4Network or IPv6Network and
97 returns a new network, its address part incremented by the increment
98 number of network sizes, each time it is iterated or when inc_fmt is called.
99 The increment may be positive, negative or 0
100 (in which case the network is always the same).
102 Both initial and subsequent IP address can have host bits set,
103 check the initial value before creating instance if needed.
104 String formatting is configurable via constructor argument.
106 def __init__(self, initial_value, increment=1, format=u"dash"):
108 :param initial_value: The initial network. Can have host bits set.
109 :param increment: The current network will be incremented by this
110 amount of network sizes in each iteration/var_str call.
111 :param format: Type of formatting to use, "dash" or "slash" or "addr".
112 :type initial_value: Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
116 super().__init__(initial_value, increment)
117 self._prefix_len = self._value.prefixlen
118 host_len = self._value.max_prefixlen - self._prefix_len
119 self._net_increment = self._increment * (1 << host_len)
120 self._format = str(format).lower()
124 Increment the network, e.g.:
125 '30.0.0.0/24' incremented by 1 (the next network) is '30.0.1.0/24'.
126 '30.0.0.0/24' incremented by 2 is '30.0.2.0/24'.
128 self._value = ip_network(
129 f"{self._value.network_address + self._net_increment}"
130 f"/{self._prefix_len}", strict=False
135 The string representation of the network depends on format.
137 Dash format is '<ip_address_start> - <ip_address_stop>',
138 useful for 'ipsec policy add spd' CLI.
140 Slash format is '<ip_address_start>/<prefix_length>',
141 useful for other CLI.
143 Addr format is '<ip_address_start>', useful for PAPI.
145 :returns: Current value converted to string according to format.
147 :raises RuntimeError: If the format is not supported.
149 if self._format == u"dash":
150 return f"{self._value.network_address} - " \
151 f"{self._value.broadcast_address}"
152 elif self._format == u"slash":
153 return f"{self._value.network_address}/{self._prefix_len}"
154 elif self._format == u"addr":
155 return f"{self._value.network_address}"
157 raise RuntimeError(f"Unsupported format {self._format}")
161 """Common IP utilities"""
164 def ip_to_int(ip_str):
165 """Convert IP address from string format (e.g. 10.0.0.1) to integer
166 representation (167772161).
168 :param ip_str: IP address in string representation.
170 :returns: Integer representation of IP address.
173 return int(ip_address(ip_str))
176 def int_to_ip(ip_int):
177 """Convert IP address from integer representation (e.g. 167772161) to
178 string format (10.0.0.1).
180 :param ip_int: IP address in integer representation.
182 :returns: String representation of IP address.
185 return str(ip_address(ip_int))
188 def vpp_get_interface_ip_addresses(node, interface, ip_version):
189 """Get list of IP addresses from an interface on a VPP node.
191 :param node: VPP node.
192 :param interface: Name of an interface on the VPP node.
193 :param ip_version: IP protocol version (ipv4 or ipv6).
196 :type ip_version: str
197 :returns: List of dictionaries, each containing IP address, subnet
198 prefix length and also the subnet mask for ipv4 addresses.
199 Note: A single interface may have multiple IP addresses assigned.
202 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
207 cmd = u"ip_address_dump"
209 sw_if_index=sw_if_index,
210 is_ipv6=bool(ip_version == u"ipv6")
212 err_msg = f"Failed to get L2FIB dump on host {node[u'host']}"
214 with PapiSocketExecutor(node) as papi_exec:
215 details = papi_exec.add(cmd, **args).get_details(err_msg)
220 def vpp_get_ip_tables(node):
221 """Get dump of all IP FIB tables on a VPP node.
223 :param node: VPP node.
226 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
227 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
228 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib")
229 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib summary")
232 def vpp_get_ip_table_summary(node):
233 """Get IPv4 FIB table summary on a VPP node.
235 :param node: VPP node.
238 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
241 def vpp_get_ip_table(node):
242 """Get IPv4 FIB table on a VPP node.
244 :param node: VPP node.
247 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
250 def vpp_get_ip_tables_prefix(node, address):
251 """Get dump of all IP FIB tables on a VPP node.
253 :param node: VPP node.
254 :param address: IP address.
258 addr = ip_address(address)
259 ip_ver = u"ip6" if addr.version == 6 else u"ip"
261 PapiSocketExecutor.run_cli_cmd(
262 node, f"show {ip_ver} fib {addr}/{addr.max_prefixlen}"
266 def get_interface_vrf_table(node, interface, ip_version='ipv4'):
267 """Get vrf ID for the given interface.
269 :param node: VPP node.
270 :param interface: Name or sw_if_index of a specific interface.
272 :param ip_version: IP protocol version (ipv4 or ipv6).
273 :type interface: str or int
274 :type ip_version: str
275 :returns: vrf ID of the specified interface.
278 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
280 cmd = u"sw_interface_get_table"
282 sw_if_index=sw_if_index,
283 is_ipv6=bool(ip_version == u"ipv6")
285 err_msg = f"Failed to get VRF id assigned to interface {interface}"
287 with PapiSocketExecutor(node) as papi_exec:
288 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
290 return reply[u"vrf_id"]
293 def vpp_ip_source_check_setup(node, if_name):
294 """Setup Reverse Path Forwarding source check on interface.
296 :param node: VPP node.
297 :param if_name: Interface name to setup RPF source check.
301 cmd = u"ip_source_check_interface_add_del"
303 sw_if_index=InterfaceUtil.get_interface_index(node, if_name),
307 err_msg = f"Failed to enable source check on interface {if_name}"
308 with PapiSocketExecutor(node) as papi_exec:
309 papi_exec.add(cmd, **args).get_reply(err_msg)
312 def vpp_ip_probe(node, interface, addr):
313 """Run ip probe on VPP node.
315 :param node: VPP node.
316 :param interface: Interface key or name.
317 :param addr: IPv4/IPv6 address.
322 cmd = u"ip_probe_neighbor"
324 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
327 err_msg = f"VPP ip probe {interface} {addr} failed on {node[u'host']}"
329 with PapiSocketExecutor(node) as papi_exec:
330 papi_exec.add(cmd, **args).get_reply(err_msg)
333 def ip_addresses_should_be_equal(ip1, ip2):
334 """Fails if the given IP addresses are unequal.
336 :param ip1: IPv4 or IPv6 address.
337 :param ip2: IPv4 or IPv6 address.
341 addr1 = ip_address(ip1)
342 addr2 = ip_address(ip2)
345 raise AssertionError(f"IP addresses are not equal: {ip1} != {ip2}")
348 def setup_network_namespace(node, namespace_name, interface_name,
349 ip_addr_list, prefix_length):
350 """Setup namespace on given node and attach interface and IP to
351 this namespace. Applicable also on TG node.
353 :param node: VPP node.
354 :param namespace_name: Namespace name.
355 :param interface_name: Interface name.
356 :param ip_addr_list: List of IP addresses of namespace's interface.
357 :param prefix_length: IP address prefix length.
359 :type namespace_name: str
360 :type interface_name: str
361 :type ip_addr_list: list
362 :type prefix_length: int
364 Namespaces.create_namespace(node, namespace_name)
366 cmd = f"ip netns exec {namespace_name} ip link set {interface_name} up"
367 exec_cmd_no_error(node, cmd, sudo=True)
369 for ip_addr in ip_addr_list:
370 cmd = f"ip netns exec {namespace_name} ip addr add " \
371 f"{ip_addr}/{prefix_length} dev {interface_name}"
372 exec_cmd_no_error(node, cmd, sudo=True)
375 def linux_enable_forwarding(node, ip_ver=u"ipv4"):
376 """Enable forwarding on a Linux node, e.g. VM.
378 :param node: VPP node.
379 :param ip_ver: IP version, 'ipv4' or 'ipv6'.
383 cmd = f"sysctl -w net.{ip_ver}.ip_forward=1"
384 exec_cmd_no_error(node, cmd, sudo=True)
387 def get_linux_interface_name(node, pci_addr):
388 """Get the interface name.
390 :param node: VPP/TG node.
391 :param pci_addr: PCI address
394 :returns: Interface name
396 :raises RuntimeError: If cannot get the information about interfaces.
399 r"pci@([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s" \
400 r"*([a-zA-Z0-9]*)\s*network"
402 cmd = u"lshw -class network -businfo"
403 ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
406 f"Could not get information about interfaces:\n{stderr}"
409 for line in stdout.splitlines()[2:]:
411 if re.search(regex_intf_info, line).group(1) == pci_addr:
412 return re.search(regex_intf_info, line).group(2)
413 except AttributeError:
418 def set_linux_interface_up(
419 node, interface, namespace=None):
420 """Set the specified interface up.
421 :param node: VPP/TG node.
422 :param interface: Interface in namespace.
423 :param namespace: Execute command in namespace. Optional
427 :raises RuntimeError: If the interface could not be set up.
429 if namespace is not None:
430 cmd = f"ip netns exec {namespace} ip link set dev {interface} up"
432 cmd = f"ip link set dev {interface} up"
433 exec_cmd_no_error(node, cmd, timeout=30, sudo=True)
437 def set_linux_interface_ip(
438 node, interface, ip_addr, prefix, namespace=None):
439 """Set IP address to interface in linux.
441 :param node: VPP/TG node.
442 :param interface: Interface in namespace.
443 :param ip_addr: IP to be set on interface.
444 :param prefix: IP prefix.
445 :param namespace: Execute command in namespace. Optional
451 :raises RuntimeError: IP could not be set.
453 if namespace is not None:
454 cmd = f"ip netns exec {namespace} ip addr add {ip_addr}/{prefix}" \
457 cmd = f"ip addr add {ip_addr}/{prefix} dev {interface}"
459 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
462 def delete_linux_interface_ip(
463 node, interface, ip_addr, prefix_length, namespace=None):
464 """Delete IP address from interface in linux.
466 :param node: VPP/TG node.
467 :param interface: Interface in namespace.
468 :param ip_addr: IP to be deleted from interface.
469 :param prefix_length: IP prefix length.
470 :param namespace: Execute command in namespace. Optional
474 :type prefix_length: int
476 :raises RuntimeError: IP could not be deleted.
478 if namespace is not None:
479 cmd = f"ip netns exec {namespace} ip addr del " \
480 f"{ip_addr}/{prefix_length} dev {interface}"
482 cmd = f"ip addr del {ip_addr}/{prefix_length} dev {interface}"
484 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
487 def linux_interface_has_ip(
488 node, interface, ip_addr, prefix_length, namespace=None):
489 """Return True if interface in linux has IP address.
491 :param node: VPP/TG node.
492 :param interface: Interface in namespace.
493 :param ip_addr: IP to be queried on interface.
494 :param prefix_length: IP prefix length.
495 :param namespace: Execute command in namespace. Optional
499 :type prefix_length: int
502 :raises RuntimeError: Request fails.
504 ip_addr_with_prefix = f"{ip_addr}/{prefix_length}"
505 if namespace is not None:
506 cmd = f"ip netns exec {namespace} ip addr show dev {interface}"
508 cmd = f"ip addr show dev {interface}"
510 cmd += u" | grep 'inet ' | awk -e '{print $2}'"
511 cmd += f" | grep '{ip_addr_with_prefix}'"
512 _, stdout, _ = exec_cmd(node, cmd, timeout=5, sudo=True)
514 has_ip = stdout.rstrip()
515 return bool(has_ip == ip_addr_with_prefix)
518 def add_linux_route(node, ip_addr, prefix, gateway, namespace=None):
519 """Add linux route in namespace.
521 :param node: Node where to execute command.
522 :param ip_addr: Route destination IP address.
523 :param prefix: IP prefix.
524 :param namespace: Execute command in namespace. Optional.
525 :param gateway: Gateway address.
532 if namespace is not None:
533 cmd = f"ip netns exec {namespace} ip route add {ip_addr}/{prefix}" \
536 cmd = f"ip route add {ip_addr}/{prefix} via {gateway}"
538 exec_cmd_no_error(node, cmd, sudo=True)
541 def vpp_interface_set_ip_address(
542 node, interface, address, prefix_length=None):
543 """Set IP address to VPP interface.
545 :param node: VPP node.
546 :param interface: Interface name.
547 :param address: IP address.
548 :param prefix_length: Prefix length.
552 :type prefix_length: int
554 ip_addr = ip_address(address)
556 cmd = u"sw_interface_add_del_address"
558 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
561 prefix=IPUtil.create_prefix_object(
563 prefix_length if prefix_length else 128
564 if ip_addr.version == 6 else 32
567 err_msg = f"Failed to add IP address on interface {interface}"
569 with PapiSocketExecutor(node) as papi_exec:
570 papi_exec.add(cmd, **args).get_reply(err_msg)
573 def vpp_interface_set_ip_addresses(node, interface, ip_addr_list,
575 """Set IP addresses to VPP interface.
577 :param node: VPP node.
578 :param interface: Interface name.
579 :param ip_addr_list: IP addresses.
580 :param prefix_length: Prefix length.
583 :type ip_addr_list: list
584 :type prefix_length: int
586 for ip_addr in ip_addr_list:
587 IPUtil.vpp_interface_set_ip_address(node, interface, ip_addr,
591 def vpp_add_ip_neighbor(node, iface_key, ip_addr, mac_address):
592 """Add IP neighbor on DUT node.
594 :param node: VPP node.
595 :param iface_key: Interface key.
596 :param ip_addr: IP address of the interface.
597 :param mac_address: MAC address of the interface.
601 :type mac_address: str
603 dst_ip = ip_address(ip_addr)
606 sw_if_index=Topology.get_interface_sw_index(node, iface_key),
608 mac_address=str(mac_address),
609 ip_address=str(dst_ip)
611 cmd = u"ip_neighbor_add_del"
616 err_msg = f"Failed to add IP neighbor on interface {iface_key}"
618 with PapiSocketExecutor(node) as papi_exec:
619 papi_exec.add(cmd, **args).get_reply(err_msg)
622 def create_prefix_object(ip_addr, addr_len):
623 """Create prefix object.
625 :param ip_addr: IPv4 or IPv6 address.
626 :param addr_len: Length of IP address.
627 :type ip_addr: IPv4Address or IPv6Address
629 :returns: Prefix object.
632 addr = IPAddress.create_ip_address_object(ip_addr)
640 def compose_vpp_route_structure(node, network, prefix_len, **kwargs):
641 """Create route object for ip_route_add_del api call.
643 :param node: VPP node.
644 :param network: Route destination network address.
645 :param prefix_len: Route destination network prefix length.
646 :param kwargs: Optional key-value arguments:
648 gateway: Route gateway address. (str)
649 interface: Route interface. (str)
650 vrf: VRF table ID. (int)
651 count: number of IP addresses to add starting from network IP (int)
652 local: The route is local with same prefix (increment is 1).
653 If None, then is not used. (bool)
654 lookup_vrf: VRF table ID for lookup. (int)
655 weight: Weight value for unequal cost multipath routing. (int)
656 (Multipath value enters at higher level.)
660 :type prefix_len: int
662 :returns: route parameter basic structure
665 interface = kwargs.get(u"interface", u"")
666 gateway = kwargs.get(u"gateway", u"")
668 net_addr = ip_address(network)
670 prefix = IPUtil.create_prefix_object(net_addr, prefix_len)
674 address=IPAddress.union_addr(ip_address(gateway)) if gateway else 0,
675 via_label=MPLS_LABEL_INVALID,
676 obj_id=Constants.BITWISE_NON_ZERO
679 sw_if_index=InterfaceUtil.get_interface_index(node, interface)
680 if interface else Constants.BITWISE_NON_ZERO,
681 table_id=int(kwargs.get(u"lookup_vrf", 0)),
682 rpf_id=Constants.BITWISE_NON_ZERO,
683 weight=int(kwargs.get(u"weight", 1)),
686 FibPathType, u"FIB_PATH_TYPE_LOCAL"
687 if kwargs.get(u"local", False)
688 else u"FIB_PATH_TYPE_NORMAL"
690 flags=getattr(FibPathFlags, u"FIB_PATH_FLAG_NONE").value,
692 FibPathNhProto, u"FIB_PATH_NH_PROTO_IP6"
693 if net_addr.version == 6
694 else u"FIB_PATH_NH_PROTO_IP4"
698 label_stack=list(0 for _ in range(16))
703 table_id=int(kwargs.get(u"vrf", 0)),
711 def vpp_route_add(node, network, prefix_len, strict=True, **kwargs):
712 """Add route to the VPP node. Prefer multipath behavior.
714 :param node: VPP node.
715 :param network: Route destination network address.
716 :param prefix_len: Route destination network prefix length.
717 :param strict: If true, fail if address has host bits set.
718 :param kwargs: Optional key-value arguments:
720 gateway: Route gateway address. (str)
721 interface: Route interface. (str)
722 vrf: VRF table ID. (int)
723 count: number of IP addresses to add starting from network IP (int)
724 local: The route is local with same prefix (increment is 1 network)
725 If None, then is not used. (bool)
726 lookup_vrf: VRF table ID for lookup. (int)
727 multipath: Enable multipath routing. (bool) Default: True.
728 weight: Weight value for unequal cost multipath routing. (int)
732 :type prefix_len: int
735 :raises RuntimeError: If the argument combination is not supported.
737 count = kwargs.get(u"count", 1)
739 cmd = u"ip_route_add_del"
742 is_multipath=kwargs.get(u"multipath", True),
745 err_msg = f"Failed to add route(s) on host {node[u'host']}"
747 netiter = NetworkIncrement(
748 ip_network(f"{network}/{prefix_len}", strict=strict),
751 with PapiSocketExecutor(node, is_async=True) as papi_exec:
752 for i in range(count):
753 args[u"route"] = IPUtil.compose_vpp_route_structure(
754 node, netiter.inc_fmt(), prefix_len, **kwargs
756 history = bool(not 0 < i < count - 1)
757 papi_exec.add(cmd, history=history, **args)
758 papi_exec.get_replies(err_msg)
761 def flush_ip_addresses(node, interface):
762 """Flush all IP addresses from specified interface.
764 :param node: VPP node.
765 :param interface: Interface name.
769 cmd = u"sw_interface_add_del_address"
771 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
775 err_msg = f"Failed to flush IP address on interface {interface}"
777 with PapiSocketExecutor(node) as papi_exec:
778 papi_exec.add(cmd, **args).get_reply(err_msg)
781 def add_fib_table(node, table_id, ipv6=False):
782 """Create new FIB table according to ID.
784 :param node: Node to add FIB on.
785 :param table_id: FIB table ID.
786 :param ipv6: Is this an IPv6 table
791 cmd = u"ip_table_add_del"
793 table_id=int(table_id),
800 err_msg = f"Failed to add FIB table on host {node[u'host']}"
802 with PapiSocketExecutor(node) as papi_exec:
803 papi_exec.add(cmd, **args).get_reply(err_msg)