blob: 6dcf6157574fb87c8607d2d113e2582dda7ccb63 [file] [log] [blame]
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +03001
2import time
3
4from tcp_tests import logger
5from tcp_tests.helpers.log_helpers import pretty_repr
6
7LOG = logger.logger
8
9
10class ExecuteCommandsMixin(object):
11 """docstring for ExecuteCommands"""
12
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030013 __config = None
14 __underlay = None
15
16 def __init__(self, config, underlay):
17 self.__config = config
18 self.__underlay = underlay
19 super(ExecuteCommandsMixin, self).__init__()
20
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030021 def execute_commands(self, commands, label="Command"):
22 """Execute a sequence of commands
23
24 Main propose is to implement workarounds for salt formulas like:
25 - exit_code == 0 when there are actual failures
26 - salt_master and/or salt_minion stop working after executing a formula
27 - a formula fails at first run, but completes at next runs
28
29 :param label: label of the current sequence of the commands, for log
30 :param commands: list of dicts with the following data:
31 commands = [
32 ...
33 {
34 # Required:
35 'cmd': 'shell command(s) to run',
36 'node_name': 'name of the node to run the command(s)',
37 # Optional:
38 'description': 'string with a readable command description',
39 'retry': {
40 'count': int, # How many times should be run the command
41 # until success
42 'delay': int, # Delay between tries in seconds
43 },
44 'skip_fail': bool # If True - continue with the next step
45 # without failure even if count number
46 # is reached.
47 # If False - rise an exception (default)
48 },
49 ...
50 ]
51 """
52 for n, step in enumerate(commands):
53 # Required fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030054 action_cmd = step.get('cmd')
55 action_do = step.get('do')
56 action_upload = step.get('upload')
57 action_download = step.get('download')
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030058 # node_name = step.get('node_name')
59 # Optional fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030060 description = step.get('description', action_cmd)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030061 # retry = step.get('retry', {'count': 1, 'delay': 1})
62 # retry_count = retry.get('count', 1)
63 # retry_delay = retry.get('delay', 1)
64 # skip_fail = step.get('skip_fail', False)
65
66 msg = "[ {0} #{1} ] {2}".format(label, n + 1, description)
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020067 log_msg = "\n\n{0}\n{1}".format(msg, '=' * len(msg))
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030068
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030069 if action_cmd:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020070 self.execute_command(step, msg)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030071 elif action_do:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020072 self.command2(step, msg)
Dennis Dmitriev707bfeb2018-03-15 17:50:28 -050073 elif action_upload:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020074 LOG.info(log_msg)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030075 self.action_upload(step)
76 elif action_download:
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020077 LOG.info(log_msg)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030078 self.action_download(step)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030079
Tatyana Leontovichafe8f952018-06-20 15:33:03 +030080 def execute_command(self, step, msg, return_res=None):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030081 # Required fields
82 cmd = step.get('cmd')
83 node_name = step.get('node_name')
84 # Optional fields
85 description = step.get('description', cmd)
86 retry = step.get('retry', {'count': 1, 'delay': 1})
87 retry_count = retry.get('count', 1)
88 retry_delay = retry.get('delay', 1)
89 skip_fail = step.get('skip_fail', False)
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +000090 timeout = step.get('timeout', None)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030091
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030092 with self.__underlay.remote(node_name=node_name) as remote:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030093
94 for x in range(retry_count, 0, -1):
95 time.sleep(3)
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +020096
Dennis Dmitrievce646fb2018-03-21 09:10:00 +020097 retry_msg = (' (try {0} of {1}, skip_fail={2}, node_name={3})'
98 .format(retry_count - x + 1,
99 retry_count,
100 skip_fail,
101 node_name))
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200102 LOG.info("\n\n{0}\n{1}".format(
103 msg + retry_msg, '=' * len(msg + retry_msg)))
104
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000105 result = remote.execute(cmd, timeout=timeout, verbose=True)
Tatyana Leontovichafe8f952018-06-20 15:33:03 +0300106 if return_res:
107 return result
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300108
109 # Workaround of exit code 0 from salt in case of failures
110 failed = 0
Dennis Dmitriev4db5bf22017-05-13 19:31:17 +0300111 for s in result['stdout'] + result['stderr']:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300112 if s.startswith("Failed:"):
113 failed += int(s.split("Failed:")[1])
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300114 if 'Minion did not return. [No response]' in s:
115 failed += 1
Dennis Dmitrievf8546172017-07-20 21:57:05 +0300116 if 'Minion did not return. [Not connected]' in s:
117 failed += 1
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200118 if ('Salt request timed out. The master is not responding.'
119 in s):
120 failed += 1
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300121 if s.startswith("[CRITICAL]"):
122 failed += 1
Tatyana Leontovich835465f2018-06-14 16:42:23 +0300123 if 'Fatal' in s:
124 failed += 1
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300125
126 if result.exit_code != 0:
127 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300128 elif failed != 0:
129 LOG.error(
130 " === SALT returned exit code = 0 while "
131 "there are failed modules! ===")
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200132 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300133 else:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300134 break
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300135
136 if x == 1 and skip_fail is False:
137 # In the last retry iteration, raise an exception
Dennis Dmitriev47800162018-10-31 11:57:02 +0200138 raise Exception("Step '{0}' failed:\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200139 "=============== Command: ==============\n"
Dennis Dmitriev44f6db22018-10-31 16:07:56 +0200140 "{1}\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200141 "=============== STDOUT: ===============\n"
Dennis Dmitriev44f6db22018-10-31 16:07:56 +0200142 "{2}\n"
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200143 "=============== STDERR: ===============\n"
144 "{3}\n"
Dennis Dmitriev47800162018-10-31 11:57:02 +0200145 .format(description,
Dennis Dmitriev79abb0d2018-11-06 16:48:30 +0200146 cmd,
Dennis Dmitriev47800162018-10-31 11:57:02 +0200147 result.stdout_str,
148 result.stderr_str))
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300149
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200150 def command2(self, step, msg):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300151 # Required fields
152 do = step['do']
153 target = step['target']
154 state = step.get('state')
155 states = step.get('states')
156 # Optional fields
157 args = step.get('args')
158 kwargs = step.get('kwargs')
159 description = step.get('description', do)
160 retry = step.get('retry', {'count': 1, 'delay': 1})
161 retry_count = retry.get('count', 1)
162 retry_delay = retry.get('delay', 1)
163 skip_fail = step.get('skip_fail', False)
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000164 timeout = step.get('timeout', None)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300165
166 if not bool(state) ^ bool(states):
167 raise ValueError("You should use state or states in step")
168
169 for x in range(retry_count, 0, -1):
170 time.sleep(3)
171
Dennis Dmitrievce646fb2018-03-21 09:10:00 +0200172 retry_msg = (' (try {0} of {1}, skip_fail={2}, target={3})'
173 .format(retry_count - x + 1,
174 retry_count,
175 skip_fail,
176 target))
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200177 LOG.info("\n\n{0}\n{1}".format(
178 msg + retry_msg, '=' * len(msg + retry_msg)))
179
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300180 method = getattr(self._salt, self._salt._map[do])
181 command_ret = method(tgt=target, state=state or states,
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000182 args=args, kwargs=kwargs, timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300183 command_ret = command_ret if \
184 isinstance(command_ret, list) else [command_ret]
185 results = [(r['return'][0], f) for r, f in command_ret]
186
187 # FIMME: Change to debug level
188 LOG.info(" === States output =======================\n"
189 "{}\n"
190 " =========================================".format(
191 pretty_repr([r for r, f in results])))
192
193 all_fails = [f for r, f in results if f]
194 if all_fails:
195 LOG.error("States finished with failures.\n{}".format(
196 all_fails))
197 time.sleep(retry_delay)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300198 else:
199 break
200
201 if x == 1 and skip_fail is False:
202 # In the last retry iteration, raise an exception
203 raise Exception("Step '{0}' failed"
204 .format(description))
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300205
206 def action_upload(self, step):
207 """Upload from local host to environment node
208
209 Example:
210
211 - description: Upload a file
212 upload:
213 local_path: /tmp/
214 local_filename: cirros*.iso
215 remote_path: /tmp/
216 node_name: ctl01
217 skip_fail: False
218 """
219 node_name = step.get('node_name')
220 local_path = step.get('upload', {}).get('local_path', None)
221 local_filename = step.get('upload', {}).get('local_filename', None)
222 remote_path = step.get('upload', {}).get('remote_path', None)
223 description = step.get('description', local_path)
224 skip_fail = step.get('skip_fail', False)
225
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300226 if not local_path or not remote_path:
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300227 raise Exception("Step '{0}' failed: please specify 'local_path', "
228 "'local_filename' and 'remote_path' correctly"
229 .format(description))
230
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300231 if not local_filename:
232 # If local_path is not specified then uploading a directory
233 with self.__underlay.remote(node_name=node_name) as remote:
234 LOG.info("Uploading directory {0} to {1}:{2}"
235 .format(local_path, node_name, remote_path))
Dmitry Tyzhnenko8f6a63e2017-09-05 15:37:23 +0300236 remote.upload(source=local_path.rstrip(),
237 target=remote_path.rstrip())
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300238 return
239
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300240 result = {}
241 with self.__underlay.local() as local:
abaraniuk7a3a05b2018-11-15 16:05:35 +0200242 result = local.execute('cd {0} && find . -maxdepth 1 -type f'
243 ' -name "{1}"'
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300244 .format(local_path, local_filename))
245 LOG.info("Found files to upload:\n{0}".format(result))
246
247 if not result['stdout'] and not skip_fail:
248 raise Exception("Nothing to upload on step {0}"
249 .format(description))
250
251 with self.__underlay.remote(node_name=node_name) as remote:
252 file_names = result['stdout']
253 for file_name in file_names:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300254 source_path = local_path + file_name.rstrip()
255 destination_path = remote_path.rstrip() + file_name.rstrip()
256 LOG.info("Uploading file {0} to {1}:{2}"
257 .format(source_path, node_name, remote_path))
258 remote.upload(source=source_path, target=destination_path)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300259
260 def action_download(self, step):
261 """Download from environment node to local host
262
263 Example:
264
265 - description: Download a file
266 download:
267 remote_path: /tmp/
268 remote_filename: report*.html
269 local_path: /tmp/
270 node_name: ctl01
271 skip_fail: False
272 """
273 node_name = step.get('node_name')
274 remote_path = step.get('download', {}).get('remote_path', None)
275 remote_filename = step.get('download', {}).get('remote_filename', None)
276 local_path = step.get('download', {}).get('local_path', None)
277 description = step.get('description', remote_path)
278 skip_fail = step.get('skip_fail', False)
279
280 if not remote_path or not remote_filename or not local_path:
281 raise Exception("Step '{0}' failed: please specify 'remote_path', "
282 "'remote_filename' and 'local_path' correctly"
283 .format(description))
284
285 with self.__underlay.remote(node_name=node_name) as remote:
286
abaraniuk7a3a05b2018-11-15 16:05:35 +0200287 result = remote.execute('find {0} -maxdepth 1 -type f -name {1}'
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300288 .format(remote_path, remote_filename))
289 LOG.info("Found files to download:\n{0}".format(result))
290
291 if not result['stdout'] and not skip_fail:
292 raise Exception("Nothing to download on step {0}"
293 .format(description))
294
295 file_names = result['stdout']
296 for file_name in file_names:
297 LOG.info("Downloading {0}:{1} to {2}"
298 .format(node_name, file_name, local_path))
299 remote.download(destination=file_name.rstrip(),
300 target=local_path)