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