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."""
18 from enum import IntEnum
20 from ipaddress import ip_address, ip_network
22 from resources.libraries.python.Constants import Constants
23 from resources.libraries.python.IncrementUtil import ObjIncrement
24 from resources.libraries.python.InterfaceUtil import InterfaceUtil
25 from resources.libraries.python.IPAddress import IPAddress
26 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
27 from resources.libraries.python.ssh import exec_cmd_no_error, exec_cmd
28 from resources.libraries.python.topology import Topology
29 from resources.libraries.python.VatExecutor import VatTerminal
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, currently only "dash".
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 depend on format.
136 Dash format is '<ip_address_start> - <ip_address_stop>',
137 useful for 'ipsec policy add spd' cli.
138 Slash format is '<ip_address_start>/<prefix_length>'.
140 :returns: Current value converted to string according to format.
142 :raises RuntimeError: If the format is not supported.
144 if self._format == u"dash":
145 return f"{self._value.network_address} - " \
146 f"{self._value.broadcast_address}"
147 # More formats will be added in subsequent changes.
149 raise RuntimeError(f"Unsupported format {self._format}")
153 """Common IP utilities"""
156 def ip_to_int(ip_str):
157 """Convert IP address from string format (e.g. 10.0.0.1) to integer
158 representation (167772161).
160 :param ip_str: IP address in string representation.
162 :returns: Integer representation of IP address.
165 return int(ip_address(ip_str))
168 def int_to_ip(ip_int):
169 """Convert IP address from integer representation (e.g. 167772161) to
170 string format (10.0.0.1).
172 :param ip_int: IP address in integer representation.
174 :returns: String representation of IP address.
177 return str(ip_address(ip_int))
180 def vpp_get_interface_ip_addresses(node, interface, ip_version):
181 """Get list of IP addresses from an interface on a VPP node.
183 :param node: VPP node.
184 :param interface: Name of an interface on the VPP node.
185 :param ip_version: IP protocol version (ipv4 or ipv6).
188 :type ip_version: str
189 :returns: List of dictionaries, each containing IP address, subnet
190 prefix length and also the subnet mask for ipv4 addresses.
191 Note: A single interface may have multiple IP addresses assigned.
194 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
199 cmd = u"ip_address_dump"
201 sw_if_index=sw_if_index,
202 is_ipv6=bool(ip_version == u"ipv6")
204 err_msg = f"Failed to get L2FIB dump on host {node[u'host']}"
206 with PapiSocketExecutor(node) as papi_exec:
207 details = papi_exec.add(cmd, **args).get_details(err_msg)
212 def vpp_get_ip_tables(node):
213 """Get dump of all IP FIB tables on a VPP node.
215 :param node: VPP node.
218 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
219 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
220 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib")
221 PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib summary")
224 def vpp_get_ip_table_summary(node):
225 """Get IPv4 FIB table summary on a VPP node.
227 :param node: VPP node.
230 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
233 def vpp_get_ip_table(node):
234 """Get IPv4 FIB table on a VPP node.
236 :param node: VPP node.
239 PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
242 def vpp_get_ip_tables_prefix(node, address):
243 """Get dump of all IP FIB tables on a VPP node.
245 :param node: VPP node.
246 :param address: IP address.
250 addr = ip_address(address)
251 ip_ver = u"ip6" if addr.version == 6 else u"ip"
253 PapiSocketExecutor.run_cli_cmd(
254 node, f"show {ip_ver} fib {addr}/{addr.max_prefixlen}"
258 def get_interface_vrf_table(node, interface, ip_version='ipv4'):
259 """Get vrf ID for the given interface.
261 :param node: VPP node.
262 :param interface: Name or sw_if_index of a specific interface.
264 :param ip_version: IP protocol version (ipv4 or ipv6).
265 :type interface: str or int
266 :type ip_version: str
267 :returns: vrf ID of the specified interface.
270 sw_if_index = InterfaceUtil.get_interface_index(node, interface)
272 cmd = u"sw_interface_get_table"
274 sw_if_index=sw_if_index,
275 is_ipv6=bool(ip_version == u"ipv6")
277 err_msg = f"Failed to get VRF id assigned to interface {interface}"
279 with PapiSocketExecutor(node) as papi_exec:
280 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
282 return reply[u"vrf_id"]
285 def vpp_ip_source_check_setup(node, if_name):
286 """Setup Reverse Path Forwarding source check on interface.
288 :param node: VPP node.
289 :param if_name: Interface name to setup RPF source check.
293 cmd = u"ip_source_check_interface_add_del"
295 sw_if_index=InterfaceUtil.get_interface_index(node, if_name),
299 err_msg = f"Failed to enable source check on interface {if_name}"
300 with PapiSocketExecutor(node) as papi_exec:
301 papi_exec.add(cmd, **args).get_reply(err_msg)
304 def vpp_ip_probe(node, interface, addr):
305 """Run ip probe on VPP node.
307 :param node: VPP node.
308 :param interface: Interface key or name.
309 :param addr: IPv4/IPv6 address.
314 cmd = u"ip_probe_neighbor"
316 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
319 err_msg = f"VPP ip probe {interface} {addr} failed on {node[u'host']}"
321 with PapiSocketExecutor(node) as papi_exec:
322 papi_exec.add(cmd, **args).get_reply(err_msg)
325 def ip_addresses_should_be_equal(ip1, ip2):
326 """Fails if the given IP addresses are unequal.
328 :param ip1: IPv4 or IPv6 address.
329 :param ip2: IPv4 or IPv6 address.
333 addr1 = ip_address(ip1)
334 addr2 = ip_address(ip2)
337 raise AssertionError(f"IP addresses are not equal: {ip1} != {ip2}")
340 def setup_network_namespace(node, namespace_name, interface_name,
341 ip_addr_list, prefix_length):
342 """Setup namespace on given node and attach interface and IP to
343 this namespace. Applicable also on TG node.
345 :param node: VPP node.
346 :param namespace_name: Namespace name.
347 :param interface_name: Interface name.
348 :param ip_addr_list: List of IP addresses of namespace's interface.
349 :param prefix_length: IP address prefix length.
351 :type namespace_name: str
352 :type interface_name: str
353 :type ip_addr_list: list
354 :type prefix_length: int
356 Namespaces.create_namespace(node, namespace_name)
358 cmd = f"ip netns exec {namespace_name} ip link set {interface_name} up"
359 exec_cmd_no_error(node, cmd, sudo=True)
361 for ip_addr in ip_addr_list:
362 cmd = f"ip netns exec {namespace_name} ip addr add " \
363 f"{ip_addr}/{prefix_length} dev {interface_name}"
364 exec_cmd_no_error(node, cmd, sudo=True)
367 def linux_enable_forwarding(node, ip_ver=u"ipv4"):
368 """Enable forwarding on a Linux node, e.g. VM.
370 :param node: VPP node.
371 :param ip_ver: IP version, 'ipv4' or 'ipv6'.
375 cmd = f"sysctl -w net.{ip_ver}.ip_forward=1"
376 exec_cmd_no_error(node, cmd, sudo=True)
379 def get_linux_interface_name(node, pci_addr):
380 """Get the interface name.
382 :param node: VPP/TG node.
383 :param pci_addr: PCI address
386 :returns: Interface name
388 :raises RuntimeError: If cannot get the information about interfaces.
391 r"pci@([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s" \
392 r"*([a-zA-Z0-9]*)\s*network"
394 cmd = u"lshw -class network -businfo"
395 ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
398 f"Could not get information about interfaces:\n{stderr}"
401 for line in stdout.splitlines()[2:]:
403 if re.search(regex_intf_info, line).group(1) == pci_addr:
404 return re.search(regex_intf_info, line).group(2)
405 except AttributeError:
410 def set_linux_interface_up(
411 node, interface, namespace=None):
412 """Set the specified interface up.
413 :param node: VPP/TG node.
414 :param interface: Interface in namespace.
415 :param namespace: Execute command in namespace. Optional
419 :raises RuntimeError: If the interface could not be set up.
421 if namespace is not None:
422 cmd = f"ip netns exec {namespace} ip link set dev {interface} up"
424 cmd = f"ip link set dev {interface} up"
425 exec_cmd_no_error(node, cmd, timeout=30, sudo=True)
429 def set_linux_interface_ip(
430 node, interface, ip_addr, prefix, namespace=None):
431 """Set IP address to interface in linux.
433 :param node: VPP/TG node.
434 :param interface: Interface in namespace.
435 :param ip_addr: IP to be set on interface.
436 :param prefix: IP prefix.
437 :param namespace: Execute command in namespace. Optional
443 :raises RuntimeError: IP could not be set.
445 if namespace is not None:
446 cmd = f"ip netns exec {namespace} ip addr add {ip_addr}/{prefix}" \
449 cmd = f"ip addr add {ip_addr}/{prefix} dev {interface}"
451 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
454 def delete_linux_interface_ip(
455 node, interface, ip_addr, prefix_length, namespace=None):
456 """Delete IP address from interface in linux.
458 :param node: VPP/TG node.
459 :param interface: Interface in namespace.
460 :param ip_addr: IP to be deleted from interface.
461 :param prefix_length: IP prefix length.
462 :param namespace: Execute command in namespace. Optional
466 :type prefix_length: int
468 :raises RuntimeError: IP could not be deleted.
470 if namespace is not None:
471 cmd = f"ip netns exec {namespace} ip addr del " \
472 f"{ip_addr}/{prefix_length} dev {interface}"
474 cmd = f"ip addr del {ip_addr}/{prefix_length} dev {interface}"
476 exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
479 def linux_interface_has_ip(
480 node, interface, ip_addr, prefix_length, namespace=None):
481 """Return True if interface in linux has IP address.
483 :param node: VPP/TG node.
484 :param interface: Interface in namespace.
485 :param ip_addr: IP to be queried on interface.
486 :param prefix_length: IP prefix length.
487 :param namespace: Execute command in namespace. Optional
491 :type prefix_length: int
494 :raises RuntimeError: Request fails.
496 ip_addr_with_prefix = f"{ip_addr}/{prefix_length}"
497 if namespace is not None:
498 cmd = f"ip netns exec {namespace} ip addr show dev {interface}"
500 cmd = f"ip addr show dev {interface}"
502 cmd += u" | grep 'inet ' | awk -e '{print $2}'"
503 cmd += f" | grep '{ip_addr_with_prefix}'"
504 _, stdout, _ = exec_cmd(node, cmd, timeout=5, sudo=True)
506 has_ip = stdout.rstrip()
507 return bool(has_ip == ip_addr_with_prefix)
510 def add_linux_route(node, ip_addr, prefix, gateway, namespace=None):
511 """Add linux route in namespace.
513 :param node: Node where to execute command.
514 :param ip_addr: Route destination IP address.
515 :param prefix: IP prefix.
516 :param namespace: Execute command in namespace. Optional.
517 :param gateway: Gateway address.
524 if namespace is not None:
525 cmd = f"ip netns exec {namespace} ip route add {ip_addr}/{prefix}" \
528 cmd = f"ip route add {ip_addr}/{prefix} via {gateway}"
530 exec_cmd_no_error(node, cmd, sudo=True)
533 def vpp_interface_set_ip_address(
534 node, interface, address, prefix_length=None):
535 """Set IP address to VPP interface.
537 :param node: VPP node.
538 :param interface: Interface name.
539 :param address: IP address.
540 :param prefix_length: Prefix length.
544 :type prefix_length: int
546 ip_addr = ip_address(address)
548 cmd = u"sw_interface_add_del_address"
550 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
553 prefix=IPUtil.create_prefix_object(
555 prefix_length if prefix_length else 128
556 if ip_addr.version == 6 else 32
559 err_msg = f"Failed to add IP address on interface {interface}"
561 with PapiSocketExecutor(node) as papi_exec:
562 papi_exec.add(cmd, **args).get_reply(err_msg)
565 def vpp_interface_set_ip_addresses(node, interface, ip_addr_list,
567 """Set IP addresses to VPP interface.
569 :param node: VPP node.
570 :param interface: Interface name.
571 :param ip_addr_list: IP addresses.
572 :param prefix_length: Prefix length.
575 :type ip_addr_list: list
576 :type prefix_length: int
578 for ip_addr in ip_addr_list:
579 IPUtil.vpp_interface_set_ip_address(node, interface, ip_addr,
583 def vpp_add_ip_neighbor(node, iface_key, ip_addr, mac_address):
584 """Add IP neighbor on DUT node.
586 :param node: VPP node.
587 :param iface_key: Interface key.
588 :param ip_addr: IP address of the interface.
589 :param mac_address: MAC address of the interface.
593 :type mac_address: str
595 dst_ip = ip_address(ip_addr)
598 sw_if_index=Topology.get_interface_sw_index(node, iface_key),
600 mac_address=str(mac_address),
601 ip_address=str(dst_ip)
603 cmd = u"ip_neighbor_add_del"
608 err_msg = f"Failed to add IP neighbor on interface {iface_key}"
610 with PapiSocketExecutor(node) as papi_exec:
611 papi_exec.add(cmd, **args).get_reply(err_msg)
614 def create_prefix_object(ip_addr, addr_len):
615 """Create prefix object.
617 :param ip_addr: IPv4 or IPv6 address.
618 :param addr_len: Length of IP address.
619 :type ip_addr: IPv4Address or IPv6Address
621 :returns: Prefix object.
624 addr = IPAddress.create_ip_address_object(ip_addr)
632 def compose_vpp_route_structure(node, network, prefix_len, **kwargs):
633 """Create route object for ip_route_add_del api call.
635 :param node: VPP node.
636 :param network: Route destination network address.
637 :param prefix_len: Route destination network prefix length.
638 :param kwargs: Optional key-value arguments:
640 gateway: Route gateway address. (str)
641 interface: Route interface. (str)
642 vrf: VRF table ID. (int)
643 count: number of IP addresses to add starting from network IP (int)
644 local: The route is local with same prefix (increment is 1).
645 If None, then is not used. (bool)
646 lookup_vrf: VRF table ID for lookup. (int)
647 multipath: Enable multipath routing. (bool)
648 weight: Weight value for unequal cost multipath routing. (int)
652 :type prefix_len: int
654 :returns: route parameter basic structure
657 interface = kwargs.get(u"interface", u"")
658 gateway = kwargs.get(u"gateway", u"")
660 net_addr = ip_address(network)
662 prefix = IPUtil.create_prefix_object(net_addr, prefix_len)
666 address=IPAddress.union_addr(ip_address(gateway)) if gateway else 0,
667 via_label=MPLS_LABEL_INVALID,
668 obj_id=Constants.BITWISE_NON_ZERO
671 sw_if_index=InterfaceUtil.get_interface_index(node, interface)
672 if interface else Constants.BITWISE_NON_ZERO,
673 table_id=int(kwargs.get(u"lookup_vrf", 0)),
674 rpf_id=Constants.BITWISE_NON_ZERO,
675 weight=int(kwargs.get(u"weight", 1)),
678 FibPathType, u"FIB_PATH_TYPE_LOCAL"
679 if kwargs.get(u"local", False)
680 else u"FIB_PATH_TYPE_NORMAL"
682 flags=getattr(FibPathFlags, u"FIB_PATH_FLAG_NONE").value,
684 FibPathNhProto, u"FIB_PATH_NH_PROTO_IP6"
685 if net_addr.version == 6
686 else u"FIB_PATH_NH_PROTO_IP4"
690 label_stack=list(0 for _ in range(16))
695 table_id=int(kwargs.get(u"vrf", 0)),
703 def vpp_route_add(node, network, prefix_len, **kwargs):
704 """Add route to the VPP node.
706 :param node: VPP node.
707 :param network: Route destination network address.
708 :param prefix_len: Route destination network prefix length.
709 :param kwargs: Optional key-value arguments:
711 gateway: Route gateway address. (str)
712 interface: Route interface. (str)
713 vrf: VRF table ID. (int)
714 count: number of IP addresses to add starting from network IP (int)
715 local: The route is local with same prefix (increment is 1).
716 If None, then is not used. (bool)
717 lookup_vrf: VRF table ID for lookup. (int)
718 multipath: Enable multipath routing. (bool)
719 weight: Weight value for unequal cost multipath routing. (int)
723 :type prefix_len: int
726 count = kwargs.get(u"count", 1)
729 gateway = kwargs.get(u"gateway", '')
730 interface = kwargs.get(u"interface", '')
731 vrf = kwargs.get(u"vrf", None)
732 multipath = kwargs.get(u"multipath", False)
734 with VatTerminal(node, json_param=False) as vat:
736 vat.vat_terminal_exec_cmd_from_template(
737 u"vpp_route_add.vat",
739 prefix_length=prefix_len,
740 via=f"via {gateway}" if gateway else u"",
741 sw_if_index=f"sw_if_index "
742 f"{InterfaceUtil.get_interface_index(node, interface)}"
743 if interface else u"",
744 vrf=f"vrf {vrf}" if vrf else u"",
745 count=f"count {count}" if count else u"",
746 multipath=u"multipath" if multipath else u""
750 net_addr = ip_address(network)
751 cmd = u"ip_route_add_del"
754 is_multipath=kwargs.get(u"multipath", False),
757 err_msg = f"Failed to add route(s) on host {node[u'host']}"
759 with PapiSocketExecutor(node) as papi_exec:
760 for i in range(kwargs.get(u"count", 1)):
761 args[u"route"] = IPUtil.compose_vpp_route_structure(
762 node, net_addr + i, prefix_len, **kwargs
764 history = bool(not 1 < i < kwargs.get(u"count", 1))
765 papi_exec.add(cmd, history=history, **args)
766 papi_exec.get_replies(err_msg)
769 def flush_ip_addresses(node, interface):
770 """Flush all IP addresses from specified interface.
772 :param node: VPP node.
773 :param interface: Interface name.
777 cmd = u"sw_interface_add_del_address"
779 sw_if_index=InterfaceUtil.get_interface_index(node, interface),
783 err_msg = f"Failed to flush IP address on interface {interface}"
785 with PapiSocketExecutor(node) as papi_exec:
786 papi_exec.add(cmd, **args).get_reply(err_msg)
789 def add_fib_table(node, table_id, ipv6=False):
790 """Create new FIB table according to ID.
792 :param node: Node to add FIB on.
793 :param table_id: FIB table ID.
794 :param ipv6: Is this an IPv6 table
799 cmd = u"ip_table_add_del"
801 table_id=int(table_id),
808 err_msg = f"Failed to add FIB table on host {node[u'host']}"
810 with PapiSocketExecutor(node) as papi_exec:
811 papi_exec.add(cmd, **args).get_reply(err_msg)