feat(tests): IPsecHW rxq ratio
[csit.git] / resources / libraries / python / NATUtil.py
1 # Copyright (c) 2023 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 """NAT utilities library."""
15
16 from math import log2, modf
17 from pprint import pformat
18 from enum import IntEnum
19
20 from ipaddress import IPv4Address
21 from robot.api import logger
22
23 from resources.libraries.python.Constants import Constants
24 from resources.libraries.python.InterfaceUtil import InterfaceUtil
25 from resources.libraries.python.topology import Topology
26 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
27
28
29 class NatConfigFlags(IntEnum):
30     """NAT plugin configuration flags"""
31     NAT_IS_NONE = 0x00
32     NAT_IS_TWICE_NAT = 0x01
33     NAT_IS_SELF_TWICE_NAT = 0x02
34     NAT_IS_OUT2IN_ONLY = 0x04
35     NAT_IS_ADDR_ONLY = 0x08
36     NAT_IS_OUTSIDE = 0x10
37     NAT_IS_INSIDE = 0x20
38     NAT_IS_STATIC = 0x40
39     NAT_IS_EXT_HOST_VALID = 0x80
40
41
42 class Nat44ConfigFlags(IntEnum):
43     """NAT44 configuration flags"""
44     NAT44_IS_ENDPOINT_INDEPENDENT = 0x00
45     NAT44_IS_ENDPOINT_DEPENDENT = 0x01
46     NAT44_IS_STATIC_MAPPING_ONLY = 0x02
47     NAT44_IS_CONNECTION_TRACKING = 0x04
48     NAT44_IS_OUT2IN_DPO = 0x08
49
50
51 class NatAddrPortAllocAlg(IntEnum):
52     """NAT Address and port assignment algorithms."""
53     NAT_ALLOC_ALG_DEFAULT = 0
54     NAT_ALLOC_ALG_MAP_E = 1
55     NAT_ALLOC_ALG_PORT_RANGE = 2
56
57
58 class NATUtil:
59     """This class defines the methods to set NAT."""
60
61     def __init__(self):
62         pass
63
64     @staticmethod
65     def enable_nat44_ed_plugin(
66             node, inside_vrf=0, outside_vrf=0, sessions=0, session_memory=0,
67             mode=u""):
68         """Enable NAT44 plugin.
69
70         :param node: DUT node.
71         :param inside_vrf: Inside VRF ID.
72         :param outside_vrf: Outside VRF ID.
73         :param sessions: Maximum number of sessions.
74         :param session_memory: Session memory size - overwrite auto calculated
75             hash allocation parameter if non-zero.
76         :param mode: NAT44 mode. Valid values:
77             - endpoint-independent
78             - endpoint-dependent
79             - static-mapping-only
80             - connection-tracking
81             - out2in-dpo
82         :type node: dict
83         :type inside_vrf: str or int
84         :type outside_vrf: str or int
85         :type sessions: str or int
86         :type session_memory: str or int
87         :type mode: str
88         """
89         cmd = u"nat44_ed_plugin_enable_disable"
90         err_msg = f"Failed to enable NAT44 plugin on the host {node[u'host']}!"
91         args_in = dict(
92             enable=True,
93             inside_vrf=int(inside_vrf),
94             outside_vrf=int(outside_vrf),
95             sessions=int(sessions),
96             session_memory=int(session_memory),
97             flags=getattr(
98                 Nat44ConfigFlags,
99                 f"NAT44_IS_{mode.replace(u'-', u'_').upper()}"
100             ).value
101         )
102
103         with PapiSocketExecutor(node) as papi_exec:
104             papi_exec.add(cmd, **args_in).get_reply(err_msg)
105
106     @staticmethod
107     def set_nat44_interface(node, interface, flag):
108         """Set inside and outside interfaces for NAT44.
109
110         :param node: DUT node.
111         :param interface: NAT44 interface.
112         :param flag: Interface NAT configuration flag name.
113         :type node: dict
114         :type interface: str
115         :type flag: str
116         """
117         cmd = u"nat44_interface_add_del_feature"
118
119         err_msg = f"Failed to set {flag} interface {interface} for NAT44 " \
120             f"on host {node[u'host']}"
121         args_in = dict(
122             sw_if_index=InterfaceUtil.get_sw_if_index(node, interface),
123             is_add=1,
124             flags=getattr(NatConfigFlags, flag).value
125         )
126
127         with PapiSocketExecutor(node) as papi_exec:
128             papi_exec.add(cmd, **args_in).get_reply(err_msg)
129
130     @staticmethod
131     def set_nat44_interfaces(node, int_in, int_out):
132         """Set inside and outside interfaces for NAT44.
133
134         :param node: DUT node.
135         :param int_in: Inside interface.
136         :param int_out: Outside interface.
137         :type node: dict
138         :type int_in: str
139         :type int_out: str
140         """
141         NATUtil.set_nat44_interface(node, int_in, u"NAT_IS_INSIDE")
142         NATUtil.set_nat44_interface(node, int_out, u"NAT_IS_OUTSIDE")
143
144     @staticmethod
145     def set_nat44_address_range(
146             node, start_ip, end_ip, vrf_id=Constants.BITWISE_NON_ZERO,
147             flag=u"NAT_IS_NONE"):
148         """Set NAT44 address range.
149
150         The return value is a callable (zero argument Python function)
151         which can be used to reset NAT state, so repeated trial measurements
152         hit the same slow path.
153
154         :param node: DUT node.
155         :param start_ip: IP range start.
156         :param end_ip: IP range end.
157         :param vrf_id: VRF index (Optional).
158         :param flag: NAT flag name.
159         :type node: dict
160         :type start_ip: str
161         :type end_ip: str
162         :type vrf_id: int
163         :type flag: str
164         :returns: Resetter of the NAT state.
165         :rtype: Callable[[], None]
166         """
167         cmd = u"nat44_add_del_address_range"
168         err_msg = f"Failed to set NAT44 address range on host {node[u'host']}"
169         args_in = dict(
170             is_add=True,
171             first_ip_address=IPv4Address(str(start_ip)).packed,
172             last_ip_address=IPv4Address(str(end_ip)).packed,
173             vrf_id=vrf_id,
174             flags=getattr(NatConfigFlags, flag).value
175         )
176
177         with PapiSocketExecutor(node) as papi_exec:
178             papi_exec.add(cmd, **args_in).get_reply(err_msg)
179
180         # A closure, accessing the variables above.
181         def resetter():
182             """Delete and re-add the NAT range setting."""
183             with PapiSocketExecutor(node) as papi_exec:
184                 args_in[u"is_add"] = False
185                 papi_exec.add(cmd, **args_in).get_reply(err_msg)
186                 args_in[u"is_add"] = True
187                 papi_exec.add(cmd, **args_in).get_reply(err_msg)
188
189         return resetter
190
191     @staticmethod
192     def show_nat44_config(node):
193         """Show the NAT44 plugin running configuration.
194
195         :param node: DUT node.
196         :type node: dict
197         """
198         cmd = u"nat44_show_running_config"
199         err_msg = f"Failed to get NAT44 configuration on host {node[u'host']}"
200
201         with PapiSocketExecutor(node) as papi_exec:
202             reply = papi_exec.add(cmd).get_reply(err_msg)
203
204         logger.debug(f"NAT44 Configuration:\n{pformat(reply)}")
205
206     @staticmethod
207     def show_nat44_summary(node):
208         """Show NAT44 summary on the specified topology node.
209
210         :param node: Topology node.
211         :type node: dict
212         :returns: NAT44 summary data.
213         :rtype: str
214         """
215         return PapiSocketExecutor.run_cli_cmd(node, u"show nat44 summary")
216
217     @staticmethod
218     def show_nat_base_data(node):
219         """Show the NAT base data.
220
221         Used data sources:
222
223             nat_worker_dump
224             nat44_interface_addr_dump
225             nat44_address_dump
226             nat44_static_mapping_dump
227             nat44_interface_dump
228
229         :param node: DUT node.
230         :type node: dict
231         """
232         cmds = [
233             u"nat_worker_dump",
234             u"nat44_interface_addr_dump",
235             u"nat44_address_dump",
236             u"nat44_static_mapping_dump",
237             u"nat44_interface_dump",
238         ]
239         PapiSocketExecutor.dump_and_log(node, cmds)
240
241     @staticmethod
242     def show_nat_user_data(node):
243         """Show the NAT user data.
244
245         Used data sources:
246
247             nat44_user_dump
248             nat44_user_session_dump
249
250         :param node: DUT node.
251         :type node: dict
252         """
253         cmds = [
254             u"nat44_user_dump",
255             u"nat44_user_session_dump",
256         ]
257         PapiSocketExecutor.dump_and_log(node, cmds)
258
259     @staticmethod
260     def compute_max_translations_per_thread(sessions, threads):
261         """Compute value of max_translations_per_thread NAT44 parameter based on
262         total number of worker threads.
263
264         :param sessions: Required number of NAT44 sessions.
265         :param threads: Number of worker threads.
266         :type sessions: int
267         :type threads: int
268         :returns: Value of max_translations_per_thread NAT44 parameter.
269         :rtype: int
270         """
271         # vpp-device tests have not dedicated physical core so
272         # ${dp_count_int} == 0 but we need to use one thread
273         threads = 1 if not int(threads) else int(threads)
274         rest, mult = modf(log2(sessions/(10*threads)))
275         return 2 ** (int(mult) + (1 if rest else 0)) * 10
276
277     @staticmethod
278     def get_nat44_sessions_number(node, proto):
279         """Get number of expected NAT44 sessions from NAT44 mapping data.
280
281         This keyword uses output from a CLI command,
282         so it can start failing when VPP changes the output format.
283         TODO: Switch to API (or stat segment) when available.
284
285         The current implementation supports both 2202 and post-2202 format.
286         (The Gerrit number changing the output format is 34877.)
287
288         For TCP proto, the expected state after rampup is
289         some number of sessions in transitory state (VPP has seen the FINs),
290         and some number of sessions in established state (meaning
291         some FINs were lost in the last trial).
292         While the two states may need slightly different number of cycles
293         to process next packet, the current implementation considers
294         both of them the "fast path", so they are both counted as expected.
295
296         As the tests should fail if a session is timed-out,
297         the logic substracts timed out sessions for the returned value
298         (only available for post-2202 format).
299
300         TODO: Investigate if it is worth to insert additional rampup trials
301         in TPUT tests to ensure all sessions are transitory before next
302         measurement.
303
304         :param node: DUT node.
305         :param proto: Required protocol - TCP/UDP/ICMP.
306         :type node: dict
307         :type proto: str
308         :returns: Number of active established NAT44 sessions.
309         :rtype: int
310         :raises ValueError: If not supported protocol.
311         :raises RuntimeError: If output is not formatted as expected.
312         """
313         proto_l = proto.strip().lower()
314         if proto_l not in [u"udp", u"tcp", u"icmp"]:
315             raise ValueError(f"Unsupported protocol: {proto}!")
316         summary_text = NATUtil.show_nat44_summary(node)
317         summary_lines = summary_text.splitlines()
318         # Output from VPP v22.02 and before, delete when no longer needed.
319         pattern_2202 = f"total {proto_l} sessions:"
320         if pattern_2202 in summary_text:
321             for line in summary_lines:
322                 if pattern_2202 not in line:
323                     continue
324                 return int(line.split(u":", 1)[1].strip())
325         # Post-2202, the proto info and session info are not on the same line.
326         found = False
327         for line in summary_lines:
328             if not found:
329                 if f"{proto_l} sessions:" in line:
330                     found = True
331                 continue
332             # Proto is found, find the line we are interested in.
333             if u"total" not in line:
334                 raise RuntimeError(f"show nat summary: no {proto} total.")
335             # We have the line with relevant numbers.
336             total_part, timed_out_part = line.split(u"(", 1)
337             timed_out_part = timed_out_part.split(u")", 1)[0]
338             total_count = int(total_part.split(u":", 1)[1].strip())
339             timed_out_count = int(timed_out_part.split(u":", 1)[1].strip())
340             active_count = total_count - timed_out_count
341             return active_count
342         raise RuntimeError(u"Unknown format of show nat44 summary")
343
344     # DET44 PAPI calls
345     # DET44 means deterministic mode of NAT44
346     @staticmethod
347     def enable_det44_plugin(node, inside_vrf=0, outside_vrf=0):
348         """Enable DET44 plugin.
349
350         :param node: DUT node.
351         :param inside_vrf: Inside VRF ID.
352         :param outside_vrf: Outside VRF ID.
353         :type node: dict
354         :type inside_vrf: str or int
355         :type outside_vrf: str or int
356         """
357         cmd = u"det44_plugin_enable_disable"
358         err_msg = f"Failed to enable DET44 plugin on the host {node[u'host']}!"
359         args_in = dict(
360             enable=True,
361             inside_vrf=int(inside_vrf),
362             outside_vrf=int(outside_vrf)
363         )
364
365         with PapiSocketExecutor(node) as papi_exec:
366             papi_exec.add(cmd, **args_in).get_reply(err_msg)
367
368     @staticmethod
369     def set_det44_interface(node, if_key, is_inside):
370         """Enable DET44 feature on the interface.
371
372         :param node: DUT node.
373         :param if_key: Interface key from topology file of interface
374             to enable DET44 feature on.
375         :param is_inside: True if interface is inside, False if outside.
376         :type node: dict
377         :type if_key: str
378         :type is_inside: bool
379         """
380         cmd = u"det44_interface_add_del_feature"
381         err_msg = f"Failed to enable DET44 feature on the interface {if_key} " \
382             f"on the host {node[u'host']}!"
383         args_in = dict(
384             is_add=True,
385             is_inside=is_inside,
386             sw_if_index=Topology.get_interface_sw_index(node, if_key)
387         )
388
389         with PapiSocketExecutor(node) as papi_exec:
390             papi_exec.add(cmd, **args_in).get_reply(err_msg)
391
392     @staticmethod
393     def set_det44_mapping(node, ip_in, subnet_in, ip_out, subnet_out):
394         """Set DET44 mapping.
395
396         The return value is a callable (zero argument Python function)
397         which can be used to reset NAT state, so repeated trial measurements
398         hit the same slow path.
399
400         :param node: DUT node.
401         :param ip_in: Inside IP.
402         :param subnet_in: Inside IP subnet.
403         :param ip_out: Outside IP.
404         :param subnet_out: Outside IP subnet.
405         :type node: dict
406         :type ip_in: str
407         :type subnet_in: str or int
408         :type ip_out: str
409         :type subnet_out: str or int
410         """
411         cmd = u"det44_add_del_map"
412         err_msg = f"Failed to set DET44 mapping on the host {node[u'host']}!"
413         args_in = dict(
414             is_add=True,
415             in_addr=IPv4Address(str(ip_in)).packed,
416             in_plen=int(subnet_in),
417             out_addr=IPv4Address(str(ip_out)).packed,
418             out_plen=int(subnet_out)
419         )
420
421         with PapiSocketExecutor(node) as papi_exec:
422             papi_exec.add(cmd, **args_in).get_reply(err_msg)
423
424         # A closure, accessing the variables above.
425         def resetter():
426             """Delete and re-add the deterministic NAT mapping."""
427             with PapiSocketExecutor(node) as papi_exec:
428                 args_in[u"is_add"] = False
429                 papi_exec.add(cmd, **args_in).get_reply(err_msg)
430                 args_in[u"is_add"] = True
431                 papi_exec.add(cmd, **args_in).get_reply(err_msg)
432
433         return resetter
434
435     @staticmethod
436     def get_det44_mapping(node):
437         """Get DET44 mapping data.
438
439         :param node: DUT node.
440         :type node: dict
441         :returns: Dictionary of DET44 mapping data.
442         :rtype: dict
443         """
444         cmd = u"det44_map_dump"
445         err_msg = f"Failed to get DET44 mapping data on the host " \
446             f"{node[u'host']}!"
447         args_in = dict()
448         with PapiSocketExecutor(node) as papi_exec:
449             details = papi_exec.add(cmd, **args_in).get_reply(err_msg)
450
451         return details
452
453     @staticmethod
454     def get_det44_sessions_number(node):
455         """Get number of established DET44 sessions from actual DET44 mapping
456         data.
457         :param node: DUT node.
458         :type node: dict
459         :returns: Number of established DET44 sessions.
460         :rtype: int
461         """
462         det44_data = NATUtil.get_det44_mapping(node)
463         return det44_data.get(u"ses_num", 0)
464
465     @staticmethod
466     def show_det44(node):
467         """Show DET44 data.
468
469         Used data sources:
470
471             det44_interface_dump
472             det44_map_dump
473             det44_session_dump
474
475         :param node: DUT node.
476         :type node: dict
477         """
478         cmds = [
479             u"det44_interface_dump",
480             u"det44_map_dump",
481             u"det44_session_dump",
482         ]
483         PapiSocketExecutor.dump_and_log(node, cmds)