salt/cli/salt.py
# -*- 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('')