1 # Copyright (c) 2021 Cisco and/or its affiliates.
2 # Copyright (c) 2021 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."""
20 from enum import IntEnum
22 from ipaddress import ip_address, ip_network
24 from resources.libraries.python.Constants import Constants
25 from resources.libraries.python.IncrementUtil import ObjIncrement
26 from resources.libraries.python.InterfaceUtil import InterfaceUtil
27 from resources.libraries.python.IPAddress import IPAddress
28 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
29 from resources.libraries.python.ssh import exec_cmd_no_error, exec_cmd
30 from resources.libraries.python.topology import Topology
31 from resources.libraries.python.VatExecutor import VatExecutor
32 from resources.libraries.python.Namespaces import Namespaces
35 # from vpp/src/vnet/vnet/mpls/mpls_types.h
36 MPLS_IETF_MAX_LABEL = 0xfffff
37 MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1
40 class FibPathType(IntEnum):
42 FIB_PATH_TYPE_NORMAL = 0
43 FIB_PATH_TYPE_LOCAL = 1
44 FIB_PATH_TYPE_DROP = 2
45 FIB_PATH_TYPE_UDP_ENCAP = 3
46 FIB_PATH_TYPE_BIER_IMP = 4
47 FIB_PATH_TYPE_ICMP_UNREACH = 5
48 FIB_PATH_TYPE_ICMP_PROHIBIT = 6
49 FIB_PATH_TYPE_SOURCE_LOOKUP = 7
51 FIB_PATH_TYPE_INTERFACE_RX = 9
52 FIB_PATH_TYPE_CLASSIFY = 10
55 class FibPathFlags(IntEnum):
57 FIB_PATH_FLAG_NONE = 0
58 FIB_PATH_FLAG_RESOLVE_VIA_ATTACHED = 1
59 FIB_PATH_FLAG_RESOLVE_VIA_HOST = 2
62 class FibPathNhProto(IntEnum):
63 """FIB path next-hop protocol."""
64 FIB_PATH_NH_PROTO_IP4 = 0
65 FIB_PATH_NH_PROTO_IP6 = 1
66 FIB_PATH_NH_PROTO_MPLS = 2
67 FIB_PATH_NH_PROTO_ETHERNET = 3
68 FIB_PATH_NH_PROTO_BIER = 4
71 class IpDscp(IntEnum):
72 """DSCP code points."""
96 class NetworkIncrement(ObjIncrement):
98 An iterator object which accepts an IPv4Network or IPv6Network and
99 returns a new network, its address part incremented by the increment
100 number of network sizes, each time it is iterated or when inc_fmt is called.
101 The increment may be positive, negative or 0
102 (in which case the network is always the same).
104 Both initial and subsequent IP address can have host bits set,
105 check the initial value before creating instance if needed.
106 String formatting is configurable via constructor argument.
108 def __init__(self, initial_value, increment=1, format=u"dash"):
110 :param initial_value: The initial network. Can have host bits set.
111 :param increment: The current network will be incremented by this
112 amount of network sizes in each iteration/var_str call.
113 :param format: Type of formatting to use, "dash" or "slash" or "addr".
114 :type initial_value: Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
118 super().__init__(initial_value, increment)
119 self._prefix_len = self._value.prefixlen
120 host_len = self._value.max_prefixlen - self._prefix_len
121 self._net_increment = self._increment * (1 << host_len)
122 self._format = str(format).lower()
126 Increment the network, e.g.:
127 '30.0.0.0/24' incremented by 1 (the next network) is '30.0.1.0/24'.
128 '30.0.0.0/24' incremented by 2 is '30.0.2.0/24'.
130 self._value = ip_network(
131 f"{self._value.network_address + self._net_increment}"
132 f"/{self._prefix_len}", strict=False
137 The string representation of the network depends on format.
139 Dash format is '<ip_address_start> - <ip_address_stop>',
140 useful for 'ipsec policy add spd' CLI.
142 Slash format is '<ip_address_start>/<prefix_length>',
143 useful for other CLI.
145 Addr format is '<ip_address_start>', useful for PAPI.
147 :returns: Current value converted to string according to format.
149 :raises RuntimeError: If the format is not supported.
151 if self._format == u"dash":
152 return f"{self._value.network_address} - " \
153 f"{self._value.broadcast_address}"
154 elif self._format == u"slash":
155 return f"{self._value.network_address}/{self._prefix_len}"
156 elif self._format == u"addr":
157 return f"{self._value.network_address}"
159 raise RuntimeError(f"Unsupported format {self._format}")
163 """Common IP utilities"""
166 def ip_to_int(ip_str):
167 """Convert IP address from string format (e.g. 10.0.0.1) to integer
168 representation (167772161).
170 :param ip_str: IP address in string representation.
172 :returns: Integer representation of IP address.
175 return int(ip_address(ip_str))
178 def int_to_ip(ip_int):
179 """Convert IP address from integer representation (e.g. 167772161) to
180 string format (10.0.0.1).
182 :param ip_int: IP address in integer representation.
184 :returns: String representation of IP address.
187 return str(ip_address(ip_int))
190 def vpp_get_interface_ip_addresses(node, interface, ip_version):
191 """Get list of IP addresses from an interface on a VPP node.
193 :param node: VPP node.
194 :param interface: Name of an interface on the VPP node.
195 :param ip_version: IP protocol version (ipv4 or ipv6).
198 :type ip_version: str
199 :returns: List of dictionaries, each containing IP address, subnet
200 prefix length and also the subnet mask for ipv4 addresses.
201 Note: A single interface may have multiple IP addresses assigned.
204 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
209 cmd = u"ip_address_dump"
211 sw_if_index=sw_if_index,
212 is_ipv6=bool(ip_version == u"ipv6")
214 err_msg = f"Failed to get L2FIB dump on host {node[u'host']}"
216 with PapiSocketExecutor(node) as papi_exec:
217 details = papi_exec.add(cmd, **args).get_details(err_msg)
222 def vpp_get_ip_tables(node):
223 """Get dump of all IP FIB tables on a VPP node.
225 :param node: VPP node.
228 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
229 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
230 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib")
231 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib summary")
234 def vpp_get_ip_table_summary(node):
235 """Get IPv4 FIB table summary on a VPP node.
237 :param node: VPP node.
240 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
243 def vpp_get_ip_table(node):
244 """Get IPv4 FIB table on a VPP node.
246 :param node: VPP node.
249 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
252 def vpp_get_ip_tables_prefix(node, address):
253 """Get dump of all IP FIB tables on a VPP node.
255 :param node: VPP node.
256 :param address: IP address.
260 addr = ip_address(address)
261 ip_ver = u"ip6" if addr.version == 6 else u"ip"
263 PapiSocketExecutor.run_cli_cmd(
264 node, f"show {ip_ver} fib {addr}/{addr.max_prefixlen}"
268 def get_interface_vrf_table(node, interface, ip_version='ipv4'):
269 """Get vrf ID for the given interface.
271 :param node: VPP node.
272 :param interface: Name or sw_if_index of a specific interface.
274 :param ip_version: IP protocol version (ipv4 or ipv6).
275 :type interface: str or int
276 :type ip_version: str
277 :returns: vrf ID of the specified interface.
280 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
282 cmd = u"sw_interface_get_table"
284 sw_if_index=sw_if_index,
285 is_ipv6=bool(ip_version == u"ipv6")
287 err_msg = f"Failed to get VRF id assigned to interface {interface}"
289 with PapiSocketExecutor(node) as papi_exec:
290 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
292 return reply[u"vrf_id"]
295 def vpp_ip_source_check_setup(node, if_name):
296 """Setup Reverse Path Forwarding source check on interface.
298 :param node: VPP node.
299 :param if_name: Interface name to setup RPF source check.
303 cmd = u"ip_source_check_interface_add_del"
305 sw_if_index=InterfaceUtil.get_interface_index(node, if_name),
309 err_msg = f"Failed to enable source check on interface {if_name}"
310 with PapiSocketExecutor(node) as papi_exec:
311 papi_exec.add(cmd, **args).get_reply(err_msg)
314 def vpp_ip_probe(node, interface, addr):
315 """Run ip probe on VPP node.
317 :param node: VPP node.
318 :param interface: Interface key or name.
319 :param addr: IPv4/IPv6 address.
324 cmd = u"ip_probe_neighbor"
326 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
329 err_msg = f"VPP ip probe {interface} {addr} failed on {node[u'host']}"
331 with PapiSocketExecutor(node) as papi_exec:
332 papi_exec.add(cmd, **args).get_reply(err_msg)
335 def ip_addresses_should_be_equal(ip1, ip2):
336 """Fails if the given IP addresses are unequal.
338 :param ip1: IPv4 or IPv6 address.
339 :param ip2: IPv4 or IPv6 address.
343 addr1 = ip_address(ip1)
344 addr2 = ip_address(ip2)
347 raise AssertionError(f"IP addresses are not equal: {ip1} != {ip2}")
350 def setup_network_namespace(node, namespace_name, interface_name,
351 ip_addr_list, prefix_length):
352 """Setup namespace on given node and attach interface and IP to
353 this namespace. Applicable also on TG node.
355 :param node: VPP node.
356 :param namespace_name: Namespace name.
357 :param interface_name: Interface name.
358 :param ip_addr_list: List of IP addresses of namespace's interface.
359 :param prefix_length: IP address prefix length.
361 :type namespace_name: str
362 :type interface_name: str
363 :type ip_addr_list: list
364 :type prefix_length: int
366 Namespaces.create_namespace(node, namespace_name)
368 cmd = f"ip netns exec {namespace_name} ip link set {interface_name} up"
369 exec_cmd_no_error(node, cmd, sudo=True)
371 for ip_addr in ip_addr_list:
372 cmd = f"ip netns exec {namespace_name} ip addr add " \
373 f"{ip_addr}/{prefix_length} dev {interface_name}"
374 exec_cmd_no_error(node, cmd, sudo=True)
377 def linux_enable_forwarding(node, ip_ver=u"ipv4"):
378 """Enable forwarding on a Linux node, e.g. VM.
380 :param node: VPP node.
381 :param ip_ver: IP version, 'ipv4' or 'ipv6'.
385 cmd = f"sysctl -w net.{ip_ver}.ip_forward=1"
386 exec_cmd_no_error(node, cmd, sudo=True)
389 def get_linux_interface_name(node, pci_addr):
390 """Get the interface name.
392 :param node: VPP/TG node.
393 :param pci_addr: PCI address
396 :returns: Interface name
398 :raises RuntimeError: If cannot get the information about interfaces.
401 r"pci@([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s" \
402 r"*([a-zA-Z0-9]*)\s*network"
404 cmd = u"lshw -class network -businfo"
405 ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
408 f"Could not get information about interfaces:\n{stderr}"
411 for line in stdout.splitlines()[2:]:
413 if re.search(regex_intf_info, line).group(1) == pci_addr:
414 return re.search(regex_intf_info, line).group(2)
415 except AttributeError:
420 def set_linux_interface_up(
421 node, interface, namespace=None):
422 """Set the specified interface up.
423 :param node: VPP/TG node.
424 :param interface: Interface in namespace.
425 :param namespace: Execute command in namespace. Optional
429 :raises RuntimeError: If the interface could not be set up.
431 if namespace is not None:
432 cmd = f"ip netns exec {namespace} ip link set dev {interface} up"
434 cmd = f"ip link set dev {interface} up"
435 exec_cmd_no_error(node, cmd, timeout=30, sudo=True)
439 def set_linux_interface_ip(
440 node, interface, ip_addr, prefix, namespace=None):
441 """Set IP address to interface in linux.
443 :param node: VPP/TG node.
444 :param interface: Interface in namespace.
445 :param ip_addr: IP to be set on interface.
446 :param prefix: IP prefix.
447 :param namespace: Execute command in namespace. Optional
453 :raises RuntimeError: IP could not be set.
455 if namespace is not None:
456 cmd = f"ip netns exec {namespace} ip addr add {ip_addr}/{prefix}" \
459 cmd = f"ip addr add {ip_addr}/{prefix} dev {interface}"
461 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
464 def delete_linux_interface_ip(
465 node, interface, ip_addr, prefix_length, namespace=None):
466 """Delete IP address from interface in linux.
468 :param node: VPP/TG node.
469 :param interface: Interface in namespace.
470 :param ip_addr: IP to be deleted from interface.
471 :param prefix_length: IP prefix length.
472 :param namespace: Execute command in namespace. Optional
476 :type prefix_length: int
478 :raises RuntimeError: IP could not be deleted.
480 if namespace is not None:
481 cmd = f"ip netns exec {namespace} ip addr del " \
482 f"{ip_addr}/{prefix_length} dev {interface}"
484 cmd = f"ip addr del {ip_addr}/{prefix_length} dev {interface}"
486 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
489 def linux_interface_has_ip(
490 node, interface, ip_addr, prefix_length, namespace=None):
491 """Return True if interface in linux has IP address.
493 :param node: VPP/TG node.
494 :param interface: Interface in namespace.
495 :param ip_addr: IP to be queried on interface.
496 :param prefix_length: IP prefix length.
497 :param namespace: Execute command in namespace. Optional
501 :type prefix_length: int
504 :raises RuntimeError: Request fails.
506 ip_addr_with_prefix = f"{ip_addr}/{prefix_length}"
507 if namespace is not None:
508 cmd = f"ip netns exec {namespace} ip addr show dev {interface}"
510 cmd = f"ip addr show dev {interface}"
512 cmd += u" | grep 'inet ' | awk -e '{print $2}'"
513 cmd += f" | grep '{ip_addr_with_prefix}'"
514 _, stdout, _ = exec_cmd(node, cmd, timeout=5, sudo=True)
516 has_ip = stdout.rstrip()
517 return bool(has_ip == ip_addr_with_prefix)
520 def add_linux_route(node, ip_addr, prefix, gateway, namespace=None):
521 """Add linux route in namespace.
523 :param node: Node where to execute command.
524 :param ip_addr: Route destination IP address.
525 :param prefix: IP prefix.
526 :param namespace: Execute command in namespace. Optional.
527 :param gateway: Gateway address.
534 if namespace is not None:
535 cmd = f"ip netns exec {namespace} ip route add {ip_addr}/{prefix}" \
538 cmd = f"ip route add {ip_addr}/{prefix} via {gateway}"
540 exec_cmd_no_error(node, cmd, sudo=True)
543 def vpp_interface_set_ip_address(
544 node, interface, address, prefix_length=None):
545 """Set IP address to VPP interface.
547 :param node: VPP node.
548 :param interface: Interface name.
549 :param address: IP address.
550 :param prefix_length: Prefix length.
554 :type prefix_length: int
556 ip_addr = ip_address(address)
558 cmd = u"sw_interface_add_del_address"
560 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
563 prefix=IPUtil.create_prefix_object(
565 prefix_length if prefix_length else 128
566 if ip_addr.version == 6 else 32
569 err_msg = f"Failed to add IP address on interface {interface}"
571 with PapiSocketExecutor(node) as papi_exec:
572 papi_exec.add(cmd, **args).get_reply(err_msg)
575 def vpp_interface_set_ip_addresses(node, interface, ip_addr_list,
577 """Set IP addresses to VPP interface.
579 :param node: VPP node.
580 :param interface: Interface name.
581 :param ip_addr_list: IP addresses.
582 :param prefix_length: Prefix length.
585 :type ip_addr_list: list
586 :type prefix_length: int
588 for ip_addr in ip_addr_list:
589 IPUtil.vpp_interface_set_ip_address(node, interface, ip_addr,
593 def vpp_add_ip_neighbor(node, iface_key, ip_addr, mac_address):
594 """Add IP neighbor on DUT node.
596 :param node: VPP node.
597 :param iface_key: Interface key.
598 :param ip_addr: IP address of the interface.
599 :param mac_address: MAC address of the interface.
603 :type mac_address: str
605 dst_ip = ip_address(ip_addr)
608 sw_if_index=Topology.get_interface_sw_index(node, iface_key),
610 mac_address=str(mac_address),
611 ip_address=str(dst_ip)
613 cmd = u"ip_neighbor_add_del"
618 err_msg = f"Failed to add IP neighbor on interface {iface_key}"
620 with PapiSocketExecutor(node) as papi_exec:
621 papi_exec.add(cmd, **args).get_reply(err_msg)
624 def create_prefix_object(ip_addr, addr_len):
625 """Create prefix object.
627 :param ip_addr: IPv4 or IPv6 address.
628 :param addr_len: Length of IP address.
629 :type ip_addr: IPv4Address or IPv6Address
631 :returns: Prefix object.
634 addr = IPAddress.create_ip_address_object(ip_addr)
642 def compose_vpp_route_structure(node, network, prefix_len, **kwargs):
643 """Create route object for ip_route_add_del api call.
645 :param node: VPP node.
646 :param network: Route destination network address.
647 :param prefix_len: Route destination network prefix length.
648 :param kwargs: Optional key-value arguments:
650 gateway: Route gateway address. (str)
651 interface: Route interface. (str)
652 vrf: VRF table ID. (int)
653 count: number of IP addresses to add starting from network IP (int)
654 local: The route is local with same prefix (increment is 1).
655 If None, then is not used. (bool)
656 lookup_vrf: VRF table ID for lookup. (int)
657 weight: Weight value for unequal cost multipath routing. (int)
658 (Multipath value enters at higher level.)
662 :type prefix_len: int
664 :returns: route parameter basic structure
667 interface = kwargs.get(u"interface", u"")
668 gateway = kwargs.get(u"gateway", u"")
670 net_addr = ip_address(network)
672 prefix = IPUtil.create_prefix_object(net_addr, prefix_len)
676 address=IPAddress.union_addr(ip_address(gateway)) if gateway else 0,
677 via_label=MPLS_LABEL_INVALID,
678 obj_id=Constants.BITWISE_NON_ZERO
681 sw_if_index=InterfaceUtil.get_interface_index(node, interface)
682 if interface else Constants.BITWISE_NON_ZERO,
683 table_id=int(kwargs.get(u"lookup_vrf", 0)),
684 rpf_id=Constants.BITWISE_NON_ZERO,
685 weight=int(kwargs.get(u"weight", 1)),
688 FibPathType, u"FIB_PATH_TYPE_LOCAL"
689 if kwargs.get(u"local", False)
690 else u"FIB_PATH_TYPE_NORMAL"
692 flags=getattr(FibPathFlags, u"FIB_PATH_FLAG_NONE").value,
694 FibPathNhProto, u"FIB_PATH_NH_PROTO_IP6"
695 if net_addr.version == 6
696 else u"FIB_PATH_NH_PROTO_IP4"
700 label_stack=list(0 for _ in range(16))
705 table_id=int(kwargs.get(u"vrf", 0)),
713 def vpp_route_add(node, network, prefix_len, strict=True, **kwargs):
714 """Add route to the VPP node. Prefer multipath behavior.
716 :param node: VPP node.
717 :param network: Route destination network address.
718 :param prefix_len: Route destination network prefix length.
719 :param strict: If true, fail if address has host bits set.
720 :param kwargs: Optional key-value arguments:
722 gateway: Route gateway address. (str)
723 interface: Route interface. (str)
724 vrf: VRF table ID. (int)
725 count: number of IP addresses to add starting from network IP (int)
726 local: The route is local with same prefix (increment is 1 network)
727 If None, then is not used. (bool)
728 lookup_vrf: VRF table ID for lookup. (int)
729 multipath: Enable multipath routing. (bool) Default: True.
730 weight: Weight value for unequal cost multipath routing. (int)
734 :type prefix_len: int
737 :raises RuntimeError: If the argument combination is not supported.
739 count = kwargs.get(u"count", 1)
742 if not kwargs.get(u"multipath", True):
743 raise RuntimeError(u"VAT exec supports only multipath behavior")
744 gateway = kwargs.get(u"gateway", u"")
745 interface = kwargs.get(u"interface", u"")
746 local = kwargs.get(u"local", u"")
748 interface = InterfaceUtil.vpp_get_interface_name(
749 node, InterfaceUtil.get_interface_index(
753 vrf = kwargs.get(u"vrf", None)
756 trailers.append(f"table {vrf}")
758 trailers.append(f"via {gateway}")
760 trailers.append(interface)
762 trailers.append(f"via {interface}")
764 if gateway or interface:
765 raise RuntimeError(u"Unsupported combination with local.")
766 trailers.append(u"local")
767 trailer = u" ".join(trailers)
768 command_parts = [u"exec ip route add", u"network goes here"]
770 command_parts.append(trailer)
771 netiter = NetworkIncrement(
772 ip_network(f"{network}/{prefix_len}", strict=strict),
775 tmp_filename = u"/tmp/routes.config"
776 with open(tmp_filename, u"w") as tmp_file:
777 for _ in range(count):
778 command_parts[1] = netiter.inc_fmt()
779 print(u" ".join(command_parts), file=tmp_file)
780 VatExecutor().execute_script(
781 tmp_filename, node, timeout=1800, json_out=False,
782 copy_on_execute=True, history=False
784 os.remove(tmp_filename)
787 cmd = u"ip_route_add_del"
790 is_multipath=kwargs.get(u"multipath", True),
793 err_msg = f"Failed to add route(s) on host {node[u'host']}"
795 netiter = NetworkIncrement(
796 ip_network(f"{network}/{prefix_len}", strict=strict),
799 with PapiSocketExecutor(node) as papi_exec:
800 for i in range(count):
801 args[u"route"] = IPUtil.compose_vpp_route_structure(
802 node, netiter.inc_fmt(), prefix_len, **kwargs
804 history = bool(not 0 < i < count - 1)
805 papi_exec.add(cmd, history=history, **args)
806 papi_exec.get_replies(err_msg)
809 def flush_ip_addresses(node, interface):
810 """Flush all IP addresses from specified interface.
812 :param node: VPP node.
813 :param interface: Interface name.
817 cmd = u"sw_interface_add_del_address"
819 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
823 err_msg = f"Failed to flush IP address on interface {interface}"
825 with PapiSocketExecutor(node) as papi_exec:
826 papi_exec.add(cmd, **args).get_reply(err_msg)
829 def add_fib_table(node, table_id, ipv6=False):
830 """Create new FIB table according to ID.
832 :param node: Node to add FIB on.
833 :param table_id: FIB table ID.
834 :param ipv6: Is this an IPv6 table
839 cmd = u"ip_table_add_del"
841 table_id=int(table_id),
848 err_msg = f"Failed to add FIB table on host {node[u'host']}"
850 with PapiSocketExecutor(node) as papi_exec:
851 papi_exec.add(cmd, **args).get_reply(err_msg)