fix(vlan): do not apply strip offload
[csit.git] / resources / libraries / python / NATUtil.py
1 # Copyright (c) 2022 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)
186                 args_in[u"is_add"] = True
187                 papi_exec.add(cmd, **args_in)
188                 papi_exec.get_replies(err_msg)
189
190         return resetter
191
192     @staticmethod
193     def show_nat44_config(node):
194         """Show the NAT44 plugin running configuration.
195
196         :param node: DUT node.
197         :type node: dict
198         """
199         cmd = u"nat44_show_running_config"
200         err_msg = f"Failed to get NAT44 configuration on host {node[u'host']}"
201
202         with PapiSocketExecutor(node) as papi_exec:
203             reply = papi_exec.add(cmd).get_reply(err_msg)
204
205         logger.debug(f"NAT44 Configuration:\n{pformat(reply)}")
206
207     @staticmethod
208     def show_nat44_summary(node):
209         """Show NAT44 summary on the specified topology node.
210
211         :param node: Topology node.
212         :type node: dict
213         :returns: NAT44 summary data.
214         :rtype: str
215         """
216         return PapiSocketExecutor.run_cli_cmd(node, u"show nat44 summary")
217
218     @staticmethod
219     def show_nat_base_data(node):
220         """Show the NAT base data.
221
222         Used data sources:
223
224             nat_worker_dump
225             nat44_interface_addr_dump
226             nat44_address_dump
227             nat44_static_mapping_dump
228             nat44_interface_dump
229
230         :param node: DUT node.
231         :type node: dict
232         """
233         cmds = [
234             u"nat_worker_dump",
235             u"nat44_interface_addr_dump",
236             u"nat44_address_dump",
237             u"nat44_static_mapping_dump",
238             u"nat44_interface_dump",
239         ]
240         PapiSocketExecutor.dump_and_log(node, cmds)
241
242     @staticmethod
243     def show_nat_user_data(node):
244         """Show the NAT user data.
245
246         Used data sources:
247
248             nat44_user_dump
249             nat44_user_session_dump
250
251         :param node: DUT node.
252         :type node: dict
253         """
254         cmds = [
255             u"nat44_user_dump",
256             u"nat44_user_session_dump",
257         ]
258         PapiSocketExecutor.dump_and_log(node, cmds)
259
260     @staticmethod
261     def compute_max_translations_per_thread(sessions, threads):
262         """Compute value of max_translations_per_thread NAT44 parameter based on
263         total number of worker threads.
264
265         :param sessions: Required number of NAT44 sessions.
266         :param threads: Number of worker threads.
267         :type sessions: int
268         :type threads: int
269         :returns: Value of max_translations_per_thread NAT44 parameter.
270         :rtype: int
271         """
272         # vpp-device tests have not dedicated physical core so
273         # ${dp_count_int} == 0 but we need to use one thread
274         threads = 1 if not int(threads) else int(threads)
275         rest, mult = modf(log2(sessions/(10*threads)))
276         return 2 ** (int(mult) + (1 if rest else 0)) * 10
277
278     @staticmethod
279     def get_nat44_sessions_number(node, proto):
280         """Get number of expected NAT44 sessions from NAT44 mapping data.
281
282         This keyword uses output from a CLI command,
283         so it can start failing when VPP changes the output format.
284         TODO: Switch to API (or stat segment) when available.
285
286         The current implementation supports both 2202 and post-2202 format.
287         (The Gerrit number changing the output format is 34877.)
288
289         For TCP proto, the expected state after rampup is
290         some number of sessions in transitory state (VPP has seen the FINs),
291         and some number of sessions in established state (meaning
292         some FINs were lost in the last trial).
293         While the two states may need slightly different number of cycles
294         to process next packet, the current implementation considers
295         both of them the "fast path", so they are both counted as expected.
296
297         As the tests should fail if a session is timed-out,
298         the logic substracts timed out sessions for the returned value
299         (only available for post-2202 format).
300
301         TODO: Investigate if it is worth to insert additional rampup trials
302         in TPUT tests to ensure all sessions are transitory before next
303         measurement.
304
305         :param node: DUT node.
306         :param proto: Required protocol - TCP/UDP/ICMP.
307         :type node: dict
308         :type proto: str
309         :returns: Number of active established NAT44 sessions.
310         :rtype: int
311         :raises ValueError: If not supported protocol.
312         :raises RuntimeError: If output is not formatted as expected.
313         """
314         proto_l = proto.strip().lower()
315         if proto_l not in [u"udp", u"tcp", u"icmp"]:
316             raise ValueError(f"Unsupported protocol: {proto}!")
317         summary_text = NATUtil.show_nat44_summary(node)
318         summary_lines = summary_text.splitlines()
319         # Output from VPP v22.02 and before, delete when no longer needed.
320         pattern_2202 = f"total {proto_l} sessions:"
321         if pattern_2202 in summary_text:
322             for line in summary_lines:
323                 if pattern_2202 not in line:
324                     continue
325                 return int(line.split(u":", 1)[1].strip())
326         # Post-2202, the proto info and session info are not on the same line.
327         found = False
328         for line in summary_lines:
329             if not found:
330                 if f"{proto_l} sessions:" in line:
331                     found = True
332                 continue
333             # Proto is found, find the line we are interested in.
334             if u"total" not in line:
335                 raise RuntimeError(f"show nat summary: no {proto} total.")
336             # We have the line with relevant numbers.
337             total_part, timed_out_part = line.split(u"(", 1)
338             timed_out_part = timed_out_part.split(u")", 1)[0]
339             total_count = int(total_part.split(u":", 1)[1].strip())
340             timed_out_count = int(timed_out_part.split(u":", 1)[1].strip())
341             active_count = total_count - timed_out_count
342             return active_count
343         raise RuntimeError(u"Unknown format of show nat44 summary")
344
345     # DET44 PAPI calls
346     # DET44 means deterministic mode of NAT44
347     @staticmethod
348     def enable_det44_plugin(node, inside_vrf=0, outside_vrf=0):
349         """Enable DET44 plugin.
350
351         :param node: DUT node.
352         :param inside_vrf: Inside VRF ID.
353         :param outside_vrf: Outside VRF ID.
354         :type node: dict
355         :type inside_vrf: str or int
356         :type outside_vrf: str or int
357         """
358         cmd = u"det44_plugin_enable_disable"
359         err_msg = f"Failed to enable DET44 plugin on the host {node[u'host']}!"
360         args_in = dict(
361             enable=True,
362             inside_vrf=int(inside_vrf),
363             outside_vrf=int(outside_vrf)
364         )
365
366         with PapiSocketExecutor(node) as papi_exec:
367             papi_exec.add(cmd, **args_in).get_reply(err_msg)
368
369     @staticmethod
370     def set_det44_interface(node, if_key, is_inside):
371         """Enable DET44 feature on the interface.
372
373         :param node: DUT node.
374         :param if_key: Interface key from topology file of interface
375             to enable DET44 feature on.
376         :param is_inside: True if interface is inside, False if outside.
377         :type node: dict
378         :type if_key: str
379         :type is_inside: bool
380         """
381         cmd = u"det44_interface_add_del_feature"
382         err_msg = f"Failed to enable DET44 feature on the interface {if_key} " \
383             f"on the host {node[u'host']}!"
384         args_in = dict(
385             is_add=True,
386             is_inside=is_inside,
387             sw_if_index=Topology.get_interface_sw_index(node, if_key)
388         )
389
390         with PapiSocketExecutor(node) as papi_exec:
391             papi_exec.add(cmd, **args_in).get_reply(err_msg)
392
393     @staticmethod
394     def set_det44_mapping(node, ip_in, subnet_in, ip_out, subnet_out):
395         """Set DET44 mapping.
396
397         The return value is a callable (zero argument Python function)
398         which can be used to reset NAT state, so repeated trial measurements
399         hit the same slow path.
400
401         :param node: DUT node.
402         :param ip_in: Inside IP.
403         :param subnet_in: Inside IP subnet.
404         :param ip_out: Outside IP.
405         :param subnet_out: Outside IP subnet.
406         :type node: dict
407         :type ip_in: str
408         :type subnet_in: str or int
409         :type ip_out: str
410         :type subnet_out: str or int
411         """
412         cmd = u"det44_add_del_map"
413         err_msg = f"Failed to set DET44 mapping on the host {node[u'host']}!"
414         args_in = dict(
415             is_add=True,
416             in_addr=IPv4Address(str(ip_in)).packed,
417             in_plen=int(subnet_in),
418             out_addr=IPv4Address(str(ip_out)).packed,
419             out_plen=int(subnet_out)
420         )
421
422         with PapiSocketExecutor(node) as papi_exec:
423             papi_exec.add(cmd, **args_in).get_reply(err_msg)
424
425         # A closure, accessing the variables above.
426         def resetter():
427             """Delete and re-add the deterministic NAT mapping."""
428             with PapiSocketExecutor(node) as papi_exec:
429                 args_in[u"is_add"] = False
430                 papi_exec.add(cmd, **args_in)
431                 args_in[u"is_add"] = True
432                 papi_exec.add(cmd, **args_in)
433                 papi_exec.get_replies(err_msg)
434
435         return resetter
436
437     @staticmethod
438     def get_det44_mapping(node):
439         """Get DET44 mapping data.
440
441         :param node: DUT node.
442         :type node: dict
443         :returns: Dictionary of DET44 mapping data.
444         :rtype: dict
445         """
446         cmd = u"det44_map_dump"
447         err_msg = f"Failed to get DET44 mapping data on the host " \
448             f"{node[u'host']}!"
449         args_in = dict()
450         with PapiSocketExecutor(node) as papi_exec:
451             details = papi_exec.add(cmd, **args_in).get_reply(err_msg)
452
453         return details
454
455     @staticmethod
456     def get_det44_sessions_number(node):
457         """Get number of established DET44 sessions from actual DET44 mapping
458         data.
459         :param node: DUT node.
460         :type node: dict
461         :returns: Number of established DET44 sessions.
462         :rtype: int
463         """
464         det44_data = NATUtil.get_det44_mapping(node)
465         return det44_data.get(u"ses_num", 0)
466
467     @staticmethod
468     def show_det44(node):
469         """Show DET44 data.
470
471         Used data sources:
472
473             det44_interface_dump
474             det44_map_dump
475             det44_session_dump
476
477         :param node: DUT node.
478         :type node: dict
479         """
480         cmds = [
481             u"det44_interface_dump",
482             u"det44_map_dump",
483             u"det44_session_dump",
484         ]
485         PapiSocketExecutor.dump_and_log(node, cmds)