|  |  | 
|  | import time | 
|  |  | 
|  | from tcp_tests import logger | 
|  | from tcp_tests.helpers.log_helpers import pretty_repr | 
|  |  | 
|  | LOG = logger.logger | 
|  |  | 
|  |  | 
|  | class ExecuteCommandsMixin(object): | 
|  | """docstring for ExecuteCommands""" | 
|  |  | 
|  | __config = None | 
|  | __underlay = None | 
|  |  | 
|  | def __init__(self, config, underlay): | 
|  | self.__config = config | 
|  | self.__underlay = underlay | 
|  | super(ExecuteCommandsMixin, self).__init__() | 
|  |  | 
|  | def execute_commands(self, commands, label="Command"): | 
|  | """Execute a sequence of commands | 
|  |  | 
|  | Main propose is to implement workarounds for salt formulas like: | 
|  | - exit_code == 0 when there are actual failures | 
|  | - salt_master and/or salt_minion stop working after executing a formula | 
|  | - a formula fails at first run, but completes at next runs | 
|  |  | 
|  | :param label: label of the current sequence of the commands, for log | 
|  | :param commands: list of dicts with the following data: | 
|  | commands = [ | 
|  | ... | 
|  | { | 
|  | # Required: | 
|  | 'cmd': 'shell command(s) to run', | 
|  | 'node_name': 'name of the node to run the command(s)', | 
|  | # Optional: | 
|  | 'description': 'string with a readable command description', | 
|  | 'retry': { | 
|  | 'count': int,  # How many times should be run the command | 
|  | # until success | 
|  | 'delay': int,  # Delay between tries in seconds | 
|  | }, | 
|  | 'skip_fail': bool  # If True - continue with the next step | 
|  | # without failure even if count number | 
|  | # is reached. | 
|  | # If False - rise an exception (default) | 
|  | }, | 
|  | ... | 
|  | ] | 
|  | """ | 
|  | for n, step in enumerate(commands): | 
|  | # Required fields | 
|  | action_cmd = step.get('cmd') | 
|  | action_do = step.get('do') | 
|  | action_upload = step.get('upload') | 
|  | action_download = step.get('download') | 
|  | # node_name = step.get('node_name') | 
|  | # Optional fields | 
|  | description = step.get('description', action_cmd) | 
|  | # retry = step.get('retry', {'count': 1, 'delay': 1}) | 
|  | # retry_count = retry.get('count', 1) | 
|  | # retry_delay = retry.get('delay', 1) | 
|  | # skip_fail = step.get('skip_fail', False) | 
|  |  | 
|  | msg = "[ {0} #{1} ] {2}".format(label, n + 1, description) | 
|  | log_msg = "\n\n{0}\n{1}".format(msg, '=' * len(msg)) | 
|  |  | 
|  | if action_cmd: | 
|  | self.execute_command(step, msg) | 
|  | elif action_do: | 
|  | self.command2(step, msg) | 
|  | elif action_upload: | 
|  | LOG.info(log_msg) | 
|  | self.action_upload(step) | 
|  | elif action_download: | 
|  | LOG.info(log_msg) | 
|  | self.action_download(step) | 
|  |  | 
|  | def execute_command(self, step, msg, return_res=None): | 
|  | # Required fields | 
|  | cmd = step.get('cmd') | 
|  | node_name = step.get('node_name') | 
|  | # Optional fields | 
|  | description = step.get('description', cmd) | 
|  | retry = step.get('retry', {'count': 1, 'delay': 1}) | 
|  | retry_count = retry.get('count', 1) | 
|  | retry_delay = retry.get('delay', 1) | 
|  | skip_fail = step.get('skip_fail', False) | 
|  | timeout = step.get('timeout', None) | 
|  |  | 
|  | with self.__underlay.remote(node_name=node_name) as remote: | 
|  |  | 
|  | for x in range(retry_count, 0, -1): | 
|  | time.sleep(3) | 
|  |  | 
|  | retry_msg = (' (try {0} of {1}, skip_fail={2}, node_name={3})' | 
|  | .format(retry_count - x + 1, | 
|  | retry_count, | 
|  | skip_fail, | 
|  | node_name)) | 
|  | LOG.info("\n\n{0}\n{1}".format( | 
|  | msg + retry_msg, '=' * len(msg + retry_msg))) | 
|  |  | 
|  | result = remote.execute(cmd, timeout=timeout, verbose=True) | 
|  | if return_res: | 
|  | return result | 
|  |  | 
|  | # Workaround of exit code 0 from salt in case of failures | 
|  | failed = 0 | 
|  | for s in result['stdout'] + result['stderr']: | 
|  | if s.startswith("Failed:"): | 
|  | failed += int(s.split("Failed:")[1]) | 
|  | if 'Minion did not return. [No response]' in s: | 
|  | failed += 1 | 
|  | if 'Minion did not return. [Not connected]' in s: | 
|  | failed += 1 | 
|  | if ('Salt request timed out. The master is not responding.' | 
|  | in s): | 
|  | failed += 1 | 
|  | if s.startswith("[CRITICAL]"): | 
|  | failed += 1 | 
|  | if 'Fatal' in s: | 
|  | failed += 1 | 
|  |  | 
|  | if result.exit_code != 0: | 
|  | time.sleep(retry_delay) | 
|  | elif failed != 0: | 
|  | LOG.error( | 
|  | " === SALT returned exit code = 0 while " | 
|  | "there are failed modules! ===") | 
|  | time.sleep(retry_delay) | 
|  | else: | 
|  | break | 
|  |  | 
|  | if x == 1 and skip_fail is False: | 
|  | # In the last retry iteration, raise an exception | 
|  | raise Exception("Step '{0}' failed:\n" | 
|  | "=============== Command: ==============\n" | 
|  | "{1}\n" | 
|  | "=============== STDOUT: ===============\n" | 
|  | "{2}\n" | 
|  | "=============== STDERR: ===============\n" | 
|  | "{3}\n" | 
|  | .format(description, | 
|  | cmd, | 
|  | result.stdout_str, | 
|  | result.stderr_str)) | 
|  |  | 
|  | def command2(self, step, msg): | 
|  | # Required fields | 
|  | do = step['do'] | 
|  | target = step['target'] | 
|  | state = step.get('state') | 
|  | states = step.get('states') | 
|  | # Optional fields | 
|  | args = step.get('args') | 
|  | kwargs = step.get('kwargs') | 
|  | description = step.get('description', do) | 
|  | retry = step.get('retry', {'count': 1, 'delay': 1}) | 
|  | retry_count = retry.get('count', 1) | 
|  | retry_delay = retry.get('delay', 1) | 
|  | skip_fail = step.get('skip_fail', False) | 
|  | timeout = step.get('timeout', None) | 
|  |  | 
|  | if not bool(state) ^ bool(states): | 
|  | raise ValueError("You should use state or states in step") | 
|  |  | 
|  | for x in range(retry_count, 0, -1): | 
|  | time.sleep(3) | 
|  |  | 
|  | retry_msg = (' (try {0} of {1}, skip_fail={2}, target={3})' | 
|  | .format(retry_count - x + 1, | 
|  | retry_count, | 
|  | skip_fail, | 
|  | target)) | 
|  | LOG.info("\n\n{0}\n{1}".format( | 
|  | msg + retry_msg, '=' * len(msg + retry_msg))) | 
|  |  | 
|  | method = getattr(self._salt, self._salt._map[do]) | 
|  | command_ret = method(tgt=target, state=state or states, | 
|  | args=args, kwargs=kwargs, timeout=timeout) | 
|  | command_ret = command_ret if \ | 
|  | isinstance(command_ret, list) else [command_ret] | 
|  | results = [(r['return'][0], f) for r, f in command_ret] | 
|  |  | 
|  | # FIMME: Change to debug level | 
|  | LOG.info(" === States output =======================\n" | 
|  | "{}\n" | 
|  | " =========================================".format( | 
|  | pretty_repr([r for r, f in results]))) | 
|  |  | 
|  | all_fails = [f for r, f in results if f] | 
|  | if all_fails: | 
|  | LOG.error("States finished with failures.\n{}".format( | 
|  | all_fails)) | 
|  | time.sleep(retry_delay) | 
|  | else: | 
|  | break | 
|  |  | 
|  | if x == 1 and skip_fail is False: | 
|  | # In the last retry iteration, raise an exception | 
|  | raise Exception("Step '{0}' failed" | 
|  | .format(description)) | 
|  |  | 
|  | def action_upload(self, step): | 
|  | """Upload from local host to environment node | 
|  |  | 
|  | Example: | 
|  |  | 
|  | - description: Upload a file | 
|  | upload: | 
|  | local_path: /tmp/ | 
|  | local_filename: cirros*.iso | 
|  | remote_path: /tmp/ | 
|  | node_name: ctl01 | 
|  | skip_fail: False | 
|  | """ | 
|  | node_name = step.get('node_name') | 
|  | local_path = step.get('upload', {}).get('local_path', None) | 
|  | local_filename = step.get('upload', {}).get('local_filename', None) | 
|  | remote_path = step.get('upload', {}).get('remote_path', None) | 
|  | description = step.get('description', local_path) | 
|  | skip_fail = step.get('skip_fail', False) | 
|  |  | 
|  | if not local_path or not remote_path: | 
|  | raise Exception("Step '{0}' failed: please specify 'local_path', " | 
|  | "'local_filename' and 'remote_path' correctly" | 
|  | .format(description)) | 
|  |  | 
|  | if not local_filename: | 
|  | # If local_path is not specified then uploading a directory | 
|  | with self.__underlay.remote(node_name=node_name) as remote: | 
|  | LOG.info("Uploading directory {0} to {1}:{2}" | 
|  | .format(local_path, node_name, remote_path)) | 
|  | remote.upload(source=local_path.rstrip(), | 
|  | target=remote_path.rstrip()) | 
|  | return | 
|  |  | 
|  | result = {} | 
|  | with self.__underlay.local() as local: | 
|  | result = local.execute('cd {0} && find . -maxdepth 1 -type f' | 
|  | ' -name "{1}"' | 
|  | .format(local_path, local_filename)) | 
|  | LOG.info("Found files to upload:\n{0}".format(result)) | 
|  |  | 
|  | if not result['stdout'] and not skip_fail: | 
|  | raise Exception("Nothing to upload on step {0}" | 
|  | .format(description)) | 
|  |  | 
|  | with self.__underlay.remote(node_name=node_name) as remote: | 
|  | file_names = result['stdout'] | 
|  | for file_name in file_names: | 
|  | source_path = local_path + file_name.rstrip() | 
|  | destination_path = remote_path.rstrip() + file_name.rstrip() | 
|  | LOG.info("Uploading file {0} to {1}:{2}" | 
|  | .format(source_path, node_name, remote_path)) | 
|  | remote.upload(source=source_path, target=destination_path) | 
|  |  | 
|  | def action_download(self, step): | 
|  | """Download from environment node to local host | 
|  |  | 
|  | Example: | 
|  |  | 
|  | - description: Download a file | 
|  | download: | 
|  | remote_path: /tmp/ | 
|  | remote_filename: report*.html | 
|  | local_path: /tmp/ | 
|  | node_name: ctl01 | 
|  | skip_fail: False | 
|  | """ | 
|  | node_name = step.get('node_name') | 
|  | remote_path = step.get('download', {}).get('remote_path', None) | 
|  | remote_filename = step.get('download', {}).get('remote_filename', None) | 
|  | local_path = step.get('download', {}).get('local_path', None) | 
|  | description = step.get('description', remote_path) | 
|  | skip_fail = step.get('skip_fail', False) | 
|  |  | 
|  | if not remote_path or not remote_filename or not local_path: | 
|  | raise Exception("Step '{0}' failed: please specify 'remote_path', " | 
|  | "'remote_filename' and 'local_path' correctly" | 
|  | .format(description)) | 
|  |  | 
|  | with self.__underlay.remote(node_name=node_name) as remote: | 
|  |  | 
|  | result = remote.execute('find {0} -maxdepth 1 -type f -name {1}' | 
|  | .format(remote_path, remote_filename)) | 
|  | LOG.info("Found files to download:\n{0}".format(result)) | 
|  |  | 
|  | if not result['stdout'] and not skip_fail: | 
|  | raise Exception("Nothing to download on step {0}" | 
|  | .format(description)) | 
|  |  | 
|  | file_names = result['stdout'] | 
|  | for file_name in file_names: | 
|  | LOG.info("Downloading {0}:{1} to {2}" | 
|  | .format(node_name, file_name, local_path)) | 
|  | remote.download(destination=file_name.rstrip(), | 
|  | target=local_path) |