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