| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 1 |  | 
|  | 2 | import time | 
|  | 3 |  | 
|  | 4 | from tcp_tests import logger | 
|  | 5 | from tcp_tests.helpers.log_helpers import pretty_repr | 
|  | 6 |  | 
|  | 7 | LOG = logger.logger | 
|  | 8 |  | 
|  | 9 |  | 
|  | 10 | class ExecuteCommandsMixin(object): | 
|  | 11 | """docstring for ExecuteCommands""" | 
|  | 12 |  | 
| Dmitry Tyzhnenko | bc0f826 | 2017-04-28 15:39:26 +0300 | [diff] [blame] | 13 | __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 Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 21 | 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 Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 54 | action_cmd = step.get('cmd') | 
|  | 55 | action_do = step.get('do') | 
|  | 56 | action_upload = step.get('upload') | 
|  | 57 | action_download = step.get('download') | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 58 | # node_name = step.get('node_name') | 
|  | 59 | # Optional fields | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 60 | description = step.get('description', action_cmd) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 61 | # 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) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 67 |  | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 68 | if action_cmd: | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 69 | self.execute_command(step, msg) | 
|  | 70 | return | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 71 | elif action_do: | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 72 | self.command2(step, msg) | 
|  | 73 | return | 
|  | 74 | LOG.info("\n\n{0}\n{1}".format(msg, '=' * len(msg))) | 
|  | 75 | if action_upload: | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 76 | self.action_upload(step) | 
|  | 77 | elif action_download: | 
|  | 78 | self.action_download(step) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 79 |  | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 80 | def execute_command(self, step, msg): | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 81 | # 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) | 
|  | 90 |  | 
| Dmitry Tyzhnenko | bc0f826 | 2017-04-28 15:39:26 +0300 | [diff] [blame] | 91 | with self.__underlay.remote(node_name=node_name) as remote: | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 92 |  | 
|  | 93 | for x in range(retry_count, 0, -1): | 
|  | 94 | time.sleep(3) | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 95 |  | 
|  | 96 | retry_msg = ' (try {0} of {1}, skip_fail={2})'.format( | 
|  | 97 | retry_count - x + 1, retry_count, skip_fail) | 
|  | 98 | LOG.info("\n\n{0}\n{1}".format( | 
|  | 99 | msg + retry_msg, '=' * len(msg + retry_msg))) | 
|  | 100 |  | 
|  | 101 | result = remote.execute('set -ex; ' + cmd, verbose=True) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 102 |  | 
|  | 103 | # Workaround of exit code 0 from salt in case of failures | 
|  | 104 | failed = 0 | 
| Dennis Dmitriev | 4db5bf2 | 2017-05-13 19:31:17 +0300 | [diff] [blame] | 105 | for s in result['stdout'] + result['stderr']: | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 106 | if s.startswith("Failed:"): | 
|  | 107 | failed += int(s.split("Failed:")[1]) | 
| Dennis Dmitriev | 68671a6 | 2017-05-13 16:40:32 +0300 | [diff] [blame] | 108 | if 'Minion did not return. [No response]' in s: | 
|  | 109 | failed += 1 | 
| Dennis Dmitriev | f854617 | 2017-07-20 21:57:05 +0300 | [diff] [blame] | 110 | if 'Minion did not return. [Not connected]' in s: | 
|  | 111 | failed += 1 | 
| Dennis Dmitriev | 68671a6 | 2017-05-13 16:40:32 +0300 | [diff] [blame] | 112 | if s.startswith("[CRITICAL]"): | 
|  | 113 | failed += 1 | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 114 |  | 
|  | 115 | if result.exit_code != 0: | 
|  | 116 | time.sleep(retry_delay) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 117 | elif failed != 0: | 
|  | 118 | LOG.error( | 
|  | 119 | " === SALT returned exit code = 0 while " | 
|  | 120 | "there are failed modules! ===") | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 121 | time.sleep(retry_delay) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 122 | else: | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 123 | break | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 124 |  | 
|  | 125 | if x == 1 and skip_fail is False: | 
|  | 126 | # In the last retry iteration, raise an exception | 
|  | 127 | raise Exception("Step '{0}' failed" | 
|  | 128 | .format(description)) | 
|  | 129 |  | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 130 | def command2(self, step, msg): | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 131 | # Required fields | 
|  | 132 | do = step['do'] | 
|  | 133 | target = step['target'] | 
|  | 134 | state = step.get('state') | 
|  | 135 | states = step.get('states') | 
|  | 136 | # Optional fields | 
|  | 137 | args = step.get('args') | 
|  | 138 | kwargs = step.get('kwargs') | 
|  | 139 | description = step.get('description', do) | 
|  | 140 | retry = step.get('retry', {'count': 1, 'delay': 1}) | 
|  | 141 | retry_count = retry.get('count', 1) | 
|  | 142 | retry_delay = retry.get('delay', 1) | 
|  | 143 | skip_fail = step.get('skip_fail', False) | 
|  | 144 |  | 
|  | 145 | if not bool(state) ^ bool(states): | 
|  | 146 | raise ValueError("You should use state or states in step") | 
|  | 147 |  | 
|  | 148 | for x in range(retry_count, 0, -1): | 
|  | 149 | time.sleep(3) | 
|  | 150 |  | 
| Dennis Dmitriev | 4a0c89c | 2018-02-12 21:10:29 +0200 | [diff] [blame^] | 151 | retry_msg = ' (try {0} of {1}, skip_fail={2})'.format( | 
|  | 152 | retry_count - x + 1, retry_count, skip_fail) | 
|  | 153 | LOG.info("\n\n{0}\n{1}".format( | 
|  | 154 | msg + retry_msg, '=' * len(msg + retry_msg))) | 
|  | 155 |  | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 156 | method = getattr(self._salt, self._salt._map[do]) | 
|  | 157 | command_ret = method(tgt=target, state=state or states, | 
|  | 158 | args=args, kwargs=kwargs) | 
|  | 159 | command_ret = command_ret if \ | 
|  | 160 | isinstance(command_ret, list) else [command_ret] | 
|  | 161 | results = [(r['return'][0], f) for r, f in command_ret] | 
|  | 162 |  | 
|  | 163 | # FIMME: Change to debug level | 
|  | 164 | LOG.info(" === States output =======================\n" | 
|  | 165 | "{}\n" | 
|  | 166 | " =========================================".format( | 
|  | 167 | pretty_repr([r for r, f in results]))) | 
|  | 168 |  | 
|  | 169 | all_fails = [f for r, f in results if f] | 
|  | 170 | if all_fails: | 
|  | 171 | LOG.error("States finished with failures.\n{}".format( | 
|  | 172 | all_fails)) | 
|  | 173 | time.sleep(retry_delay) | 
| Dmitry Tyzhnenko | 2b730a0 | 2017-04-07 19:31:32 +0300 | [diff] [blame] | 174 | else: | 
|  | 175 | break | 
|  | 176 |  | 
|  | 177 | if x == 1 and skip_fail is False: | 
|  | 178 | # In the last retry iteration, raise an exception | 
|  | 179 | raise Exception("Step '{0}' failed" | 
|  | 180 | .format(description)) | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 181 |  | 
|  | 182 | def action_upload(self, step): | 
|  | 183 | """Upload from local host to environment node | 
|  | 184 |  | 
|  | 185 | Example: | 
|  | 186 |  | 
|  | 187 | - description: Upload a file | 
|  | 188 | upload: | 
|  | 189 | local_path: /tmp/ | 
|  | 190 | local_filename: cirros*.iso | 
|  | 191 | remote_path: /tmp/ | 
|  | 192 | node_name: ctl01 | 
|  | 193 | skip_fail: False | 
|  | 194 | """ | 
|  | 195 | node_name = step.get('node_name') | 
|  | 196 | local_path = step.get('upload', {}).get('local_path', None) | 
|  | 197 | local_filename = step.get('upload', {}).get('local_filename', None) | 
|  | 198 | remote_path = step.get('upload', {}).get('remote_path', None) | 
|  | 199 | description = step.get('description', local_path) | 
|  | 200 | skip_fail = step.get('skip_fail', False) | 
|  | 201 |  | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 202 | if not local_path or not remote_path: | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 203 | raise Exception("Step '{0}' failed: please specify 'local_path', " | 
|  | 204 | "'local_filename' and 'remote_path' correctly" | 
|  | 205 | .format(description)) | 
|  | 206 |  | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 207 | if not local_filename: | 
|  | 208 | # If local_path is not specified then uploading a directory | 
|  | 209 | with self.__underlay.remote(node_name=node_name) as remote: | 
|  | 210 | LOG.info("Uploading directory {0} to {1}:{2}" | 
|  | 211 | .format(local_path, node_name, remote_path)) | 
| Dmitry Tyzhnenko | 8f6a63e | 2017-09-05 15:37:23 +0300 | [diff] [blame] | 212 | remote.upload(source=local_path.rstrip(), | 
|  | 213 | target=remote_path.rstrip()) | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 214 | return | 
|  | 215 |  | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 216 | result = {} | 
|  | 217 | with self.__underlay.local() as local: | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 218 | result = local.execute('cd {0} && find . -type f -name "{1}"' | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 219 | .format(local_path, local_filename)) | 
|  | 220 | LOG.info("Found files to upload:\n{0}".format(result)) | 
|  | 221 |  | 
|  | 222 | if not result['stdout'] and not skip_fail: | 
|  | 223 | raise Exception("Nothing to upload on step {0}" | 
|  | 224 | .format(description)) | 
|  | 225 |  | 
|  | 226 | with self.__underlay.remote(node_name=node_name) as remote: | 
|  | 227 | file_names = result['stdout'] | 
|  | 228 | for file_name in file_names: | 
| Dennis Dmitriev | 9dada8a | 2017-08-30 17:38:55 +0300 | [diff] [blame] | 229 | source_path = local_path + file_name.rstrip() | 
|  | 230 | destination_path = remote_path.rstrip() + file_name.rstrip() | 
|  | 231 | LOG.info("Uploading file {0} to {1}:{2}" | 
|  | 232 | .format(source_path, node_name, remote_path)) | 
|  | 233 | remote.upload(source=source_path, target=destination_path) | 
| Dennis Dmitriev | 2dfb8ef | 2017-07-21 20:19:38 +0300 | [diff] [blame] | 234 |  | 
|  | 235 | def action_download(self, step): | 
|  | 236 | """Download from environment node to local host | 
|  | 237 |  | 
|  | 238 | Example: | 
|  | 239 |  | 
|  | 240 | - description: Download a file | 
|  | 241 | download: | 
|  | 242 | remote_path: /tmp/ | 
|  | 243 | remote_filename: report*.html | 
|  | 244 | local_path: /tmp/ | 
|  | 245 | node_name: ctl01 | 
|  | 246 | skip_fail: False | 
|  | 247 | """ | 
|  | 248 | node_name = step.get('node_name') | 
|  | 249 | remote_path = step.get('download', {}).get('remote_path', None) | 
|  | 250 | remote_filename = step.get('download', {}).get('remote_filename', None) | 
|  | 251 | local_path = step.get('download', {}).get('local_path', None) | 
|  | 252 | description = step.get('description', remote_path) | 
|  | 253 | skip_fail = step.get('skip_fail', False) | 
|  | 254 |  | 
|  | 255 | if not remote_path or not remote_filename or not local_path: | 
|  | 256 | raise Exception("Step '{0}' failed: please specify 'remote_path', " | 
|  | 257 | "'remote_filename' and 'local_path' correctly" | 
|  | 258 | .format(description)) | 
|  | 259 |  | 
|  | 260 | with self.__underlay.remote(node_name=node_name) as remote: | 
|  | 261 |  | 
|  | 262 | result = remote.execute('find {0} -type f -name {1}' | 
|  | 263 | .format(remote_path, remote_filename)) | 
|  | 264 | LOG.info("Found files to download:\n{0}".format(result)) | 
|  | 265 |  | 
|  | 266 | if not result['stdout'] and not skip_fail: | 
|  | 267 | raise Exception("Nothing to download on step {0}" | 
|  | 268 | .format(description)) | 
|  | 269 |  | 
|  | 270 | file_names = result['stdout'] | 
|  | 271 | for file_name in file_names: | 
|  | 272 | LOG.info("Downloading {0}:{1} to {2}" | 
|  | 273 | .format(node_name, file_name, local_path)) | 
|  | 274 | remote.download(destination=file_name.rstrip(), | 
|  | 275 | target=local_path) |