f95a6a6636f7afcc30570663aea6915bd1baa312
[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"time" in iperf3_attributes:
98                 iperf3_cmd[u"args"] += \
99                     f" --time {iperf3_attributes[u'time']}"
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"native", 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, core_list, 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 core_list: List of cpu's to pass to taskset to pin the test
171             program to a different set of cores on the same numa node as VPP.
172         :param program: Test program.
173         :type node: dict
174         :type namespace: str
175         :type core_list: str
176         :type program: dict
177         :returns: Process ID
178         :rtype: int
179         :raises RuntimeError: If node subtype is not a DUT or startup failed.
180         """
181         if node[u"type"] != u"DUT":
182             raise RuntimeError(u"Node type is not a DUT!")
183
184         program_name = program[u"name"]
185         DUTSetup.kill_program(node, program_name, namespace)
186
187         if namespace == u"default":
188             shell_cmd = u"sh -c"
189         else:
190             shell_cmd = f"ip netns exec {namespace} sh -c"
191
192         env_vars = f"{program[u'env_vars']} " if u"env_vars" in program else u""
193         args = program[u"args"]
194         cmd = f"nohup {shell_cmd} \'{env_vars}taskset --cpu-list {core_list} " \
195             f"{program_name} {args} >/tmp/{program_name}_stdout.log " \
196             f"2>/tmp/{program_name}_stderr.log &\'"
197         try:
198             exec_cmd_no_error(node, cmd, sudo=True)
199             return DUTSetup.get_pid(node, program_name)[0]
200         except RuntimeError:
201             stdout_log, stderr_log = \
202                 HoststackUtil.get_hoststack_test_program_logs(node,
203                                                               program)
204             raise RuntimeError(f"Start {program_name} failed!\nSTDERR:\n" \
205                                f"{stderr_log}\nSTDOUT:\n{stdout_log}")
206         return None
207
208     @staticmethod
209     def stop_hoststack_test_program(node, program, pid):
210         """Stop the specified Hoststack test program.
211
212         :param node: DUT node.
213         :param program: Test program.
214         :param pid: Process ID of test program.
215         :type node: dict
216         :type program: dict
217         :type pid: int
218         """
219         program_name = program[u"name"]
220         if program_name == u"nginx":
221             cmd = u"nginx -s quit"
222             errmsg = u"Quit nginx failed!"
223         else:
224             cmd = f'if [ -n "$(ps {pid} | grep {program_name})" ] ; ' \
225                 f'then kill -s SIGTERM {pid}; fi'
226             errmsg = f"Kill {program_name} ({pid}) failed!"
227
228         exec_cmd_no_error(node, cmd, message=errmsg, sudo=True)
229
230     @staticmethod
231     def hoststack_test_program_finished(node, program_pid):
232         """Wait for the specified HostStack test program process to complete.
233
234         :param node: DUT node.
235         :param program_pid: test program pid.
236         :type node: dict
237         :type program_pid: str
238         :raises RuntimeError: If node subtype is not a DUT.
239         """
240         if node[u"type"] != u"DUT":
241             raise RuntimeError(u"Node type is not a DUT!")
242
243         cmd = f"sh -c 'strace -qqe trace=none -p {program_pid}'"
244         exec_cmd(node, cmd, sudo=True)
245         # Wait a bit for stdout/stderr to be flushed to log files
246         # TODO: see if sub-second sleep works e.g. sleep(0.1)
247         sleep(1)
248
249     @staticmethod
250     def analyze_hoststack_test_program_output(node, role, nsim_attr,
251                                               program):
252         """Gather HostStack test program output and check for errors.
253
254         :param node: DUT node.
255         :param role: Role (client|server) of test program.
256         :param nsim_attr: Network Simulation Attributes.
257         :param program: Test program.
258         :param program_args: List of test program args.
259         :type node: dict
260         :type role: str
261         :type nsim_attr: dict
262         :type program: dict
263         :returns: tuple of no results bool and test program results.
264         :rtype: bool, str
265         :raises RuntimeError: If node subtype is not a DUT.
266         """
267         if node[u"type"] != u"DUT":
268             raise RuntimeError(u"Node type is not a DUT!")
269
270         program_name = program[u"name"]
271         program_stdout, program_stderr = \
272             HoststackUtil.get_hoststack_test_program_logs(node, program)
273         if len(program_stdout) == 0 and len(program_stderr) == 0:
274             logger.trace(f"Retrying {program_name} log retrieval")
275             program_stdout, program_stderr = \
276                HoststackUtil.get_hoststack_test_program_logs(node, program)
277
278         no_results = False
279         env_vars = f"{program[u'env_vars']} " if u"env_vars" in program else u""
280         program_cmd = f"{env_vars}{program_name} {program[u'args']}"
281         test_results = f"Test Results of '{program_cmd}':\n"
282
283         if nsim_attr[u"output_feature_enable"] or \
284             nsim_attr[u"cross_connect_feature_enable"]:
285             if nsim_attr[u"output_feature_enable"]:
286                 feature_name = u"output"
287             else:
288                 feature_name = u"cross-connect"
289             test_results += \
290                 f"NSIM({feature_name}): delay " \
291                 f"{nsim_attr[u'delay_in_usec']} usecs, " \
292                 f"avg-pkt-size {nsim_attr[u'average_packet_size']}, " \
293                 f"bandwidth {nsim_attr[u'bandwidth_in_bits_per_second']} " \
294                 f"bits/sec, pkt-drop-rate {nsim_attr[u'packets_per_drop']} " \
295                 f"pkts/drop\n"
296
297         if u"error" in program_stderr.lower():
298             test_results += f"ERROR DETECTED:\n{program_stderr}"
299             raise RuntimeError(test_results)
300         if program_stdout:
301             bad_test_results = False
302             if program == u"vpp_echo" and u"JSON stats" not in program_stdout:
303                 test_results += u"Invalid test data output!\n"
304                 bad_test_results = True
305             test_results += program_stdout
306             if bad_test_results:
307                 raise RuntimeError(test_results)
308         else:
309             no_results = True
310             test_results += f"\nNo {program} test data retrieved!\n"
311             cmd = u"ls -l /tmp/*.log"
312             ls_stdout, _ = exec_cmd_no_error(node, cmd, sudo=True)
313             test_results += f"{ls_stdout}\n"
314
315         # TODO: Incorporate show error stats into results analysis
316         host = node[u"host"]
317         test_results += \
318             f"\n{role} VPP 'show errors' on host {host}:\n" \
319             f"{PapiSocketExecutor.run_cli_cmd(node, u'show error')}\n"
320
321         return no_results, test_results
322
323     @staticmethod
324     def no_hoststack_test_program_results(server_no_results, client_no_results):
325         """Return True if no HostStack test program output was gathered.
326
327         :param server_no_results: server no results value.
328         :param client_no_results: client no results value.
329         :type server_no_results: bool
330         :type client_no_results: bool
331         :rtype: bool
332         """
333         return server_no_results and client_no_results