perf: add TCP Iperf3+LDPRELOAD test suite
[csit.git] / resources / libraries / python / HoststackUtil.py
1 # Copyright (c) 2020 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 """Host Stack util library."""
15 from time import sleep
16 from robot.api import logger
17
18 from resources.libraries.python.Constants import Constants
19 from resources.libraries.python.ssh import exec_cmd, exec_cmd_no_error
20 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
21 from resources.libraries.python.DUTSetup import DUTSetup
22
23 class HoststackUtil():
24     """Utilities for Host Stack tests."""
25
26     @staticmethod
27     def get_vpp_echo_command(vpp_echo_attributes):
28         """Construct the vpp_echo command using the specified attributes.
29
30         :param vpp_echo_attributes: vpp_echo test program attributes.
31         :type vpp_echo_attributes: dict
32         :returns: Command line components of the vpp_echo command
33             'name' - program name
34             'args' - command arguments.
35         :rtype: dict
36         """
37         # TODO: Use a python class instead of dictionary for the return type
38         proto = vpp_echo_attributes[u"uri_protocol"]
39         addr = vpp_echo_attributes[u"uri_ip4_addr"]
40         port = vpp_echo_attributes[u"uri_port"]
41         vpp_echo_cmd = {}
42         vpp_echo_cmd[u"name"] = u"vpp_echo"
43         vpp_echo_cmd[u"args"] = f"{vpp_echo_attributes[u'role']} " \
44             f"socket-name {vpp_echo_attributes[u'vpp_api_socket']} " \
45             f"{vpp_echo_attributes[u'json_output']} " \
46             f"uri {proto}://{addr}/{port} " \
47             f"nclients {vpp_echo_attributes[u'nclients']} " \
48             f"quic-streams {vpp_echo_attributes[u'quic_streams']} " \
49             f"time {vpp_echo_attributes[u'time']} " \
50             f"fifo-size {vpp_echo_attributes[u'fifo_size']} " \
51             f"TX={vpp_echo_attributes[u'tx_bytes']} " \
52             f"RX={vpp_echo_attributes[u'rx_bytes']}"
53         if vpp_echo_attributes[u"rx_results_diff"]:
54             vpp_echo_cmd[u"args"] += u" rx-results-diff"
55         if vpp_echo_attributes[u"tx_results_diff"]:
56             vpp_echo_cmd[u"args"] += u" tx-results-diff"
57         return vpp_echo_cmd
58
59     @staticmethod
60     def get_iperf3_command(iperf3_attributes):
61         """Construct the iperf3 command using the specified attributes.
62
63         :param iperf3_attributes: iperf3 test program attributes.
64         :type iperf3_attributes: dict
65         :returns: Command line components of the iperf3 command
66             'env_vars' - environment variables
67             'name' - program name
68             'args' - command arguments.
69         :rtype: dict
70         """
71         # TODO: Use a python class instead of dictionary for the return type
72         iperf3_cmd = {}
73         iperf3_cmd[u"env_vars"] = f"VCL_CONFIG={Constants.REMOTE_FW_DIR}/" \
74             f"{Constants.RESOURCES_TPL_VCL}/" \
75             f"{iperf3_attributes[u'vcl_config']}"
76         if iperf3_attributes[u"ld_preload"]:
77             iperf3_cmd[u"env_vars"] += \
78                 f" LD_PRELOAD={Constants.VCL_LDPRELOAD_LIBRARY}"
79         if iperf3_attributes[u'transparent_tls']:
80             iperf3_cmd[u"env_vars"] += u" LDP_ENV_TLS_TRANS=1"
81
82         json_results = u" --json" if iperf3_attributes[u'json'] else u""
83         ip_address = f" {iperf3_attributes[u'ip_address']}" if u"ip_address" \
84                      in iperf3_attributes else u""
85         iperf3_cmd[u"name"] = u"iperf3"
86         iperf3_cmd[u"args"] = f"--{iperf3_attributes[u'role']}{ip_address} " \
87                               f"--interval 0{json_results} " \
88                               f"--version{iperf3_attributes[u'ip_version']}"
89
90         if iperf3_attributes[u"role"] == u"server":
91             iperf3_cmd[u"args"] += u" --one-off"
92         else:
93             iperf3_cmd[u"args"] += u" --get-server-output"
94             if u"parallel" in iperf3_attributes:
95                 iperf3_cmd[u"args"] += \
96                     f" --parallel {iperf3_attributes[u'parallel']}"
97             if u"bytes" in iperf3_attributes:
98                 iperf3_cmd[u"args"] += \
99                     f" --bytes {iperf3_attributes[u'bytes']}"
100         return iperf3_cmd
101
102     @staticmethod
103     def set_hoststack_quic_fifo_size(node, fifo_size):
104         """Set the QUIC protocol fifo size.
105
106         :param node: Node to set the QUIC fifo size on.
107         :param fifo_size: fifo size, passed to the quic set fifo-size command.
108         :type node: dict
109         :type fifo_size: str
110         """
111         cmd = f"quic set fifo-size {fifo_size}"
112         PapiSocketExecutor.run_cli_cmd(node, cmd)
113
114     @staticmethod
115     def set_hoststack_quic_crypto_engine(node, quic_crypto_engine,
116                                          fail_on_error=False):
117         """Set the Hoststack QUIC crypto engine on node
118
119         :param node: Node to enable/disable HostStack.
120         :param quic_crypto_engine: type of crypto engine
121         :type node: dict
122         :type quic_crypto_engine: str
123         """
124         vpp_crypto_engines = {u"openssl", u"ia32", u"ipsecmb"}
125         if quic_crypto_engine == u"nocrypto":
126             logger.trace(u"No QUIC crypto engine.")
127             return
128
129         if quic_crypto_engine in vpp_crypto_engines:
130             cmds = [u"quic set crypto api vpp",
131                     f"set crypto handler aes-128-gcm {quic_crypto_engine}",
132                     f"set crypto handler aes-256-gcm {quic_crypto_engine}"]
133         elif quic_crypto_engine == u"picotls":
134             cmds = [u"quic set crypto api picotls"]
135         else:
136             raise ValueError(f"Unknown QUIC crypto_engine {quic_crypto_engine}")
137
138         for cmd in cmds:
139             try:
140                 PapiSocketExecutor.run_cli_cmd(node, cmd)
141             except AssertionError:
142                 if fail_on_error:
143                     raise
144
145     @staticmethod
146     def get_hoststack_test_program_logs(node, program):
147         """Get HostStack test program stdout log.
148
149         :param node: DUT node.
150         :param program: test program.
151         :type node: dict
152         :type program: dict
153         """
154         program_name = program[u"name"]
155         cmd = f"sh -c \'cat /tmp/{program_name}_stdout.log\'"
156         stdout_log, _ = exec_cmd_no_error(node, cmd, sudo=True, \
157             message=f"Get {program_name} stdout log failed!")
158
159         cmd = f"sh -c \'cat /tmp/{program_name}_stderr.log\'"
160         stderr_log, _ = exec_cmd_no_error(node, cmd, sudo=True, \
161             message=f"Get {program_name} stderr log failed!")
162         return stdout_log, stderr_log
163
164     @staticmethod
165     def start_hoststack_test_program(node, namespace, program):
166         """Start the specified HostStack test program.
167
168         :param node: DUT node.
169         :param namespace: Net Namespace to run program in.
170         :param program: Test program.
171         :type node: dict
172         :type namespace: str
173         :type program: dict
174         :returns: Process ID
175         :rtype: int
176         :raises RuntimeError: If node subtype is not a DUT or startup failed.
177         """
178         # TODO: Pin test program to core(s) on same numa node as VPP.
179         if node[u"type"] != u"DUT":
180             raise RuntimeError(u"Node type is not a DUT!")
181
182         program_name = program[u"name"]
183         DUTSetup.kill_program(node, program_name, namespace)
184
185         if namespace == u"default":
186             shell_cmd = u"sh -c"
187         else:
188             shell_cmd = f"ip netns exec {namespace} sh -c"
189
190         env_vars = f"{program[u'env_vars']} " if u"env_vars" in program else u""
191         args = program[u"args"]
192         cmd = f"nohup {shell_cmd} \'{env_vars}{program_name} {args} " \
193             f">/tmp/{program_name}_stdout.log " \
194             f"2>/tmp/{program_name}_stderr.log &\'"
195         try:
196             exec_cmd_no_error(node, cmd, sudo=True)
197             return DUTSetup.get_pid(node, program_name)[0]
198         except RuntimeError:
199             stdout_log, stderr_log = \
200                 HoststackUtil.get_hoststack_test_program_logs(node,
201                                                               program)
202             raise RuntimeError(f"Start {program_name} failed!\nSTDERR:\n" \
203                                f"{stderr_log}\nSTDOUT:\n{stdout_log}")
204         return None
205
206     @staticmethod
207     def stop_hoststack_test_program(node, program, pid):
208         """Stop the specified Hoststack test program.
209
210         :param node: DUT node.
211         :param program: Test program.
212         :param pid: Process ID of test program.
213         :type node: dict
214         :type program: dict
215         :type pid: int
216         """
217         program_name = program[u"name"]
218         if program_name == u"nginx":
219             cmd = u"nginx -s quit"
220             errmsg = u"Quit nginx failed!"
221         else:
222             cmd = f'if [ -n "$(ps {pid} | grep {program_name})" ] ; ' \
223                 f'then kill -s SIGTERM {pid}; fi'
224             errmsg = f"Kill {program_name} ({pid}) failed!"
225
226         exec_cmd_no_error(node, cmd, message=errmsg, sudo=True)
227
228     @staticmethod
229     def hoststack_test_program_finished(node, program_pid):
230         """Wait for the specified HostStack test program process to complete.
231
232         :param node: DUT node.
233         :param program_pid: test program pid.
234         :type node: dict
235         :type program_pid: str
236         :raises RuntimeError: If node subtype is not a DUT.
237         """
238         if node[u"type"] != u"DUT":
239             raise RuntimeError(u"Node type is not a DUT!")
240
241         cmd = f"sh -c 'strace -qqe trace=none -p {program_pid}'"
242         exec_cmd(node, cmd, sudo=True)
243         # Wait a bit for stdout/stderr to be flushed to log files
244         # TODO: see if sub-second sleep works e.g. sleep(0.1)
245         sleep(1)
246
247     @staticmethod
248     def analyze_hoststack_test_program_output(node, role, nsim_attr,
249                                               program):
250         """Gather HostStack test program output and check for errors.
251
252         :param node: DUT node.
253         :param role: Role (client|server) of test program.
254         :param nsim_attr: Network Simulation Attributes.
255         :param program: Test program.
256         :param program_args: List of test program args.
257         :type node: dict
258         :type role: str
259         :type nsim_attr: dict
260         :type program: dict
261         :returns: tuple of no results bool and test program results.
262         :rtype: bool, str
263         :raises RuntimeError: If node subtype is not a DUT.
264         """
265         if node[u"type"] != u"DUT":
266             raise RuntimeError(u"Node type is not a DUT!")
267
268         program_name = program[u"name"]
269         program_stdout, program_stderr = \
270             HoststackUtil.get_hoststack_test_program_logs(node, program)
271         if len(program_stdout) == 0 and len(program_stderr) == 0:
272             logger.trace(f"Retrying {program_name} log retrieval")
273             program_stdout, program_stderr = \
274                HoststackUtil.get_hoststack_test_program_logs(node, program)
275
276         no_results = False
277         env_vars = f"{program[u'env_vars']} " if u"env_vars" in program else u""
278         program_cmd = f"{env_vars}{program_name} {program[u'args']}"
279         test_results = f"Test Results of '{program_cmd}':\n"
280
281         if nsim_attr[u"output_feature_enable"] or \
282             nsim_attr[u"cross_connect_feature_enable"]:
283             if nsim_attr[u"output_feature_enable"]:
284                 feature_name = u"output"
285             else:
286                 feature_name = u"cross-connect"
287             test_results += \
288                 f"NSIM({feature_name}): delay " \
289                 f"{nsim_attr[u'delay_in_usec']} usecs, " \
290                 f"avg-pkt-size {nsim_attr[u'average_packet_size']}, " \
291                 f"bandwidth {nsim_attr[u'bandwidth_in_bits_per_second']} " \
292                 f"bits/sec, pkt-drop-rate {nsim_attr[u'packets_per_drop']} " \
293                 f"pkts/drop\n"
294
295         if u"error" in program_stderr.lower():
296             test_results += f"ERROR DETECTED:\n{program_stderr}"
297             raise RuntimeError(test_results)
298         if program_stdout:
299             bad_test_results = False
300             if program == u"vpp_echo" and u"JSON stats" not in program_stdout:
301                 test_results += u"Invalid test data output!\n"
302                 bad_test_results = True
303             test_results += program_stdout
304             if bad_test_results:
305                 raise RuntimeError(test_results)
306         else:
307             no_results = True
308             test_results += f"\nNo {program} test data retrieved!\n"
309             cmd = u"ls -l /tmp/*.log"
310             ls_stdout, _ = exec_cmd_no_error(node, cmd, sudo=True)
311             test_results += f"{ls_stdout}\n"
312
313         # TODO: Incorporate show error stats into results analysis
314         host = node[u"host"]
315         test_results += \
316             f"\n{role} VPP 'show errors' on host {host}:\n" \
317             f"{PapiSocketExecutor.run_cli_cmd(node, u'show error')}\n"
318
319         return no_results, test_results
320
321     @staticmethod
322     def no_hoststack_test_program_results(server_no_results, client_no_results):
323         """Return True if no HostStack test program output was gathered.
324
325         :param server_no_results: server no results value.
326         :param client_no_results: client no results value.
327         :type server_no_results: bool
328         :type client_no_results: bool
329         :rtype: bool
330         """
331         return server_no_results and client_no_results