blob: 1b961b243729a40c4d2db2fa6504e06759cba512 [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 ensure_running_service(self, service_name, host, check_cmd,
22 state_running='start/running'):
23 """Check if the service_name running or try to restart it
24
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030025 :param node_name: node on which the service will be checked
26 :param check_cmd: shell command to ensure that the service is running
27 :param state_running: string for check the service state
28 """
29 cmd = "service {0} status | grep -q '{1}'".format(
30 service_name, state_running)
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030031 with self.__underlay.remote(host=host) as remote:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030032 result = remote.execute(cmd)
33 if result.exit_code != 0:
34 LOG.info("{0} is not in running state on the node {1},"
35 " trying to start".format(service_name, host))
36 cmd = ("service {0} stop;"
37 " sleep 3; killall -9 {0};"
38 "service {0} start; sleep 5;"
39 .format(service_name))
40 remote.execute(cmd)
41
42 remote.execute(check_cmd)
43 remote.execute(check_cmd)
44
45 def execute_commands(self, commands, label="Command"):
46 """Execute a sequence of commands
47
48 Main propose is to implement workarounds for salt formulas like:
49 - exit_code == 0 when there are actual failures
50 - salt_master and/or salt_minion stop working after executing a formula
51 - a formula fails at first run, but completes at next runs
52
53 :param label: label of the current sequence of the commands, for log
54 :param commands: list of dicts with the following data:
55 commands = [
56 ...
57 {
58 # Required:
59 'cmd': 'shell command(s) to run',
60 'node_name': 'name of the node to run the command(s)',
61 # Optional:
62 'description': 'string with a readable command description',
63 'retry': {
64 'count': int, # How many times should be run the command
65 # until success
66 'delay': int, # Delay between tries in seconds
67 },
68 'skip_fail': bool # If True - continue with the next step
69 # without failure even if count number
70 # is reached.
71 # If False - rise an exception (default)
72 },
73 ...
74 ]
75 """
76 for n, step in enumerate(commands):
77 # Required fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030078 action_cmd = step.get('cmd')
79 action_do = step.get('do')
80 action_upload = step.get('upload')
81 action_download = step.get('download')
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030082 # node_name = step.get('node_name')
83 # Optional fields
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030084 description = step.get('description', action_cmd)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030085 # retry = step.get('retry', {'count': 1, 'delay': 1})
86 # retry_count = retry.get('count', 1)
87 # retry_delay = retry.get('delay', 1)
88 # skip_fail = step.get('skip_fail', False)
89
90 msg = "[ {0} #{1} ] {2}".format(label, n + 1, description)
91 LOG.info("\n\n{0}\n{1}".format(msg, '=' * len(msg)))
92
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030093 if action_cmd:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030094 self.execute_command(step)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030095 elif action_do:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030096 self.command2(step)
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030097 elif action_upload:
98 self.action_upload(step)
99 elif action_download:
100 self.action_download(step)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300101
102 def execute_command(self, step):
103 # 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)
112
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300113 with self.__underlay.remote(node_name=node_name) as remote:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300114
115 for x in range(retry_count, 0, -1):
116 time.sleep(3)
117 result = remote.execute(cmd, verbose=True)
118
119 # Workaround of exit code 0 from salt in case of failures
120 failed = 0
Dennis Dmitriev4db5bf22017-05-13 19:31:17 +0300121 for s in result['stdout'] + result['stderr']:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300122 if s.startswith("Failed:"):
123 failed += int(s.split("Failed:")[1])
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300124 if 'Minion did not return. [No response]' in s:
125 failed += 1
Dennis Dmitrievf8546172017-07-20 21:57:05 +0300126 if 'Minion did not return. [Not connected]' in s:
127 failed += 1
Dennis Dmitriev68671a62017-05-13 16:40:32 +0300128 if s.startswith("[CRITICAL]"):
129 failed += 1
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300130
131 if result.exit_code != 0:
132 time.sleep(retry_delay)
133 LOG.info(
134 " === RETRY ({0}/{1}) ========================="
135 .format(x - 1, retry_count))
136 elif failed != 0:
137 LOG.error(
138 " === SALT returned exit code = 0 while "
139 "there are failed modules! ===")
140 LOG.info(
141 " === RETRY ({0}/{1}) ======================="
142 .format(x - 1, retry_count))
143 else:
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300144 if self.__config.salt.salt_master_host != '0.0.0.0':
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300145 # Workarounds for crashed services
146 self.ensure_running_service(
147 "salt-master",
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300148 self.__config.salt.salt_master_host,
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300149 "salt-call pillar.items",
150 'active (running)') # Hardcoded for now
151 self.ensure_running_service(
152 "salt-minion",
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300153 self.__config.salt.salt_master_host,
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300154 "salt 'cfg01*' pillar.items",
155 "active (running)") # Hardcoded for now
156 break
157
158 if x == 1 and skip_fail is False:
159 # In the last retry iteration, raise an exception
160 raise Exception("Step '{0}' failed"
161 .format(description))
162
163 def command2(self, step):
164 # Required fields
165 do = step['do']
166 target = step['target']
167 state = step.get('state')
168 states = step.get('states')
169 # Optional fields
170 args = step.get('args')
171 kwargs = step.get('kwargs')
172 description = step.get('description', do)
173 retry = step.get('retry', {'count': 1, 'delay': 1})
174 retry_count = retry.get('count', 1)
175 retry_delay = retry.get('delay', 1)
176 skip_fail = step.get('skip_fail', False)
177
178 if not bool(state) ^ bool(states):
179 raise ValueError("You should use state or states in step")
180
181 for x in range(retry_count, 0, -1):
182 time.sleep(3)
183
184 method = getattr(self._salt, self._salt._map[do])
185 command_ret = method(tgt=target, state=state or states,
186 args=args, kwargs=kwargs)
187 command_ret = command_ret if \
188 isinstance(command_ret, list) else [command_ret]
189 results = [(r['return'][0], f) for r, f in command_ret]
190
191 # FIMME: Change to debug level
192 LOG.info(" === States output =======================\n"
193 "{}\n"
194 " =========================================".format(
195 pretty_repr([r for r, f in results])))
196
197 all_fails = [f for r, f in results if f]
198 if all_fails:
199 LOG.error("States finished with failures.\n{}".format(
200 all_fails))
201 time.sleep(retry_delay)
202 LOG.info(" === RETRY ({0}/{1}) ========================="
203 .format(x - 1, retry_count))
204 else:
205 break
206
207 if x == 1 and skip_fail is False:
208 # In the last retry iteration, raise an exception
209 raise Exception("Step '{0}' failed"
210 .format(description))
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300211
212 def action_upload(self, step):
213 """Upload from local host to environment node
214
215 Example:
216
217 - description: Upload a file
218 upload:
219 local_path: /tmp/
220 local_filename: cirros*.iso
221 remote_path: /tmp/
222 node_name: ctl01
223 skip_fail: False
224 """
225 node_name = step.get('node_name')
226 local_path = step.get('upload', {}).get('local_path', None)
227 local_filename = step.get('upload', {}).get('local_filename', None)
228 remote_path = step.get('upload', {}).get('remote_path', None)
229 description = step.get('description', local_path)
230 skip_fail = step.get('skip_fail', False)
231
232 if not local_path or not local_filename or not remote_path:
233 raise Exception("Step '{0}' failed: please specify 'local_path', "
234 "'local_filename' and 'remote_path' correctly"
235 .format(description))
236
237 result = {}
238 with self.__underlay.local() as local:
239 result = local.execute('find {0} -type f -name {1}'
240 .format(local_path, local_filename))
241 LOG.info("Found files to upload:\n{0}".format(result))
242
243 if not result['stdout'] and not skip_fail:
244 raise Exception("Nothing to upload on step {0}"
245 .format(description))
246
247 with self.__underlay.remote(node_name=node_name) as remote:
248 file_names = result['stdout']
249 for file_name in file_names:
250 LOG.info("Uploading {0} to {1}:{2}"
251 .format(local_path, node_name, file_name))
252 remote.upload(source=local_path, target=file_name.rstrip())
253
254 def action_download(self, step):
255 """Download from environment node to local host
256
257 Example:
258
259 - description: Download a file
260 download:
261 remote_path: /tmp/
262 remote_filename: report*.html
263 local_path: /tmp/
264 node_name: ctl01
265 skip_fail: False
266 """
267 node_name = step.get('node_name')
268 remote_path = step.get('download', {}).get('remote_path', None)
269 remote_filename = step.get('download', {}).get('remote_filename', None)
270 local_path = step.get('download', {}).get('local_path', None)
271 description = step.get('description', remote_path)
272 skip_fail = step.get('skip_fail', False)
273
274 if not remote_path or not remote_filename or not local_path:
275 raise Exception("Step '{0}' failed: please specify 'remote_path', "
276 "'remote_filename' and 'local_path' correctly"
277 .format(description))
278
279 with self.__underlay.remote(node_name=node_name) as remote:
280
281 result = remote.execute('find {0} -type f -name {1}'
282 .format(remote_path, remote_filename))
283 LOG.info("Found files to download:\n{0}".format(result))
284
285 if not result['stdout'] and not skip_fail:
286 raise Exception("Nothing to download on step {0}"
287 .format(description))
288
289 file_names = result['stdout']
290 for file_name in file_names:
291 LOG.info("Downloading {0}:{1} to {2}"
292 .format(node_name, file_name, local_path))
293 remote.download(destination=file_name.rstrip(),
294 target=local_path)