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