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