CSIT-1597 API cleanup: tap
[csit.git] / resources / libraries / python / IPUtil.py
1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Common IP utilities library."""
15
16 import re
17
18 from enum import IntEnum
19
20 from ipaddress import ip_address
21
22 from resources.libraries.python.Constants import Constants
23 from resources.libraries.python.InterfaceUtil import InterfaceUtil
24 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
25 from resources.libraries.python.ssh import exec_cmd_no_error, exec_cmd
26 from resources.libraries.python.topology import Topology
27 from resources.libraries.python.VatExecutor import VatTerminal
28
29
30 # from vpp/src/vnet/vnet/mpls/mpls_types.h
31 MPLS_IETF_MAX_LABEL = 0xfffff
32 MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1
33
34
35 class AddressFamily(IntEnum):
36     """IP address family."""
37     ADDRESS_IP4 = 0
38     ADDRESS_IP6 = 1
39
40
41 class FibPathType(IntEnum):
42     """FIB path types."""
43     FIB_PATH_TYPE_NORMAL = 0
44     FIB_PATH_TYPE_LOCAL = 1
45     FIB_PATH_TYPE_DROP = 2
46     FIB_PATH_TYPE_UDP_ENCAP = 3
47     FIB_PATH_TYPE_BIER_IMP = 4
48     FIB_PATH_TYPE_ICMP_UNREACH = 5
49     FIB_PATH_TYPE_ICMP_PROHIBIT = 6
50     FIB_PATH_TYPE_SOURCE_LOOKUP = 7
51     FIB_PATH_TYPE_DVR = 8
52     FIB_PATH_TYPE_INTERFACE_RX = 9
53     FIB_PATH_TYPE_CLASSIFY = 10
54
55
56 class FibPathFlags(IntEnum):
57     """FIB path flags."""
58     FIB_PATH_FLAG_NONE = 0
59     # TODO: Name too long for pylint, fix in VPP.
60     FIB_PATH_FLAG_RESOLVE_VIA_ATTACHED = 1
61     FIB_PATH_FLAG_RESOLVE_VIA_HOST = 2
62
63
64 class FibPathNhProto(IntEnum):
65     """FIB path next-hop protocol."""
66     FIB_PATH_NH_PROTO_IP4 = 0
67     FIB_PATH_NH_PROTO_IP6 = 1
68     FIB_PATH_NH_PROTO_MPLS = 2
69     FIB_PATH_NH_PROTO_ETHERNET = 3
70     FIB_PATH_NH_PROTO_BIER = 4
71
72
73 class IPUtil:
74     """Common IP utilities"""
75
76     @staticmethod
77     def ip_to_int(ip_str):
78         """Convert IP address from string format (e.g. 10.0.0.1) to integer
79         representation (167772161).
80
81         :param ip_str: IP address in string representation.
82         :type ip_str: str
83         :returns: Integer representation of IP address.
84         :rtype: int
85         """
86         return int(ip_address(ip_str))
87
88     @staticmethod
89     def int_to_ip(ip_int):
90         """Convert IP address from integer representation (e.g. 167772161) to
91         string format (10.0.0.1).
92
93         :param ip_int: IP address in integer representation.
94         :type ip_int: int
95         :returns: String representation of IP address.
96         :rtype: str
97         """
98         return str(ip_address(ip_int))
99
100     @staticmethod
101     def vpp_get_interface_ip_addresses(node, interface, ip_version):
102         """Get list of IP addresses from an interface on a VPP node.
103
104         :param node: VPP node.
105         :param interface: Name of an interface on the VPP node.
106         :param ip_version: IP protocol version (ipv4 or ipv6).
107         :type node: dict
108         :type interface: str
109         :type ip_version: str
110         :returns: List of dictionaries, each containing IP address, subnet
111             prefix length and also the subnet mask for ipv4 addresses.
112             Note: A single interface may have multiple IP addresses assigned.
113         :rtype: list
114         """
115         sw_if_index = InterfaceUtil.get_interface_index(node, interface)
116
117         if not sw_if_index:
118             return list()
119
120         cmd = u"ip_address_dump"
121         args = dict(
122             sw_if_index=sw_if_index,
123             is_ipv6=bool(ip_version == u"ipv6")
124         )
125         err_msg = f"Failed to get L2FIB dump on host {node[u'host']}"
126
127         with PapiSocketExecutor(node) as papi_exec:
128             details = papi_exec.add(cmd, **args).get_details(err_msg)
129
130         # TODO: CSIT currently looks only whether the list is empty.
131         # Add proper value processing if values become important.
132
133         return details
134
135     @staticmethod
136     def vpp_get_ip_tables(node):
137         """Get dump of all IP FIB tables on a VPP node.
138
139         :param node: VPP node.
140         :type node: dict
141         """
142         PapiSocketExecutor.run_cli_cmd(node, u"show ip fib")
143         PapiSocketExecutor.run_cli_cmd(node, u"show ip fib summary")
144         PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib")
145         PapiSocketExecutor.run_cli_cmd(node, u"show ip6 fib summary")
146
147     @staticmethod
148     def vpp_get_ip_tables_prefix(node, address):
149         """Get dump of all IP FIB tables on a VPP node.
150
151         :param node: VPP node.
152         :param address: IP address.
153         :type node: dict
154         :type address: str
155         """
156         addr = ip_address(address)
157         ip_ver = u"ip6" if addr.version == 6 else u"ip"
158
159         PapiSocketExecutor.run_cli_cmd(
160             node, f"show {ip_ver} fib {addr}/{addr.max_prefixlen}"
161         )
162
163     @staticmethod
164     def get_interface_vrf_table(node, interface, ip_version='ipv4'):
165         """Get vrf ID for the given interface.
166
167         :param node: VPP node.
168         :param interface: Name or sw_if_index of a specific interface.
169         :type node: dict
170         :param ip_version: IP protocol version (ipv4 or ipv6).
171         :type interface: str or int
172         :type ip_version: str
173         :returns: vrf ID of the specified interface.
174         :rtype: int
175         """
176         sw_if_index = InterfaceUtil.get_interface_index(node, interface)
177
178         cmd = u"sw_interface_get_table"
179         args = dict(
180             sw_if_index=sw_if_index,
181             is_ipv6=bool(ip_version == u"ipv6")
182         )
183         err_msg = f"Failed to get VRF id assigned to interface {interface}"
184
185         with PapiSocketExecutor(node) as papi_exec:
186             reply = papi_exec.add(cmd, **args).get_reply(err_msg)
187
188         return reply[u"vrf_id"]
189
190     @staticmethod
191     def vpp_ip_source_check_setup(node, if_name):
192         """Setup Reverse Path Forwarding source check on interface.
193
194         :param node: VPP node.
195         :param if_name: Interface name to setup RPF source check.
196         :type node: dict
197         :type if_name: str
198         """
199         cmd = u"ip_source_check_interface_add_del"
200         args = dict(
201             sw_if_index=InterfaceUtil.get_interface_index(node, if_name),
202             is_add=1,
203             loose=0
204         )
205         err_msg = f"Failed to enable source check on interface {if_name}"
206         with PapiSocketExecutor(node) as papi_exec:
207             papi_exec.add(cmd, **args).get_reply(err_msg)
208
209     @staticmethod
210     def vpp_ip_probe(node, interface, addr):
211         """Run ip probe on VPP node.
212
213         :param node: VPP node.
214         :param interface: Interface key or name.
215         :param addr: IPv4/IPv6 address.
216         :type node: dict
217         :type interface: str
218         :type addr: str
219         """
220         cmd = u"ip_probe_neighbor"
221         args = dict(
222             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
223             dst=str(addr)
224         )
225         err_msg = f"VPP ip probe {interface} {addr} failed on {node[u'host']}"
226
227         with PapiSocketExecutor(node) as papi_exec:
228             papi_exec.add(cmd, **args).get_reply(err_msg)
229
230     @staticmethod
231     def ip_addresses_should_be_equal(ip1, ip2):
232         """Fails if the given IP addresses are unequal.
233
234         :param ip1: IPv4 or IPv6 address.
235         :param ip2: IPv4 or IPv6 address.
236         :type ip1: str
237         :type ip2: str
238         """
239         addr1 = ip_address(ip1)
240         addr2 = ip_address(ip2)
241
242         if addr1 != addr2:
243             raise AssertionError(f"IP addresses are not equal: {ip1} != {ip2}")
244
245     @staticmethod
246     def setup_network_namespace(
247             node, namespace_name, interface_name, ip_addr, prefix):
248         """Setup namespace on given node and attach interface and IP to
249         this namespace. Applicable also on TG node.
250
251         :param node: VPP node.
252         :param namespace_name: Namespace name.
253         :param interface_name: Interface name.
254         :param ip_addr: IP address of namespace's interface.
255         :param prefix: IP address prefix length.
256         :type node: dict
257         :type namespace_name: str
258         :type interface_name: str
259         :type ip_addr: str
260         :type prefix: int
261         """
262         cmd = f"ip netns add {namespace_name}"
263         exec_cmd_no_error(node, cmd, sudo=True)
264
265         cmd = f"ip link set dev {interface_name} up netns {namespace_name}"
266         exec_cmd_no_error(node, cmd, sudo=True)
267
268         cmd = f"ip netns exec {namespace_name} ip addr add {ip_addr}/{prefix}" \
269             f" dev {interface_name}"
270         exec_cmd_no_error(node, cmd, sudo=True)
271
272     @staticmethod
273     def linux_enable_forwarding(node, ip_ver=u"ipv4"):
274         """Enable forwarding on a Linux node, e.g. VM.
275
276         :param node: VPP node.
277         :param ip_ver: IP version, 'ipv4' or 'ipv6'.
278         :type node: dict
279         :type ip_ver: str
280         """
281         cmd = f"sysctl -w net.{ip_ver}.ip_forward=1"
282         exec_cmd_no_error(node, cmd, sudo=True)
283
284     @staticmethod
285     def get_linux_interface_name(node, pci_addr):
286         """Get the interface name.
287
288         :param node: VPP/TG node.
289         :param pci_addr: PCI address
290         :type node: dict
291         :type pci_addr: str
292         :returns: Interface name
293         :rtype: str
294         :raises RuntimeError: If cannot get the information about interfaces.
295         """
296         regex_intf_info = \
297             r"pci@([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s" \
298             r"*([a-zA-Z0-9]*)\s*network"
299
300         cmd = u"lshw -class network -businfo"
301         ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
302         if ret_code != 0:
303             raise RuntimeError(
304                 f"Could not get information about interfaces:\n{stderr}"
305             )
306
307         for line in stdout.splitlines()[2:]:
308             try:
309                 if re.search(regex_intf_info, line).group(1) == pci_addr:
310                     return re.search(regex_intf_info, line).group(2)
311             except AttributeError:
312                 continue
313         return None
314
315     @staticmethod
316     def set_linux_interface_up(node, interface):
317         """Set the specified interface up.
318
319         :param node: VPP/TG node.
320         :param interface: Interface in namespace.
321         :type node: dict
322         :type interface: str
323         :raises RuntimeError: If the interface could not be set up.
324         """
325         cmd = f"ip link set {interface} up"
326         exec_cmd_no_error(node, cmd, timeout=30, sudo=True)
327
328     @staticmethod
329     def set_linux_interface_ip(
330             node, interface, ip_addr, prefix, namespace=None):
331         """Set IP address to interface in linux.
332
333         :param node: VPP/TG node.
334         :param interface: Interface in namespace.
335         :param ip_addr: IP to be set on interface.
336         :param prefix: IP prefix.
337         :param namespace: Execute command in namespace. Optional
338         :type node: dict
339         :type interface: str
340         :type ip_addr: str
341         :type prefix: int
342         :type namespace: str
343         :raises RuntimeError: IP could not be set.
344         """
345         if namespace is not None:
346             cmd = f"ip netns exec {namespace} ip addr add {ip_addr}/{prefix}" \
347                 f" dev {interface}"
348         else:
349             cmd = f"ip addr add {ip_addr}/{prefix} dev {interface}"
350
351         exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
352
353     @staticmethod
354     def add_linux_route(node, ip_addr, prefix, gateway, namespace=None):
355         """Add linux route in namespace.
356
357         :param node: Node where to execute command.
358         :param ip_addr: Route destination IP address.
359         :param prefix: IP prefix.
360         :param namespace: Execute command in namespace. Optional.
361         :param gateway: Gateway address.
362         :type node: dict
363         :type ip_addr: str
364         :type prefix: int
365         :type gateway: str
366         :type namespace: str
367         """
368         if namespace is not None:
369             cmd = f"ip netns exec {namespace} ip route add {ip_addr}/{prefix}" \
370                 f" via {gateway}"
371         else:
372             cmd = f"ip route add {ip_addr}/{prefix} via {gateway}"
373
374         exec_cmd_no_error(node, cmd, sudo=True)
375
376     @staticmethod
377     def vpp_interface_set_ip_address(
378             node, interface, address, prefix_length=None):
379         """Set IP address to VPP interface.
380
381         :param node: VPP node.
382         :param interface: Interface name.
383         :param address: IP address.
384         :param prefix_length: Prefix length.
385         :type node: dict
386         :type interface: str
387         :type address: str
388         :type prefix_length: int
389         """
390         ip_addr = ip_address(address)
391
392         cmd = u"sw_interface_add_del_address"
393         args = dict(
394             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
395             is_add=True,
396             del_all=False,
397             prefix=IPUtil.create_prefix_object(
398                 ip_addr,
399                 prefix_length if prefix_length else 128
400                 if ip_addr.version == 6 else 32
401             )
402         )
403         err_msg = f"Failed to add IP address on interface {interface}"
404
405         with PapiSocketExecutor(node) as papi_exec:
406             papi_exec.add(cmd, **args).get_reply(err_msg)
407
408     @staticmethod
409     def vpp_add_ip_neighbor(node, iface_key, ip_addr, mac_address):
410         """Add IP neighbor on DUT node.
411
412         :param node: VPP node.
413         :param iface_key: Interface key.
414         :param ip_addr: IP address of the interface.
415         :param mac_address: MAC address of the interface.
416         :type node: dict
417         :type iface_key: str
418         :type ip_addr: str
419         :type mac_address: str
420         """
421         dst_ip = ip_address(ip_addr)
422
423         neighbor = dict(
424             sw_if_index=Topology.get_interface_sw_index(node, iface_key),
425             flags=0,
426             mac_address=str(mac_address),
427             ip_address=str(dst_ip)
428         )
429         cmd = u"ip_neighbor_add_del"
430         args = dict(
431             is_add=True,
432             neighbor=neighbor
433         )
434         err_msg = f"Failed to add IP neighbor on interface {iface_key}"
435
436         with PapiSocketExecutor(node) as papi_exec:
437             papi_exec.add(cmd, **args).get_reply(err_msg)
438
439     @staticmethod
440     def union_addr(ip_addr):
441         """Creates union IP address.
442
443         :param ip_addr: IPv4 or IPv6 address.
444         :type ip_addr: IPv4Address or IPv6Address
445         :returns: Union IP address.
446         :rtype: dict
447         """
448         return dict(ip6=ip_addr.packed) if ip_addr.version == 6 \
449             else dict(ip4=ip_addr.packed)
450
451     @staticmethod
452     def create_ip_address_object(ip_addr):
453         """Create IP address object.
454
455         :param ip_addr: IPv4 or IPv6 address
456         :type ip_addr: IPv4Address or IPv6Address
457         :returns: IP address object.
458         :rtype: dict
459         """
460         return dict(
461             af=getattr(
462                 AddressFamily, u"ADDRESS_IP6" if ip_addr.version == 6
463                 else u"ADDRESS_IP4"
464             ).value,
465             un=IPUtil.union_addr(ip_addr)
466         )
467
468     @staticmethod
469     def create_prefix_object(ip_addr, addr_len):
470         """Create prefix object.
471
472         :param ip_addr: IPv4 or IPv6 address.
473         :para, addr_len: Length of IP address.
474         :type ip_addr: IPv4Address or IPv6Address
475         :type addr_len: int
476         :returns: Prefix object.
477         :rtype: dict
478         """
479         addr = IPUtil.create_ip_address_object(ip_addr)
480
481         return dict(
482             len=int(addr_len),
483             address=addr
484         )
485
486     @staticmethod
487     def compose_vpp_route_structure(node, network, prefix_len, **kwargs):
488         """Create route object for ip_route_add_del api call.
489
490         :param node: VPP node.
491         :param network: Route destination network address.
492         :param prefix_len: Route destination network prefix length.
493         :param kwargs: Optional key-value arguments:
494
495             gateway: Route gateway address. (str)
496             interface: Route interface. (str)
497             vrf: VRF table ID. (int)
498             count: number of IP addresses to add starting from network IP (int)
499             local: The route is local with same prefix (increment is 1).
500                 If None, then is not used. (bool)
501             lookup_vrf: VRF table ID for lookup. (int)
502             multipath: Enable multipath routing. (bool)
503             weight: Weight value for unequal cost multipath routing. (int)
504
505         :type node: dict
506         :type network: str
507         :type prefix_len: int
508         :type kwargs: dict
509         :returns: route parameter basic structure
510         :rtype: dict
511         """
512         interface = kwargs.get(u"interface", u"")
513         gateway = kwargs.get(u"gateway", u"")
514
515         net_addr = ip_address(network)
516
517         prefix = IPUtil.create_prefix_object(net_addr, prefix_len)
518
519         paths = list()
520         n_hop = dict(
521             address=IPUtil.union_addr(ip_address(gateway)) if gateway else 0,
522             via_label=MPLS_LABEL_INVALID,
523             obj_id=Constants.BITWISE_NON_ZERO
524         )
525         path = dict(
526             sw_if_index=InterfaceUtil.get_interface_index(node, interface)
527             if interface else Constants.BITWISE_NON_ZERO,
528             table_id=int(kwargs.get(u"lookup_vrf", 0)),
529             rpf_id=Constants.BITWISE_NON_ZERO,
530             weight=int(kwargs.get(u"weight", 1)),
531             preference=1,
532             type=getattr(
533                 FibPathType, u"FIB_PATH_TYPE_LOCAL"
534                 if kwargs.get(u"local", False)
535                 else u"FIB_PATH_TYPE_NORMAL"
536             ).value,
537             flags=getattr(FibPathFlags, u"FIB_PATH_FLAG_NONE").value,
538             proto=getattr(
539                 FibPathNhProto, u"FIB_PATH_NH_PROTO_IP6"
540                 if net_addr.version == 6
541                 else u"FIB_PATH_NH_PROTO_IP4"
542             ).value,
543             nh=n_hop,
544             n_labels=0,
545             label_stack=list(0 for _ in range(16))
546         )
547         paths.append(path)
548
549         route = dict(
550             table_id=int(kwargs.get(u"vrf", 0)),
551             prefix=prefix,
552             n_paths=len(paths),
553             paths=paths
554         )
555         return route
556
557     @staticmethod
558     def vpp_route_add(node, network, prefix_len, **kwargs):
559         """Add route to the VPP node.
560
561         :param node: VPP node.
562         :param network: Route destination network address.
563         :param prefix_len: Route destination network prefix length.
564         :param kwargs: Optional key-value arguments:
565
566             gateway: Route gateway address. (str)
567             interface: Route interface. (str)
568             vrf: VRF table ID. (int)
569             count: number of IP addresses to add starting from network IP (int)
570             local: The route is local with same prefix (increment is 1).
571                 If None, then is not used. (bool)
572             lookup_vrf: VRF table ID for lookup. (int)
573             multipath: Enable multipath routing. (bool)
574             weight: Weight value for unequal cost multipath routing. (int)
575
576         :type node: dict
577         :type network: str
578         :type prefix_len: int
579         :type kwargs: dict
580         """
581         count = kwargs.get(u"count", 1)
582
583         if count > 100:
584             gateway = kwargs.get(u"gateway", '')
585             interface = kwargs.get(u"interface", '')
586             vrf = kwargs.get(u"vrf", None)
587             multipath = kwargs.get(u"multipath", False)
588
589             with VatTerminal(node, json_param=False) as vat:
590
591                 vat.vat_terminal_exec_cmd_from_template(
592                     u"vpp_route_add.vat",
593                     network=network,
594                     prefix_length=prefix_len,
595                     via=f"via {gateway}" if gateway else u"",
596                     sw_if_index=f"sw_if_index "
597                     f"{InterfaceUtil.get_interface_index(node, interface)}"
598                     if interface else u"",
599                     vrf=f"vrf {vrf}" if vrf else u"",
600                     count=f"count {count}" if count else u"",
601                     multipath=u"multipath" if multipath else u""
602                 )
603             return
604
605         net_addr = ip_address(network)
606         cmd = u"ip_route_add_del"
607         args = dict(
608             is_add=True,
609             is_multipath=kwargs.get(u"multipath", False),
610             route=None
611         )
612         err_msg = f"Failed to add route(s) on host {node[u'host']}"
613
614         with PapiSocketExecutor(node) as papi_exec:
615             for i in range(kwargs.get(u"count", 1)):
616                 args[u"route"] = IPUtil.compose_vpp_route_structure(
617                     node, net_addr + i, prefix_len, **kwargs
618                 )
619                 history = bool(not 1 < i < kwargs.get(u"count", 1))
620                 papi_exec.add(cmd, history=history, **args)
621             papi_exec.get_replies(err_msg)
622
623     @staticmethod
624     def flush_ip_addresses(node, interface):
625         """Flush all IP addresses from specified interface.
626
627         :param node: VPP node.
628         :param interface: Interface name.
629         :type node: dict
630         :type interface: str
631         """
632         cmd = u"sw_interface_add_del_address"
633         args = dict(
634             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
635             is_add=False,
636             del_all=True
637         )
638         err_msg = f"Failed to flush IP address on interface {interface}"
639
640         with PapiSocketExecutor(node) as papi_exec:
641             papi_exec.add(cmd, **args).get_reply(err_msg)
642
643     @staticmethod
644     def add_fib_table(node, table_id, ipv6=False):
645         """Create new FIB table according to ID.
646
647         :param node: Node to add FIB on.
648         :param table_id: FIB table ID.
649         :param ipv6: Is this an IPv6 table
650         :type node: dict
651         :type table_id: int
652         :type ipv6: bool
653         """
654         cmd = u"ip_table_add_del"
655         table = dict(
656             table_id=int(table_id),
657             is_ip6=ipv6
658         )
659         args = dict(
660             table=table,
661             is_add=True
662         )
663         err_msg = f"Failed to add FIB table on host {node[u'host']}"
664
665         with PapiSocketExecutor(node) as papi_exec:
666             papi_exec.add(cmd, **args).get_reply(err_msg)