CSIT-1459: Migrate IP libraries from VAT to PAPI
[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 socket import AF_INET, AF_INET6, inet_ntop, inet_pton
19
20 from ipaddress import ip_address
21 from ipaddress import IPv4Network, IPv6Network, IPv4Address, IPv6Address
22 from ipaddress import AddressValueError, NetmaskValueError
23
24 from resources.libraries.python.Constants import Constants
25 from resources.libraries.python.InterfaceUtil import InterfaceUtil
26 from resources.libraries.python.PapiExecutor import PapiExecutor
27 from resources.libraries.python.ssh import exec_cmd_no_error, exec_cmd
28 from resources.libraries.python.topology import NodeType, Topology
29
30
31 class IPUtil(object):
32     """Common IP utilities"""
33
34     @staticmethod
35     def ip_to_int(ip_str):
36         """Convert IP address from string format (e.g. 10.0.0.1) to integer
37         representation (167772161).
38
39         :param ip_str: IP address in string representation.
40         :type ip_str: str
41         :returns: Integer representation of IP address.
42         :rtype: int
43         """
44         return int(ip_address(unicode(ip_str)))
45
46     @staticmethod
47     def int_to_ip(ip_int):
48         """Convert IP address from integer representation (e.g. 167772161) to
49         string format (10.0.0.1).
50
51         :param ip_int: IP address in integer representation.
52         :type ip_int: int
53         :returns: String representation of IP address.
54         :rtype: str
55         """
56         return str(ip_address(ip_int))
57
58     @staticmethod
59     def vpp_get_interface_ip_addresses(node, interface, ip_version):
60         """Get list of IP addresses from an interface on a VPP node.
61
62         :param node: VPP node.
63         :param interface: Name of an interface on the VPP node.
64         :param ip_version: IP protocol version (ipv4 or ipv6).
65         :type node: dict
66         :type interface: str
67         :type ip_version: str
68         :returns: List of dictionaries, each containing IP address, subnet
69             prefix length and also the subnet mask for ipv4 addresses.
70             Note: A single interface may have multiple IP addresses assigned.
71         :rtype: list
72         """
73         try:
74             sw_if_index = Topology.convert_interface_reference(
75                 node, interface, 'sw_if_index')
76         except RuntimeError:
77             if isinstance(interface, basestring):
78                 sw_if_index = InterfaceUtil.get_sw_if_index(node, interface)
79             else:
80                 raise
81
82         is_ipv6 = 1 if ip_version == 'ipv6' else 0
83
84         cmd = 'ip_address_dump'
85         cmd_reply = 'ip_address_details'
86         args = dict(sw_if_index=sw_if_index,
87                     is_ipv6=is_ipv6)
88         err_msg = 'Failed to get L2FIB dump on host {host}'.format(
89             host=node['host'])
90
91         with PapiExecutor(node) as papi_exec:
92             papi_resp = papi_exec.add(cmd, **args).get_dump(err_msg)
93
94         data = list()
95         for item in papi_resp.reply[0]['api_reply']:
96             item[cmd_reply]['ip'] = inet_ntop(AF_INET6, item[cmd_reply]['ip']) \
97                 if is_ipv6 else inet_ntop(AF_INET, item[cmd_reply]['ip'][0:4])
98             item[cmd_reply]['netmask'] = str(
99                 IPv6Network(unicode('::/{pl}'.format(
100                     pl=item[cmd_reply]['prefix_length']))).netmask) if is_ipv6 \
101                 else str(IPv4Network(unicode('0.0.0.0/{pl}'.format(
102                     pl=item[cmd_reply]['prefix_length']))).netmask)
103             data.append(item[cmd_reply])
104
105         return data
106
107     @staticmethod
108     def get_interface_vrf_table(node, interface, ip_version='ipv4'):
109         """Get vrf ID for the given interface.
110
111         :param node: VPP node.
112         :param interface: Name or sw_if_index of a specific interface.
113         :type node: dict
114         :param ip_version: IP protocol version (ipv4 or ipv6).
115         :type interface: str or int
116         :type ip_version: str
117         :returns: vrf ID of the specified interface.
118         :rtype: int
119         """
120         if isinstance(interface, basestring):
121             sw_if_index = InterfaceUtil.get_sw_if_index(node, interface)
122         else:
123             sw_if_index = interface
124
125         is_ipv6 = 1 if ip_version == 'ipv6' else 0
126
127         cmd = 'sw_interface_get_table'
128         args = dict(sw_if_index=sw_if_index,
129                     is_ipv6=is_ipv6)
130         err_msg = 'Failed to get VRF id assigned to interface {ifc}'.format(
131             ifc=interface)
132
133         with PapiExecutor(node) as papi_exec:
134             papi_resp = papi_exec.add(cmd, **args).get_replies(err_msg). \
135                 verify_reply(err_msg=err_msg)
136
137         return papi_resp['vrf_id']
138
139     @staticmethod
140     def vpp_ip_source_check_setup(node, if_name):
141         """Setup Reverse Path Forwarding source check on interface.
142
143         :param node: VPP node.
144         :param if_name: Interface name to setup RPF source check.
145         :type node: dict
146         :type if_name: str
147         """
148         cmd = 'ip_source_check_interface_add_del'
149         args = dict(
150             sw_if_index=InterfaceUtil.get_interface_index(node, if_name),
151             is_add=1,
152             loose=0)
153         err_msg = 'Failed to enable source check on interface {ifc}'.format(
154             ifc=if_name)
155         with PapiExecutor(node) as papi_exec:
156             papi_exec.add(cmd, **args).get_replies(err_msg). \
157                 verify_reply(err_msg=err_msg)
158
159     @staticmethod
160     def vpp_ip_probe(node, interface, addr):
161         """Run ip probe on VPP node.
162
163         :param node: VPP node.
164         :param interface: Interface key or name.
165         :param addr: IPv4/IPv6 address.
166         :type node: dict
167         :type interface: str
168         :type addr: str
169         """
170         cmd = 'ip_probe_neighbor'
171         cmd_reply = 'proxy_arp_intfc_enable_disable_reply'
172         args = dict(
173             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
174             dst=str(addr))
175         err_msg = 'VPP ip probe {dev} {ip} failed on {h}'.format(
176             dev=interface, ip=addr, h=node['host'])
177
178         with PapiExecutor(node) as papi_exec:
179             papi_exec.add(cmd, **args).get_replies(err_msg). \
180                 verify_reply(cmd_reply=cmd_reply, err_msg=err_msg)
181
182     @staticmethod
183     def ip_addresses_should_be_equal(ip1, ip2):
184         """Fails if the given IP addresses are unequal.
185
186         :param ip1: IPv4 or IPv6 address.
187         :param ip2: IPv4 or IPv6 address.
188         :type ip1: str
189         :type ip2: str
190         """
191         addr1 = ip_address(unicode(ip1))
192         addr2 = ip_address(unicode(ip2))
193
194         if addr1 != addr2:
195             raise AssertionError('IP addresses are not equal: {0} != {1}'.
196                                  format(ip1, ip2))
197
198     @staticmethod
199     def setup_network_namespace(node, namespace_name, interface_name,
200                                 ip_addr, prefix):
201         """Setup namespace on given node and attach interface and IP to
202         this namespace. Applicable also on TG node.
203
204         :param node: VPP node.
205         :param namespace_name: Namespace name.
206         :param interface_name: Interface name.
207         :param ip_addr: IP address of namespace's interface.
208         :param prefix: IP address prefix length.
209         :type node: dict
210         :type namespace_name: str
211         :type interface_name: str
212         :type ip_addr: str
213         :type prefix: int
214         """
215         cmd = ('ip netns add {0}'.format(namespace_name))
216         exec_cmd_no_error(node, cmd, sudo=True)
217
218         cmd = ('ip link set dev {0} up netns {1}'.format(interface_name,
219                                                          namespace_name))
220         exec_cmd_no_error(node, cmd, sudo=True)
221
222         cmd = ('ip netns exec {0} ip addr add {1}/{2} dev {3}'.format(
223             namespace_name, ip_addr, prefix, interface_name))
224         exec_cmd_no_error(node, cmd, sudo=True)
225
226     @staticmethod
227     def linux_enable_forwarding(node, ip_ver='ipv4'):
228         """Enable forwarding on a Linux node, e.g. VM.
229
230         :param node: VPP node.
231         :param ip_ver: IP version, 'ipv4' or 'ipv6'.
232         :type node: dict
233         :type ip_ver: str
234         """
235         cmd = 'sysctl -w net.{0}.ip_forward=1'.format(ip_ver)
236         exec_cmd_no_error(node, cmd, sudo=True)
237
238     @staticmethod
239     def get_linux_interface_name(node, pci_addr):
240         """Get the interface name.
241
242         :param node: VPP/TG node.
243         :param pci_addr: PCI address
244         :type node: dict
245         :type pci_addr: str
246         :returns: Interface name
247         :rtype: str
248         :raises RuntimeError: If cannot get the information about interfaces.
249         """
250         regex_intf_info = r"pci@" \
251                           r"([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s*" \
252                           r"([a-zA-Z0-9]*)\s*network"
253
254         cmd = "lshw -class network -businfo"
255         ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
256         if ret_code != 0:
257             raise RuntimeError('Could not get information about interfaces:\n'
258                                '{err}'.format(err=stderr))
259
260         for line in stdout.splitlines()[2:]:
261             try:
262                 if re.search(regex_intf_info, line).group(1) == pci_addr:
263                     return re.search(regex_intf_info, line).group(2)
264             except AttributeError:
265                 continue
266         return None
267
268     @staticmethod
269     def set_linux_interface_up(node, interface):
270         """Set the specified interface up.
271
272         :param node: VPP/TG node.
273         :param interface: Interface in namespace.
274         :type node: dict
275         :type interface: str
276         :raises RuntimeError: If the interface could not be set up.
277         """
278         cmd = "ip link set {0} up".format(interface)
279         exec_cmd_no_error(node, cmd, timeout=30, sudo=True)
280
281     @staticmethod
282     def set_linux_interface_ip(node, interface, ip_addr, prefix,
283                                namespace=None):
284         """Set IP address to interface in linux.
285
286         :param node: VPP/TG node.
287         :param interface: Interface in namespace.
288         :param ip_addr: IP to be set on interface.
289         :param prefix: IP prefix.
290         :param namespace: Execute command in namespace. Optional
291         :type node: dict
292         :type interface: str
293         :type ip_addr: str
294         :type prefix: int
295         :type namespace: str
296         :raises RuntimeError: IP could not be set.
297         """
298         if namespace is not None:
299             cmd = 'ip netns exec {ns} ip addr add {ip}/{p} dev {dev}'.format(
300                 ns=namespace, ip=ip_addr, p=prefix, dev=interface)
301         else:
302             cmd = 'ip addr add {ip}/{p} dev {dev}'.format(
303                 ip=ip_addr, p=prefix, dev=interface)
304
305         exec_cmd_no_error(node, cmd, timeout=5, sudo=True)
306
307     @staticmethod
308     def add_linux_route(node, ip_addr, prefix, gateway, namespace=None):
309         """Add linux route in namespace.
310
311         :param node: Node where to execute command.
312         :param ip_addr: Route destination IP address.
313         :param prefix: IP prefix.
314         :param namespace: Execute command in namespace. Optional.
315         :param gateway: Gateway address.
316         :type node: dict
317         :type ip_addr: str
318         :type prefix: int
319         :type gateway: str
320         :type namespace: str
321         """
322         if namespace is not None:
323             cmd = 'ip netns exec {} ip route add {}/{} via {}'.format(
324                 namespace, ip_addr, prefix, gateway)
325         else:
326             cmd = 'ip route add {}/{} via {}'.format(ip_addr, prefix, gateway)
327         exec_cmd_no_error(node, cmd, sudo=True)
328
329     @staticmethod
330     def vpp_interface_set_ip_address(node, interface, address,
331                                      prefix_length=None):
332         """Set IP address to VPP interface.
333
334         :param node: VPP node.
335         :param interface: Interface name.
336         :param address: IP address.
337         :param prefix_length: Prefix length.
338         :type node: dict
339         :type interface: str
340         :type address: str
341         :type prefix_length: int
342         """
343         try:
344             ip_addr = IPv6Address(unicode(address))
345             af_inet = AF_INET6
346             is_ipv6 = 1
347         except (AddressValueError, NetmaskValueError):
348             ip_addr = IPv4Address(unicode(address))
349             af_inet = AF_INET
350             is_ipv6 = 0
351
352         cmd = 'sw_interface_add_del_address'
353         args = dict(
354             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
355             is_add=1,
356             is_ipv6=is_ipv6,
357             del_all=0,
358             address_length=int(prefix_length) if prefix_length else 128
359             if is_ipv6 else 32,
360             address=inet_pton(af_inet, str(ip_addr)))
361         err_msg = 'Failed to add IP address on interface {ifc}'.format(
362             ifc=interface)
363         with PapiExecutor(node) as papi_exec:
364             papi_exec.add(cmd, **args).get_replies(err_msg). \
365                 verify_reply(err_msg=err_msg)
366
367     @staticmethod
368     def vpp_add_ip_neighbor(node, iface_key, ip_addr, mac_address):
369         """Add IP neighbor on DUT node.
370
371         :param node: VPP node.
372         :param iface_key: Interface key.
373         :param ip_addr: IP address of the interface.
374         :param mac_address: MAC address of the interface.
375         :type node: dict
376         :type iface_key: str
377         :type ip_addr: str
378         :type mac_address: str
379         """
380         try:
381             dst_ip = IPv6Address(unicode(ip_addr))
382         except (AddressValueError, NetmaskValueError):
383             dst_ip = IPv4Address(unicode(ip_addr))
384
385         neighbor = dict(
386             sw_if_index=Topology.get_interface_sw_index(
387                 node, iface_key),
388             flags=0,
389             mac_address=str(mac_address),
390             ip_address=str(dst_ip))
391         cmd = 'ip_neighbor_add_del'
392         args = dict(
393             is_add=1,
394             neighbor=neighbor)
395         err_msg = 'Failed to add IP neighbor on interface {ifc}'.format(
396             ifc=iface_key)
397         with PapiExecutor(node) as papi_exec:
398             papi_exec.add(cmd, **args).get_replies(err_msg). \
399                 verify_reply(err_msg=err_msg)
400
401     @staticmethod
402     def vpp_route_add(node, network, prefix_len, **kwargs):
403         """Add route to the VPP node.
404
405         :param node: VPP node.
406         :param network: Route destination network address.
407         :param prefix_len: Route destination network prefix length.
408         :param kwargs: Optional key-value arguments:
409
410             gateway: Route gateway address. (str)
411             interface: Route interface. (str)
412             vrf: VRF table ID. (int)
413             count: number of IP addresses to add starting from network IP (int)
414             local: The route is local with same prefix (increment is 1).
415                 If None, then is not used. (bool)
416             lookup_vrf: VRF table ID for lookup. (int)
417             multipath: Enable multipath routing. (bool)
418             weight: Weight value for unequal cost multipath routing. (int)
419
420         :type node: dict
421         :type network: str
422         :type prefix_len: int
423         :type kwargs: dict
424         """
425         interface = kwargs.get('interface', None)
426         gateway = kwargs.get('gateway', None)
427
428         try:
429             net_addr = IPv6Address(unicode(network))
430             af_inet = AF_INET6
431             is_ipv6 = 1
432         except (AddressValueError, NetmaskValueError):
433             net_addr = IPv4Address(unicode(network))
434             af_inet = AF_INET
435             is_ipv6 = 0
436
437         if gateway:
438             try:
439                 gt_addr = IPv6Address(unicode(gateway))
440                 af_inet_gt = AF_INET6
441             except (AddressValueError, NetmaskValueError):
442                 gt_addr = IPv4Address(unicode(gateway))
443                 af_inet_gt = AF_INET
444
445         cmd = 'ip_add_del_route'
446         args = dict(
447             next_hop_sw_if_index=InterfaceUtil.get_interface_index(
448                 node, interface) if interface else Constants.BITWISE_NON_ZERO,
449             table_id=int(kwargs.get('vrf', 0)),
450             is_add=1,
451             is_ipv6=is_ipv6,
452             is_local=int(kwargs.get('local', False)),
453             is_multipath=int(kwargs.get('multipath', False)),
454             next_hop_weight=int(kwargs.get('weight', 1)),
455             next_hop_proto=1 if is_ipv6 else 0,
456             dst_address_length=int(prefix_len),
457             next_hop_address=inet_pton(af_inet_gt, str(gt_addr)) if gateway
458             else 0,
459             next_hop_table_id=int(kwargs.get('lookup_vrf', 0)))
460         err_msg = 'Failed to add route(s) on host {host}'.format(
461             host=node['host'])
462         with PapiExecutor(node) as papi_exec:
463             for i in xrange(kwargs.get('count', 1)):
464                 papi_exec.add(cmd, dst_address=inet_pton(
465                     af_inet, str(net_addr+i)), **args)
466             papi_exec.get_replies(err_msg).verify_replies(err_msg=err_msg)
467
468     @staticmethod
469     def vpp_nodes_set_ipv4_addresses(nodes, nodes_addr):
470         """Set IPv4 addresses on all VPP nodes in topology.
471
472         :param nodes: Nodes of the test topology.
473         :param nodes_addr: Available nodes IPv4 addresses.
474         :type nodes: dict
475         :type nodes_addr: dict
476         :returns: Affected interfaces as list of (node, interface) tuples.
477         :rtype: list
478         """
479         interfaces = []
480         for net in nodes_addr.values():
481             for port in net['ports'].values():
482                 host = port.get('node')
483                 if host is None:
484                     continue
485                 topo = Topology()
486                 node = topo.get_node_by_hostname(nodes, host)
487                 if node is None:
488                     continue
489                 if node['type'] != NodeType.DUT:
490                     continue
491                 iface_key = topo.get_interface_by_name(node, port['if'])
492                 IPUtil.vpp_interface_set_ip_address(
493                     node, iface_key, port['addr'], net['prefix'])
494                 interfaces.append((node, port['if']))
495
496         return interfaces
497
498     @staticmethod
499     def flush_ip_addresses(node, interface):
500         """Flush all IPv4addresses from specified interface.
501
502         :param node: VPP node.
503         :param interface: Interface name.
504         :type node: dict
505         :type interface: str
506         """
507         cmd = 'sw_interface_add_del_address'
508         args = dict(
509             sw_if_index=InterfaceUtil.get_interface_index(node, interface),
510             del_all=1)
511         err_msg = 'Failed to flush IP address on interface {ifc}'.format(
512             ifc=interface)
513         with PapiExecutor(node) as papi_exec:
514             papi_exec.add(cmd, **args).get_replies(err_msg). \
515                 verify_reply(err_msg=err_msg)
516
517     @staticmethod
518     def add_fib_table(node, table_id, ipv6=False):
519         """Create new FIB table according to ID.
520
521         :param node: Node to add FIB on.
522         :param table_id: FIB table ID.
523         :param ipv6: Is this an IPv6 table
524         :type node: dict
525         :type table_id: int
526         :type ipv6: bool
527         """
528         cmd = 'ip_table_add_del'
529         args = dict(
530             table_id=int(table_id),
531             is_ipv6=int(ipv6),
532             is_add=1)
533         err_msg = 'Failed to add FIB table on host {host}'.format(
534             host=node['host'])
535         with PapiExecutor(node) as papi_exec:
536             papi_exec.add(cmd, **args).get_replies(err_msg). \
537                 verify_reply(err_msg=err_msg)