chore(mtu): delete some unused keywords related to mtu
[csit.git] / resources / libraries / python / SetupFramework.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 """This module exists to provide setup utilities for the framework on topology
15 nodes. All tasks required to be run before the actual tests are started is
16 supposed to end up here.
17
18 TODO: Figure out how to export JSON from SSH outside main Robot thread.
19 """
20
21 from os import environ, remove
22 from tempfile import NamedTemporaryFile
23 import threading
24 import traceback
25
26 from robot.api import logger
27
28 from resources.libraries.python.Constants import Constants as con
29 from resources.libraries.python.ssh import exec_cmd_no_error, scp_node
30 from resources.libraries.python.LocalExecution import run
31 from resources.libraries.python.topology import NodeType
32
33 __all__ = [u"SetupFramework"]
34
35
36 def pack_framework_dir():
37     """Pack the testing WS into temp file, return its name.
38
39     :returns: Tarball file name.
40     :rtype: str
41     :raises Exception: When failed to pack testing framework.
42     """
43
44     try:
45         directory = environ[u"TMPDIR"]
46     except KeyError:
47         directory = None
48
49     if directory is not None:
50         tmpfile = NamedTemporaryFile(
51             suffix=u".tgz", prefix=u"csit-testing-", dir=f"{directory}"
52         )
53     else:
54         tmpfile = NamedTemporaryFile(suffix=u".tgz", prefix=u"csit-testing-")
55     file_name = tmpfile.name
56     tmpfile.close()
57
58     run(
59         [
60             u"tar", u"--sparse", u"--exclude-vcs", u"--exclude=output*.xml",
61             u"--exclude=./tmp", u"--exclude=./env", u"--exclude=./.git",
62             u"-zcf", file_name, u"."
63         ], msg=u"Could not pack testing framework"
64     )
65
66     return file_name
67
68
69 def copy_tarball_to_node(tarball, node):
70     """Copy tarball file from local host to remote node.
71
72     :param tarball: Path to tarball to upload.
73     :param node: Dictionary created from topology.
74     :type tarball: str
75     :type node: dict
76     :returns: nothing
77     """
78     logger.console(
79         f"Copying tarball to {node[u'type']} host {node[u'host']}, "
80         f"port {node[u'port']} starts."
81     )
82     scp_node(node, tarball, u"/tmp/")
83     logger.console(
84         f"Copying tarball to {node[u'type']} host {node[u'host']}, "
85         f"port {node[u'port']} done."
86     )
87
88
89 def extract_tarball_at_node(tarball, node):
90     """Extract tarball at given node.
91
92     Extracts tarball using tar on given node to specific CSIT location.
93
94     :param tarball: Path to tarball to upload.
95     :param node: Dictionary created from topology.
96     :type tarball: str
97     :type node: dict
98     :returns: nothing
99     :raises RuntimeError: When failed to unpack tarball.
100     """
101     logger.console(
102         f"Extracting tarball to {con.REMOTE_FW_DIR} on {node[u'type']} "
103         f"host {node[u'host']}, port {node[u'port']} starts."
104     )
105     cmd = f"sudo rm -rf {con.REMOTE_FW_DIR}; mkdir {con.REMOTE_FW_DIR}; " \
106         f"tar -zxf {tarball} -C {con.REMOTE_FW_DIR}; rm -f {tarball}"
107     exec_cmd_no_error(
108         node, cmd,
109         message=f"Failed to extract {tarball} at node {node[u'type']} "
110         f"host {node[u'host']}, port {node[u'port']}",
111         timeout=240, include_reason=True, export=False
112     )
113     logger.console(
114         f"Extracting tarball to {con.REMOTE_FW_DIR} on {node[u'type']} "
115         f"host {node[u'host']}, port {node[u'port']} done."
116     )
117
118
119 def create_env_directory_at_node(node):
120     """Create fresh virtualenv to a directory, install pip requirements.
121
122     Return stdout and stderr of the command,
123     so we see which installs are behaving weird (e.g. attempting download).
124
125     :param node: Node to create virtualenv on.
126     :type node: dict
127     :returns: Stdout and stderr.
128     :rtype: str, str
129     :raises RuntimeError: When failed to setup virtualenv.
130     """
131     logger.console(
132         f"Virtualenv setup including requirements.txt on {node[u'type']} "
133         f"host {node[u'host']}, port {node[u'port']} starts."
134     )
135     cmd = f"cd {con.REMOTE_FW_DIR} && rm -rf env && virtualenv " \
136         f"-p $(which python3) --system-site-packages --never-download env " \
137         f"&& source env/bin/activate && ANSIBLE_SKIP_CONFLICT_CHECK=1 " \
138         f"pip3 install -r requirements.txt"
139     stdout, stderr = exec_cmd_no_error(
140         node, cmd, timeout=300, include_reason=True, export=False,
141         message=f"Failed install at node {node[u'type']} host {node[u'host']}, "
142         f"port {node[u'port']}"
143     )
144     logger.console(
145         f"Virtualenv setup on {node[u'type']} host {node[u'host']}, "
146         f"port {node[u'port']} done."
147     )
148     return stdout, stderr
149
150
151 def setup_node(node, tarball, remote_tarball, results=None, logs=None):
152     """Copy a tarball to a node and extract it.
153
154     :param node: A node where the tarball will be copied and extracted.
155     :param tarball: Local path of tarball to be copied.
156     :param remote_tarball: Remote path of the tarball.
157     :param results: A list where to store the result of node setup, optional.
158     :param logs: A list where to store anything that should be logged.
159     :type node: dict
160     :type tarball: str
161     :type remote_tarball: str
162     :type results: list
163     :type logs: list
164     :returns: True - success, False - error
165     :rtype: bool
166     """
167     try:
168         copy_tarball_to_node(tarball, node)
169         extract_tarball_at_node(remote_tarball, node)
170         if node[u"type"] == NodeType.TG:
171             stdout, stderr = create_env_directory_at_node(node)
172             if isinstance(logs, list):
173                 logs.append(f"{node[u'host']} Env stdout: {stdout}")
174                 logs.append(f"{node[u'host']} Env stderr: {stderr}")
175     except Exception:
176         # any exception must result in result = False
177         # since this runs in a thread and can't be caught anywhere else
178         err_msg = f"Node {node[u'type']} host {node[u'host']}, " \
179                   f"port {node[u'port']} setup failed."
180         logger.console(err_msg)
181         if isinstance(logs, list):
182             logs.append(f"{err_msg} Exception: {traceback.format_exc()}")
183         result = False
184     else:
185         logger.console(
186             f"Setup of node {node[u'type']} host {node[u'host']}, "
187             f"port {node[u'port']} done."
188         )
189         result = True
190
191     if isinstance(results, list):
192         results.append(result)
193     return result
194
195
196 def delete_local_tarball(tarball):
197     """Delete local tarball to prevent disk pollution.
198
199     :param tarball: Path of local tarball to delete.
200     :type tarball: str
201     :returns: nothing
202     """
203     remove(tarball)
204
205
206 def delete_framework_dir(node):
207     """Delete framework directory in /tmp/ on given node.
208
209     :param node: Node to delete framework directory on.
210     :type node: dict
211     """
212     logger.console(
213         f"Deleting framework directory on {node[u'type']} host {node[u'host']},"
214         f" port {node[u'port']} starts."
215     )
216     exec_cmd_no_error(
217         node, f"sudo rm -rf {con.REMOTE_FW_DIR}",
218         message=f"Framework delete failed at node {node[u'type']} "
219         f"host {node[u'host']}, port {node[u'port']}",
220         timeout=100, include_reason=True, export=False
221     )
222     logger.console(
223         f"Deleting framework directory on {node[u'type']} host {node[u'host']},"
224         f" port {node[u'port']} done."
225     )
226
227
228 def cleanup_node(node, results=None, logs=None):
229     """Delete a tarball from a node.
230
231     :param node: A node where the tarball will be delete.
232     :param results: A list where to store the result of node cleanup, optional.
233     :param logs: A list where to store anything that should be logged.
234     :type node: dict
235     :type results: list
236     :type logs: list
237     :returns: True - success, False - error
238     :rtype: bool
239     """
240     try:
241         delete_framework_dir(node)
242     except Exception:
243         err_msg = f"Cleanup of node {node[u'type']} host {node[u'host']}, " \
244                   f"port {node[u'port']} failed."
245         logger.console(err_msg)
246         if isinstance(logs, list):
247             logs.append(f"{err_msg} Exception: {traceback.format_exc()}")
248         result = False
249     else:
250         logger.console(
251             f"Cleanup of node {node[u'type']} host {node[u'host']}, "
252             f"port {node[u'port']} done."
253         )
254         result = True
255
256     if isinstance(results, list):
257         results.append(result)
258     return result
259
260
261 class SetupFramework:
262     """Setup suite run on topology nodes.
263
264     Many VAT/CLI based tests need the scripts at remote hosts before executing
265     them. This class packs the whole testing directory and copies it over
266     to all nodes in topology under /tmp/
267     """
268
269     @staticmethod
270     def setup_framework(nodes):
271         """Pack the whole directory and extract in temp on each node.
272
273         :param nodes: Topology nodes.
274         :type nodes: dict
275         :raises RuntimeError: If setup framework failed.
276         """
277
278         tarball = pack_framework_dir()
279         msg = f"Framework packed to {tarball}"
280         logger.console(msg)
281         logger.trace(msg)
282         remote_tarball = f"{tarball}"
283
284         results = list()
285         logs = list()
286         threads = list()
287
288         for node in nodes.values():
289             args = node, tarball, remote_tarball, results, logs
290             thread = threading.Thread(target=setup_node, args=args)
291             thread.start()
292             threads.append(thread)
293
294         logger.info(
295             u"Executing node setups in parallel, waiting for threads to end."
296         )
297
298         for thread in threads:
299             thread.join()
300
301         logger.info(f"Results: {results}")
302
303         for log in logs:
304             logger.trace(log)
305
306         delete_local_tarball(tarball)
307         if all(results):
308             logger.console(u"All nodes are ready.")
309             for node in nodes.values():
310                 logger.info(
311                     f"Setup of node {node[u'type']} host {node[u'host']}, "
312                     f"port {node[u'port']} done."
313                 )
314         else:
315             raise RuntimeError(u"Failed to setup framework.")
316
317
318 class CleanupFramework:
319     """Clean up suite run on topology nodes."""
320
321     @staticmethod
322     def cleanup_framework(nodes):
323         """Perform cleanup on each node.
324
325         :param nodes: Topology nodes.
326         :type nodes: dict
327         :raises RuntimeError: If cleanup framework failed.
328         """
329
330         results = list()
331         logs = list()
332         threads = list()
333
334         for node in nodes.values():
335             thread = threading.Thread(target=cleanup_node,
336                                       args=(node, results, logs))
337             thread.start()
338             threads.append(thread)
339
340         logger.info(
341             u"Executing node cleanups in parallel, waiting for threads to end."
342         )
343
344         for thread in threads:
345             thread.join()
346
347         logger.info(f"Results: {results}")
348
349         for log in logs:
350             logger.trace(log)
351
352         if all(results):
353             logger.console(u"All nodes cleaned up.")
354         else:
355             raise RuntimeError(u"Failed to cleaned up framework.")