saltstack/salt

View on GitHub
salt/cli/salt.py

Summary

Maintainability
F
5 days
Test Coverage
# -*- coding: utf-8 -*-

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import sys
sys.modules['pkg_resources'] = None
import os

# Import Salt libs
import salt.defaults.exitcodes
import salt.utils.job
import salt.utils.parsers
import salt.utils.stringutils
import salt.log
from salt.utils.args import yamlify_arg
from salt.utils.verify import verify_log
from salt.exceptions import (
    AuthenticationError,
    AuthorizationError,
    EauthAuthenticationError,
    LoaderError,
    SaltClientError,
    SaltInvocationError,
    SaltSystemExit
)

# Import 3rd-party libs
from salt.ext import six


class SaltCMD(salt.utils.parsers.SaltCMDOptionParser):
    '''
    The execution of a salt command happens here
    '''

    def run(self):
        '''
        Execute the salt command line
        '''
        import salt.client
        self.parse_args()

        if self.config['log_level'] not in ('quiet', ):
            # Setup file logging!
            self.setup_logfile_logger()
            verify_log(self.config)

        try:
            # We don't need to bail on config file permission errors
            # if the CLI process is run with the -a flag
            skip_perm_errors = self.options.eauth != ''

            self.local_client = salt.client.get_local_client(
                self.get_config_file_path(),
                skip_perm_errors=skip_perm_errors,
                auto_reconnect=True)
        except SaltClientError as exc:
            self.exit(2, '{0}\n'.format(exc))
            return

        if self.options.batch or self.options.static:
            # _run_batch() will handle all output and
            # exit with the appropriate error condition
            # Execution will not continue past this point
            # in batch mode.
            self._run_batch()
            return

        if self.options.preview_target:
            minion_list = self._preview_target()
            self._output_ret(minion_list, self.config.get('output', 'nested'))
            return

        if self.options.timeout <= 0:
            self.options.timeout = self.local_client.opts['timeout']

        kwargs = {
            'tgt': self.config['tgt'],
            'fun': self.config['fun'],
            'arg': self.config['arg'],
            'timeout': self.options.timeout,
            'show_timeout': self.options.show_timeout,
            'show_jid': self.options.show_jid}

        if 'token' in self.config:
            import salt.utils.files
            try:
                with salt.utils.files.fopen(os.path.join(self.config['cachedir'], '.root_key'), 'r') as fp_:
                    kwargs['key'] = fp_.readline()
            except IOError:
                kwargs['token'] = self.config['token']

        kwargs['delimiter'] = self.options.delimiter

        if self.selected_target_option:
            kwargs['tgt_type'] = self.selected_target_option
        else:
            kwargs['tgt_type'] = 'glob'

        # If batch_safe_limit is set, check minions matching target and
        # potentially switch to batch execution
        if self.options.batch_safe_limit > 1:
            if len(self._preview_target()) >= self.options.batch_safe_limit:
                salt.utils.stringutils.print_cli('\nNOTICE: Too many minions targeted, switching to batch execution.')
                self.options.batch = self.options.batch_safe_size
                self._run_batch()
                return

        if getattr(self.options, 'return'):
            kwargs['ret'] = getattr(self.options, 'return')

        if getattr(self.options, 'return_config'):
            kwargs['ret_config'] = getattr(self.options, 'return_config')

        if getattr(self.options, 'return_kwargs'):
            kwargs['ret_kwargs'] = yamlify_arg(
                    getattr(self.options, 'return_kwargs'))

        if getattr(self.options, 'module_executors'):
            kwargs['module_executors'] = yamlify_arg(getattr(self.options, 'module_executors'))

        if getattr(self.options, 'executor_opts'):
            kwargs['executor_opts'] = yamlify_arg(getattr(self.options, 'executor_opts'))

        if getattr(self.options, 'metadata'):
            kwargs['metadata'] = yamlify_arg(
                    getattr(self.options, 'metadata'))

        # If using eauth and a token hasn't already been loaded into
        # kwargs, prompt the user to enter auth credentials
        if 'token' not in kwargs and 'key' not in kwargs and self.options.eauth:
            # This is expensive. Don't do it unless we need to.
            import salt.auth
            resolver = salt.auth.Resolver(self.config)
            res = resolver.cli(self.options.eauth)
            if self.options.mktoken and res:
                tok = resolver.token_cli(
                        self.options.eauth,
                        res
                        )
                if tok:
                    kwargs['token'] = tok.get('token', '')
            if not res:
                sys.stderr.write('ERROR: Authentication failed\n')
                sys.exit(2)
            kwargs.update(res)
            kwargs['eauth'] = self.options.eauth

        if self.config['async']:
            jid = self.local_client.cmd_async(**kwargs)
            salt.utils.stringutils.print_cli('Executed command with job ID: {0}'.format(jid))
            return

        # local will be None when there was an error
        if not self.local_client:
            return

        retcodes = []
        errors = []

        try:
            if self.options.subset:
                cmd_func = self.local_client.cmd_subset
                kwargs['sub'] = self.options.subset
                kwargs['cli'] = True
            else:
                cmd_func = self.local_client.cmd_cli

            if self.options.progress:
                kwargs['progress'] = True
                self.config['progress'] = True
                ret = {}
                for progress in cmd_func(**kwargs):
                    out = 'progress'
                    try:
                        self._progress_ret(progress, out)
                    except LoaderError as exc:
                        raise SaltSystemExit(exc)
                    if 'return_count' not in progress:
                        ret.update(progress)
                self._progress_end(out)
                self._print_returns_summary(ret)
            elif self.config['fun'] == 'sys.doc':
                ret = {}
                out = ''
                for full_ret in self.local_client.cmd_cli(**kwargs):
                    ret_, out, retcode = self._format_ret(full_ret)
                    ret.update(ret_)
                self._output_ret(ret, out, retcode=retcode)
            else:
                if self.options.verbose:
                    kwargs['verbose'] = True
                ret = {}
                for full_ret in cmd_func(**kwargs):
                    try:
                        ret_, out, retcode = self._format_ret(full_ret)
                        retcodes.append(retcode)
                        self._output_ret(ret_, out, retcode=retcode)
                        ret.update(full_ret)
                    except KeyError:
                        errors.append(full_ret)

            # Returns summary
            if self.config['cli_summary'] is True:
                if self.config['fun'] != 'sys.doc':
                    if self.options.output is None:
                        self._print_returns_summary(ret)
                        self._print_errors_summary(errors)

            # NOTE: Return code is set here based on if all minions
            # returned 'ok' with a retcode of 0.
            # This is the final point before the 'salt' cmd returns,
            # which is why we set the retcode here.
            if not all(exit_code == salt.defaults.exitcodes.EX_OK for exit_code in retcodes):
                sys.stderr.write('ERROR: Minions returned with non-zero exit code\n')
                sys.exit(salt.defaults.exitcodes.EX_GENERIC)

        except (AuthenticationError,
                AuthorizationError,
                SaltInvocationError,
                EauthAuthenticationError,
                SaltClientError) as exc:
            ret = six.text_type(exc)
            self._output_ret(ret, '', retcode=1)

    def _preview_target(self):
        '''
        Return a list of minions from a given target
        '''
        return self.local_client.gather_minions(self.config['tgt'], self.selected_target_option or 'glob')

    def _run_batch(self):
        import salt.cli.batch
        eauth = {}
        if 'token' in self.config:
            eauth['token'] = self.config['token']

        # If using eauth and a token hasn't already been loaded into
        # kwargs, prompt the user to enter auth credentials
        if 'token' not in eauth and self.options.eauth:
            # This is expensive. Don't do it unless we need to.
            import salt.auth
            resolver = salt.auth.Resolver(self.config)
            res = resolver.cli(self.options.eauth)
            if self.options.mktoken and res:
                tok = resolver.token_cli(
                        self.options.eauth,
                        res
                        )
                if tok:
                    eauth['token'] = tok.get('token', '')
            if not res:
                sys.stderr.write('ERROR: Authentication failed\n')
                sys.exit(2)
            eauth.update(res)
            eauth['eauth'] = self.options.eauth

        if self.options.static:

            if not self.options.batch:
                self.config['batch'] = '100%'

            try:
                batch = salt.cli.batch.Batch(self.config, eauth=eauth, quiet=True)
            except SaltClientError:
                sys.exit(2)

            ret = {}

            for res in batch.run():
                ret.update(res)

            self._output_ret(ret, '')

        else:
            try:
                self.config['batch'] = self.options.batch
                batch = salt.cli.batch.Batch(self.config, eauth=eauth, parser=self.options)
            except SaltClientError:
                # We will print errors to the console further down the stack
                sys.exit(1)
            # Printing the output is already taken care of in run() itself
            retcode = 0
            for res in batch.run():
                for ret in six.itervalues(res):
                    job_retcode = salt.utils.job.get_retcode(ret)
                    if job_retcode > retcode:
                        # Exit with the highest retcode we find
                        retcode = job_retcode
            sys.exit(retcode)

    def _print_errors_summary(self, errors):
        if errors:
            salt.utils.stringutils.print_cli('\n')
            salt.utils.stringutils.print_cli('---------------------------')
            salt.utils.stringutils.print_cli('Errors')
            salt.utils.stringutils.print_cli('---------------------------')
            for error in errors:
                salt.utils.stringutils.print_cli(self._format_error(error))

    def _print_returns_summary(self, ret):
        '''
        Display returns summary
        '''
        return_counter = 0
        not_return_counter = 0
        not_return_minions = []
        not_response_minions = []
        not_connected_minions = []
        failed_minions = []
        for each_minion in ret:
            minion_ret = ret[each_minion]
            if isinstance(minion_ret, dict) and 'ret' in minion_ret:
                minion_ret = ret[each_minion].get('ret')
            if (
                    isinstance(minion_ret, six.string_types)
                    and minion_ret.startswith("Minion did not return")
                    ):
                if "Not connected" in minion_ret:
                    not_connected_minions.append(each_minion)
                elif "No response" in minion_ret:
                    not_response_minions.append(each_minion)
                not_return_counter += 1
                not_return_minions.append(each_minion)
            else:
                return_counter += 1
                if self._get_retcode(ret[each_minion]):
                    failed_minions.append(each_minion)
        salt.utils.stringutils.print_cli('\n')
        salt.utils.stringutils.print_cli('-------------------------------------------')
        salt.utils.stringutils.print_cli('Summary')
        salt.utils.stringutils.print_cli('-------------------------------------------')
        salt.utils.stringutils.print_cli('# of minions targeted: {0}'.format(return_counter + not_return_counter))
        salt.utils.stringutils.print_cli('# of minions returned: {0}'.format(return_counter))
        salt.utils.stringutils.print_cli('# of minions that did not return: {0}'.format(not_return_counter))
        salt.utils.stringutils.print_cli('# of minions with errors: {0}'.format(len(failed_minions)))
        if self.options.verbose:
            if not_connected_minions:
                salt.utils.stringutils.print_cli('Minions not connected: {0}'.format(" ".join(not_connected_minions)))
            if not_response_minions:
                salt.utils.stringutils.print_cli('Minions not responding: {0}'.format(" ".join(not_response_minions)))
            if failed_minions:
                salt.utils.stringutils.print_cli('Minions with failures: {0}'.format(" ".join(failed_minions)))
        salt.utils.stringutils.print_cli('-------------------------------------------')

    def _progress_end(self, out):
        import salt.output
        salt.output.progress_end(self.progress_bar)

    def _progress_ret(self, progress, out):
        '''
        Print progress events
        '''
        import salt.output
        # Get the progress bar
        if not hasattr(self, 'progress_bar'):
            try:
                self.progress_bar = salt.output.get_progress(self.config, out, progress)
            except Exception:
                raise LoaderError('\nWARNING: Install the `progressbar` python package. '
                                  'Requested job was still run but output cannot be displayed.\n')
        salt.output.update_progress(self.config, progress, self.progress_bar, out)

    def _output_ret(self, ret, out, retcode=0):
        '''
        Print the output from a single return to the terminal
        '''
        import salt.output
        # Handle special case commands
        if self.config['fun'] == 'sys.doc' and not isinstance(ret, Exception):
            self._print_docs(ret)
        else:
            # Determine the proper output method and run it
            salt.output.display_output(ret,
                                       out=out,
                                       opts=self.config,
                                       _retcode=retcode)
        if not ret:
            sys.stderr.write('ERROR: No return received\n')
            sys.exit(2)

    def _format_ret(self, full_ret):
        '''
        Take the full return data and format it to simple output
        '''
        ret = {}
        out = ''
        retcode = 0
        for key, data in six.iteritems(full_ret):
            ret[key] = data['ret']
            if 'out' in data:
                out = data['out']
            ret_retcode = self._get_retcode(data)
            if ret_retcode > retcode:
                retcode = ret_retcode
        return ret, out, retcode

    def _get_retcode(self, ret):
        '''
        Determine a retcode for a given return
        '''
        retcode = 0
        # if there is a dict with retcode, use that
        if isinstance(ret, dict) and ret.get('retcode', 0) != 0:
            if isinstance(ret.get('retcode', 0), dict):
                return max(six.itervalues(ret.get('retcode', {0: 0})))
            return ret['retcode']
        # if its a boolean, False means 1
        elif isinstance(ret, bool) and not ret:
            return 1
        return retcode

    def _format_error(self, minion_error):
        for minion, error_doc in six.iteritems(minion_error):
            error = 'Minion [{0}] encountered exception \'{1}\''.format(minion, error_doc['message'])
        return error

    def _print_docs(self, ret):
        '''
        Print out the docstrings for all of the functions on the minions
        '''
        import salt.output
        docs = {}
        if not ret:
            self.exit(2, 'No minions found to gather docs from\n')
        if isinstance(ret, six.string_types):
            self.exit(2, '{0}\n'.format(ret))
        for host in ret:
            if isinstance(ret[host], six.string_types) \
                    and (ret[host].startswith("Minion did not return")
                         or ret[host] == 'VALUE_TRIMMED'):
                continue
            for fun in ret[host]:
                if fun not in docs and ret[host][fun]:
                    docs[fun] = ret[host][fun]
        if self.options.output:
            for fun in sorted(docs):
                salt.output.display_output({fun: docs[fun]}, 'nested', self.config)
        else:
            for fun in sorted(docs):
                salt.utils.stringutils.print_cli('{0}:'.format(fun))
                salt.utils.stringutils.print_cli(docs[fun])
                salt.utils.stringutils.print_cli('')