blob: 3c018675cadf82e6c9c245cec0a0a7243fbdcff3 [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)
67 LOG.info("\n\n{0}\n{1}".format(msg, '=' * len(msg)))
68
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030069 if action_cmd:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030070 self.execute_command(step)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030071 elif action_do:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030072 self.command2(step)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030073 elif action_upload:
74 self.action_upload(step)
75 elif action_download:
76 self.action_download(step)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030077
78 def execute_command(self, step):
79 # Required fields
80 cmd = step.get('cmd')
81 node_name = step.get('node_name')
82 # Optional fields
83 description = step.get('description', cmd)
84 retry = step.get('retry', {'count': 1, 'delay': 1})
85 retry_count = retry.get('count', 1)
86 retry_delay = retry.get('delay', 1)
87 skip_fail = step.get('skip_fail', False)
88
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030089 with self.__underlay.remote(node_name=node_name) as remote:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030090
91 for x in range(retry_count, 0, -1):
92 time.sleep(3)
93 result = remote.execute(cmd, verbose=True)
94
95 # Workaround of exit code 0 from salt in case of failures
96 failed = 0
Dennis Dmitriev4db5bf22017-05-13 19:31:17 +030097 for s in result['stdout'] + result['stderr']:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030098 if s.startswith("Failed:"):
99 failed += int(s.split("Failed:")[1])
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300100 if 'Minion did not return. [No response]' in s:
101 failed += 1
Dennis Dmitrievf8546172017-07-20 21:57:05 +0300102 if 'Minion did not return. [Not connected]' in s:
103 failed += 1
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300104 if s.startswith("[CRITICAL]"):
105 failed += 1
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300106
107 if result.exit_code != 0:
108 time.sleep(retry_delay)
109 LOG.info(
110 " === RETRY ({0}/{1}) ========================="
111 .format(x - 1, retry_count))
112 elif failed != 0:
113 LOG.error(
114 " === SALT returned exit code = 0 while "
115 "there are failed modules! ===")
116 LOG.info(
117 " === RETRY ({0}/{1}) ======================="
118 .format(x - 1, retry_count))
119 else:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300120 break
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300121
122 if x == 1 and skip_fail is False:
123 # In the last retry iteration, raise an exception
124 raise Exception("Step '{0}' failed"
125 .format(description))
126
127 def command2(self, step):
128 # Required fields
129 do = step['do']
130 target = step['target']
131 state = step.get('state')
132 states = step.get('states')
133 # Optional fields
134 args = step.get('args')
135 kwargs = step.get('kwargs')
136 description = step.get('description', do)
137 retry = step.get('retry', {'count': 1, 'delay': 1})
138 retry_count = retry.get('count', 1)
139 retry_delay = retry.get('delay', 1)
140 skip_fail = step.get('skip_fail', False)
141
142 if not bool(state) ^ bool(states):
143 raise ValueError("You should use state or states in step")
144
145 for x in range(retry_count, 0, -1):
146 time.sleep(3)
147
148 method = getattr(self._salt, self._salt._map[do])
149 command_ret = method(tgt=target, state=state or states,
150 args=args, kwargs=kwargs)
151 command_ret = command_ret if \
152 isinstance(command_ret, list) else [command_ret]
153 results = [(r['return'][0], f) for r, f in command_ret]
154
155 # FIMME: Change to debug level
156 LOG.info(" === States output =======================\n"
157 "{}\n"
158 " =========================================".format(
159 pretty_repr([r for r, f in results])))
160
161 all_fails = [f for r, f in results if f]
162 if all_fails:
163 LOG.error("States finished with failures.\n{}".format(
164 all_fails))
165 time.sleep(retry_delay)
166 LOG.info(" === RETRY ({0}/{1}) ========================="
167 .format(x - 1, retry_count))
168 else:
169 break
170
171 if x == 1 and skip_fail is False:
172 # In the last retry iteration, raise an exception
173 raise Exception("Step '{0}' failed"
174 .format(description))
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300175
176 def action_upload(self, step):
177 """Upload from local host to environment node
178
179 Example:
180
181 - description: Upload a file
182 upload:
183 local_path: /tmp/
184 local_filename: cirros*.iso
185 remote_path: /tmp/
186 node_name: ctl01
187 skip_fail: False
188 """
189 node_name = step.get('node_name')
190 local_path = step.get('upload', {}).get('local_path', None)
191 local_filename = step.get('upload', {}).get('local_filename', None)
192 remote_path = step.get('upload', {}).get('remote_path', None)
193 description = step.get('description', local_path)
194 skip_fail = step.get('skip_fail', False)
195
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300196 if not local_path or not remote_path:
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300197 raise Exception("Step '{0}' failed: please specify 'local_path', "
198 "'local_filename' and 'remote_path' correctly"
199 .format(description))
200
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300201 if not local_filename:
202 # If local_path is not specified then uploading a directory
203 with self.__underlay.remote(node_name=node_name) as remote:
204 LOG.info("Uploading directory {0} to {1}:{2}"
205 .format(local_path, node_name, remote_path))
Dmitry Tyzhnenko8f6a63e2017-09-05 15:37:23 +0300206 remote.upload(source=local_path.rstrip(),
207 target=remote_path.rstrip())
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300208 return
209
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300210 result = {}
211 with self.__underlay.local() as local:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300212 result = local.execute('cd {0} && find . -type f -name "{1}"'
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300213 .format(local_path, local_filename))
214 LOG.info("Found files to upload:\n{0}".format(result))
215
216 if not result['stdout'] and not skip_fail:
217 raise Exception("Nothing to upload on step {0}"
218 .format(description))
219
220 with self.__underlay.remote(node_name=node_name) as remote:
221 file_names = result['stdout']
222 for file_name in file_names:
Dennis Dmitriev9dada8a2017-08-30 17:38:55 +0300223 source_path = local_path + file_name.rstrip()
224 destination_path = remote_path.rstrip() + file_name.rstrip()
225 LOG.info("Uploading file {0} to {1}:{2}"
226 .format(source_path, node_name, remote_path))
227 remote.upload(source=source_path, target=destination_path)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300228
229 def action_download(self, step):
230 """Download from environment node to local host
231
232 Example:
233
234 - description: Download a file
235 download:
236 remote_path: /tmp/
237 remote_filename: report*.html
238 local_path: /tmp/
239 node_name: ctl01
240 skip_fail: False
241 """
242 node_name = step.get('node_name')
243 remote_path = step.get('download', {}).get('remote_path', None)
244 remote_filename = step.get('download', {}).get('remote_filename', None)
245 local_path = step.get('download', {}).get('local_path', None)
246 description = step.get('description', remote_path)
247 skip_fail = step.get('skip_fail', False)
248
249 if not remote_path or not remote_filename or not local_path:
250 raise Exception("Step '{0}' failed: please specify 'remote_path', "
251 "'remote_filename' and 'local_path' correctly"
252 .format(description))
253
254 with self.__underlay.remote(node_name=node_name) as remote:
255
256 result = remote.execute('find {0} -type f -name {1}'
257 .format(remote_path, remote_filename))
258 LOG.info("Found files to download:\n{0}".format(result))
259
260 if not result['stdout'] and not skip_fail:
261 raise Exception("Nothing to download on step {0}"
262 .format(description))
263
264 file_names = result['stdout']
265 for file_name in file_names:
266 LOG.info("Downloading {0}:{1} to {2}"
267 .format(node_name, file_name, local_path))
268 remote.download(destination=file_name.rstrip(),
269 target=local_path)