blob: 314c641c75591e1e861b6343660bf71496454c39 [file] [log] [blame]
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +03001
2import time
3
Anna Arhipovab8869ae2023-04-06 12:40:42 +02004from tcp_tests import logger, settings
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +03005from tcp_tests.helpers.log_helpers import pretty_repr
Anna Arhipovab8869ae2023-04-06 12:40:42 +02006from tcp_tests.helpers.utils import Worker
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +03007
8LOG = logger.logger
9
10
11class ExecuteCommandsMixin(object):
12 """docstring for ExecuteCommands"""
13
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030014 __config = None
15 __underlay = None
16
17 def __init__(self, config, underlay):
18 self.__config = config
19 self.__underlay = underlay
20 super(ExecuteCommandsMixin, self).__init__()
21
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030022 def execute_commands(self, commands, label="Command"):
23 """Execute a sequence of commands
24
25 Main propose is to implement workarounds for salt formulas like:
26 - exit_code == 0 when there are actual failures
27 - salt_master and/or salt_minion stop working after executing a formula
28 - a formula fails at first run, but completes at next runs
29
30 :param label: label of the current sequence of the commands, for log
31 :param commands: list of dicts with the following data:
32 commands = [
33 ...
34 {
35 # Required:
36 'cmd': 'shell command(s) to run',
37 'node_name': 'name of the node to run the command(s)',
38 # Optional:
39 'description': 'string with a readable command description',
Anna Arhipovab8869ae2023-04-06 12:40:42 +020040 'parallel': 'bool (True of False) to enable executing these
41 type of command in multithreading'
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030042 'retry': {
43 'count': int, # How many times should be run the command
44 # until success
45 'delay': int, # Delay between tries in seconds
46 },
47 'skip_fail': bool # If True - continue with the next step
48 # without failure even if count number
49 # is reached.
50 # If False - rise an exception (default)
51 },
52 ...
53 ]
54 """
Anna Arhipovab8869ae2023-04-06 12:40:42 +020055 worker = Worker(limit=settings.THREADS, timeout=3*60)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030056 for n, step in enumerate(commands):
57 # Required fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030058 action_cmd = step.get('cmd')
59 action_do = step.get('do')
60 action_upload = step.get('upload')
61 action_download = step.get('download')
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030062 # node_name = step.get('node_name')
63 # Optional fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030064 description = step.get('description', action_cmd)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030065 # retry = step.get('retry', {'count': 1, 'delay': 1})
66 # retry_count = retry.get('count', 1)
67 # retry_delay = retry.get('delay', 1)
68 # skip_fail = step.get('skip_fail', False)
69
70 msg = "[ {0} #{1} ] {2}".format(label, n + 1, description)
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020071 log_msg = "\n\n{0}\n{1}".format(msg, '=' * len(msg))
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030072
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030073 if action_cmd:
Anna Arhipovab8869ae2023-04-06 12:40:42 +020074 if step.get('parallel'):
75 name = description + " on " + step.get("node_name")
76 worker.start(func=self.execute_command,
77 args=(step, msg),
78 name=name
79 )
80 else:
81 while not worker.are_completed():
82 LOG.info("Waiting {}".format(worker.pool))
83 if worker.all_tasks_successfully_completed():
84 worker.clean_pool()
85 self.execute_command(step, msg)
86
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030087 elif action_do:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020088 self.command2(step, msg)
Dennis Dmitriev707bfeb2018-03-15 17:50:28 -050089 elif action_upload:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020090 LOG.info(log_msg)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030091 self.action_upload(step)
92 elif action_download:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020093 LOG.info(log_msg)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030094 self.action_download(step)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030095
Anna Arhipovab8869ae2023-04-06 12:40:42 +020096 while not worker.are_completed():
97 LOG.info("Waiting {}".format(worker.pool))
98
99 assert worker.all_tasks_successfully_completed(), \
100 worker.print_failed_tasks()
101
Tatyana Leontovichafe8f952018-06-20 15:33:03 +0300102 def execute_command(self, step, msg, return_res=None):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300103 # Required fields
104 cmd = step.get('cmd')
105 node_name = step.get('node_name')
106 # Optional fields
107 description = step.get('description', cmd)
108 retry = step.get('retry', {'count': 1, 'delay': 1})
109 retry_count = retry.get('count', 1)
110 retry_delay = retry.get('delay', 1)
111 skip_fail = step.get('skip_fail', False)
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000112 timeout = step.get('timeout', None)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300113
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300114 with self.__underlay.remote(node_name=node_name) as remote:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300115 for x in range(retry_count, 0, -1):
116 time.sleep(3)
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200117
Dennis Dmitrievce646fb2018-03-21 09:10:00 +0200118 retry_msg = (' (try {0} of {1}, skip_fail={2}, node_name={3})'
119 .format(retry_count - x + 1,
120 retry_count,
121 skip_fail,
122 node_name))
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200123 LOG.info("\n\n{0}\n{1}".format(
124 msg + retry_msg, '=' * len(msg + retry_msg)))
125
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000126 result = remote.execute(cmd, timeout=timeout, verbose=True)
Tatyana Leontovichafe8f952018-06-20 15:33:03 +0300127 if return_res:
128 return result
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300129
130 # Workaround of exit code 0 from salt in case of failures
131 failed = 0
Dennis Dmitriev4db5bf22017-05-13 19:31:17 +0300132 for s in result['stdout'] + result['stderr']:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300133 if s.startswith("Failed:"):
134 failed += int(s.split("Failed:")[1])
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300135 if 'Minion did not return. [No response]' in s:
136 failed += 1
Dennis Dmitrievf8546172017-07-20 21:57:05 +0300137 if 'Minion did not return. [Not connected]' in s:
138 failed += 1
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200139 if ('Salt request timed out. The master is not responding.'
140 in s):
141 failed += 1
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300142 if s.startswith("[CRITICAL]"):
143 failed += 1
Tatyana Leontovich835465f2018-06-14 16:42:23 +0300144 if 'Fatal' in s:
145 failed += 1
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300146
147 if result.exit_code != 0:
148 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300149 elif failed != 0:
150 LOG.error(
151 " === SALT returned exit code = 0 while "
152 "there are failed modules! ===")
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200153 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300154 else:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300155 break
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300156
157 if x == 1 and skip_fail is False:
158 # In the last retry iteration, raise an exception
Dennis Dmitriev47800162018-10-31 11:57:02 +0200159 raise Exception("Step '{0}' failed:\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200160 "=============== Command: ==============\n"
Dennis Dmitriev44f6db22018-10-31 16:07:56 +0200161 "{1}\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200162 "=============== STDOUT: ===============\n"
Dennis Dmitriev44f6db22018-10-31 16:07:56 +0200163 "{2}\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200164 "=============== STDERR: ===============\n"
165 "{3}\n"
Dennis Dmitriev47800162018-10-31 11:57:02 +0200166 .format(description,
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200167 cmd,
Dennis Dmitriev47800162018-10-31 11:57:02 +0200168 result.stdout_str,
169 result.stderr_str))
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300170
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200171 def command2(self, step, msg):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300172 # Required fields
173 do = step['do']
174 target = step['target']
175 state = step.get('state')
176 states = step.get('states')
177 # Optional fields
178 args = step.get('args')
179 kwargs = step.get('kwargs')
180 description = step.get('description', do)
181 retry = step.get('retry', {'count': 1, 'delay': 1})
182 retry_count = retry.get('count', 1)
183 retry_delay = retry.get('delay', 1)
184 skip_fail = step.get('skip_fail', False)
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000185 timeout = step.get('timeout', None)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300186
187 if not bool(state) ^ bool(states):
188 raise ValueError("You should use state or states in step")
189
190 for x in range(retry_count, 0, -1):
191 time.sleep(3)
192
Dennis Dmitrievce646fb2018-03-21 09:10:00 +0200193 retry_msg = (' (try {0} of {1}, skip_fail={2}, target={3})'
194 .format(retry_count - x + 1,
195 retry_count,
196 skip_fail,
197 target))
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200198 LOG.info("\n\n{0}\n{1}".format(
199 msg + retry_msg, '=' * len(msg + retry_msg)))
200
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300201 method = getattr(self._salt, self._salt._map[do])
202 command_ret = method(tgt=target, state=state or states,
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000203 args=args, kwargs=kwargs, timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300204 command_ret = command_ret if \
205 isinstance(command_ret, list) else [command_ret]
206 results = [(r['return'][0], f) for r, f in command_ret]
207
208 # FIMME: Change to debug level
209 LOG.info(" === States output =======================\n"
210 "{}\n"
211 " =========================================".format(
212 pretty_repr([r for r, f in results])))
213
214 all_fails = [f for r, f in results if f]
215 if all_fails:
216 LOG.error("States finished with failures.\n{}".format(
217 all_fails))
218 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300219 else:
220 break
221
222 if x == 1 and skip_fail is False:
223 # In the last retry iteration, raise an exception
224 raise Exception("Step '{0}' failed"
225 .format(description))
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300226
227 def action_upload(self, step):
228 """Upload from local host to environment node
229
230 Example:
231
232 - description: Upload a file
233 upload:
234 local_path: /tmp/
235 local_filename: cirros*.iso
236 remote_path: /tmp/
237 node_name: ctl01
238 skip_fail: False
239 """
240 node_name = step.get('node_name')
241 local_path = step.get('upload', {}).get('local_path', None)
242 local_filename = step.get('upload', {}).get('local_filename', None)
243 remote_path = step.get('upload', {}).get('remote_path', None)
244 description = step.get('description', local_path)
245 skip_fail = step.get('skip_fail', False)
246
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300247 if not local_path or not remote_path:
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300248 raise Exception("Step '{0}' failed: please specify 'local_path', "
249 "'local_filename' and 'remote_path' correctly"
250 .format(description))
251
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300252 if not local_filename:
253 # If local_path is not specified then uploading a directory
254 with self.__underlay.remote(node_name=node_name) as remote:
255 LOG.info("Uploading directory {0} to {1}:{2}"
256 .format(local_path, node_name, remote_path))
Dmitry Tyzhnenko8f6a63e2017-09-05 15:37:23 +0300257 remote.upload(source=local_path.rstrip(),
258 target=remote_path.rstrip())
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300259 return
260
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300261 result = {}
262 with self.__underlay.local() as local:
abaraniuk7a3a05b2018-11-15 16:05:35 +0200263 result = local.execute('cd {0} && find . -maxdepth 1 -type f'
264 ' -name "{1}"'
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300265 .format(local_path, local_filename))
266 LOG.info("Found files to upload:\n{0}".format(result))
267
268 if not result['stdout'] and not skip_fail:
269 raise Exception("Nothing to upload on step {0}"
270 .format(description))
271
272 with self.__underlay.remote(node_name=node_name) as remote:
273 file_names = result['stdout']
274 for file_name in file_names:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300275 source_path = local_path + file_name.rstrip()
276 destination_path = remote_path.rstrip() + file_name.rstrip()
277 LOG.info("Uploading file {0} to {1}:{2}"
278 .format(source_path, node_name, remote_path))
279 remote.upload(source=source_path, target=destination_path)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300280
281 def action_download(self, step):
282 """Download from environment node to local host
283
284 Example:
285
286 - description: Download a file
287 download:
288 remote_path: /tmp/
289 remote_filename: report*.html
290 local_path: /tmp/
291 node_name: ctl01
292 skip_fail: False
293 """
294 node_name = step.get('node_name')
295 remote_path = step.get('download', {}).get('remote_path', None)
296 remote_filename = step.get('download', {}).get('remote_filename', None)
297 local_path = step.get('download', {}).get('local_path', None)
298 description = step.get('description', remote_path)
299 skip_fail = step.get('skip_fail', False)
300
301 if not remote_path or not remote_filename or not local_path:
302 raise Exception("Step '{0}' failed: please specify 'remote_path', "
303 "'remote_filename' and 'local_path' correctly"
304 .format(description))
305
306 with self.__underlay.remote(node_name=node_name) as remote:
307
abaraniuk7a3a05b2018-11-15 16:05:35 +0200308 result = remote.execute('find {0} -maxdepth 1 -type f -name {1}'
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300309 .format(remote_path, remote_filename))
310 LOG.info("Found files to download:\n{0}".format(result))
311
312 if not result['stdout'] and not skip_fail:
313 raise Exception("Nothing to download on step {0}"
314 .format(description))
315
316 file_names = result['stdout']
317 for file_name in file_names:
318 LOG.info("Downloading {0}:{1} to {2}"
319 .format(node_name, file_name, local_path))
320 remote.download(destination=file_name.rstrip(),
321 target=local_path)