Cleanup the salt(salt.cli.SaltCMD) binary parser.

* Created 2 mix-ins as option groups, the output options and the target options. This will allow adding some explanatory text besides separating these options from the parser's main options.
* All options on the parser, including the grouped options are now merged to the loaded configuration which will latter get passed on.
* Also created the timeout mix-in which will be used in other binaries.
This commit is contained in:
Pedro Algarvio 2012-08-04 22:08:06 +01:00
parent 503f4bdf08
commit b582e40ff1
5 changed files with 395 additions and 312 deletions

View file

@ -15,310 +15,73 @@ import salt.client
import salt.output
import salt.runner
#from salt.utils import parsers as optparse
import optparse
from salt.utils import parsers
from salt.utils.verify import verify_env
from salt.version import __version__ as VERSION
from salt.exceptions import SaltInvocationError, SaltClientError, \
SaltException
from salt.exceptions import SaltInvocationError, SaltClientError, SaltException
class SaltCMD(object):
class SaltCMD(parsers.SaltCMDOptionParser):
'''
The execution of a salt command happens here
'''
def __init__(self):
'''
Create a SaltCMD object
'''
self.opts = self.__parse()
def __parse(self):
'''
Parse the command line
'''
usage = "%prog [options] '<target>' <function> [arguments]"
parser = optparse.OptionParser(version="%%prog %s" % VERSION, usage=usage)
parser.add_option('-t',
'--timeout',
default=None,
dest='timeout',
help=('Set the return timeout for batch jobs; '
'default=5 seconds'))
parser.add_option('-s',
'--static',
default=False,
dest='static',
action='store_true',
help=('Return the data from minions as a group after they '
'all return.'))
parser.add_option('-v',
'--verbose',
default=False,
dest='verbose',
action='store_true',
help=('Turn on command verbosity, display jid and active job '
'queries'))
parser.add_option('-b',
'--batch',
'--batch-size',
default='',
dest='batch',
help=('Execute the salt job in batch mode, pass either the '
'number of minions to batch at a time, or the '
'percentage of minions to have running'))
parser.add_option('-E',
'--pcre',
default=False,
dest='pcre',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'servers, use pcre regular expressions'))
parser.add_option('-L',
'--list',
default=False,
dest='list',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'servers, take a comma delimited list of servers.'))
parser.add_option('-G',
'--grain',
default=False,
dest='grain',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a grain value to identify targets, the syntax '
'for the target is the grain key followed by a glob'
'expression:\n"os:Arch*"'))
parser.add_option('--grain-pcre',
default=False,
dest='grain_pcre',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a grain value to identify targets, the syntax '
'for the target is the grain key followed by a pcre '
'regular expression:\n"os:Arch.*"'))
parser.add_option('-X',
'--exsel',
default=False,
dest='exsel',
action='store_true',
help=('Instead of using shell globs use the return code '
'of a function.'))
parser.add_option('-I',
'--pillar',
default=False,
dest='pillar',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a pillar value to identify targets, the syntax '
'for the target is the pillar key followed by a glob'
'expression:\n"role:production*"'))
parser.add_option('-N',
'--nodegroup',
default=False,
dest='nodegroup',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use one of the predefined nodegroups to identify a '
'list of targets.'))
parser.add_option('-R',
'--range',
default=False,
dest='range',
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a range expression to identify targets. '
'Range expressions look like %cluster'))
parser.add_option('-C',
'--compound',
default=False,
dest='compound',
action='store_true',
help=('The compound target option allows for multiple '
'target types to be evaluated, allowing for greater '
'granularity in target matching. The compound target '
'is space delimited, targets other than globs are '
'preceted with an identifyer matching the specific '
'targets argument type: salt \'G@os:RedHat and '
'webser* or E@database.*\''))
parser.add_option('--return',
default='',
dest='return',
metavar='RETURNER',
help=('Set an alternative return method. By default salt will '
'send the return data from the command back to the '
'master, but the return data can be redirected into '
'any number of systems, databases or applications.'))
parser.add_option('-Q',
'--query',
dest='query',
action='store_true',
help=('This option is deprecated and will be removed in a '
'future release, please use salt-run jobs instead\n'
'Execute a salt command query, this can be used to find '
'the results of a previous function call: -Q test.echo'))
parser.add_option('-c',
'--config',
default='/etc/salt/master',
dest='conf_file',
help=('The location of the salt master configuration file, '
'the salt master settings are required to know where '
'the connections are; default=/etc/salt/master'))
parser.add_option('--raw-out',
default=False,
action='store_true',
dest='raw_out',
help=('Print the output from the salt command in raw python '
'form, this is suitable for re-reading the output into '
'an executing python script with eval.'))
parser.add_option('--text-out',
default=False,
action='store_true',
dest='txt_out',
help=('Print the output from the salt command in the same '
'form the shell would.'))
parser.add_option('--yaml-out',
default=False,
action='store_true',
dest='yaml_out',
help='Print the output from the salt command in yaml.')
parser.add_option('--json-out',
default=False,
action='store_true',
dest='json_out',
help='Print the output from the salt command in json.')
parser.add_option('--no-color',
default=False,
action='store_true',
dest='no_color',
help='Disable all colored output')
options, args = parser.parse_args()
opts = {}
for k, v in options.__dict__.items():
if v is not None:
opts[k] = v
if not options.timeout is None:
opts['timeout'] = int(options.timeout)
if options.query:
opts['query'] = options.query
if len(args) < 1:
err = ('Please pass in a command to query the old salt '
'calls for.')
sys.stderr.write(err + '\n')
sys.exit('2')
opts['cmd'] = args[0]
else:
# Catch invalid invocations of salt such as: salt run
if len(args) <= 1:
parser.print_help()
parser.exit(1)
if opts['list']:
opts['tgt'] = args[0].split(',')
else:
opts['tgt'] = args[0]
# Detect compound command and set up the data for it
if ',' in args[1]:
opts['fun'] = args[1].split(',')
opts['arg'] = []
for comp in ' '.join(args[2:]).split(','):
opts['arg'].append(comp.split())
if len(opts['fun']) != len(opts['arg']):
err = ('Cannot execute compound command without defining '
'all arguments.')
sys.stderr.write(err + '\n')
sys.exit(42)
else:
opts['fun'] = args[1]
opts['arg'] = args[2:]
return opts
def run(self):
'''
Execute the salt command line
'''
self.parse_args()
try:
local = salt.client.LocalClient(self.opts['conf_file'])
local = salt.client.LocalClient(self.get_config_file_path('master'))
except SaltClientError as exc:
sys.stderr.write('{0}\n'.format(exc))
sys.exit(2)
self.exit(2, '{0}\n'.format(exc))
return
if 'query' in self.opts:
ret = local.find_cmd(self.opts['cmd'])
if self.options.query:
ret = local.find_cmd(self.config['cmd'])
for jid in ret:
if isinstance(ret, list) or isinstance(ret, dict):
# Determine the proper output method and run it
get_outputter = salt.output.get_outputter
if self.opts['raw_out']:
printout = get_outputter('raw')
elif self.opts['json_out']:
printout = get_outputter('json')
elif self.opts['txt_out']:
printout = get_outputter('txt')
elif self.opts['yaml_out']:
printout = get_outputter('yaml')
else:
printout = get_outputter(None)
printout = self.get_outputter()
print('Return data for job {0}:'.format(jid))
printout(ret[jid])
print('')
elif self.opts['batch']:
batch = salt.cli.batch.Batch(self.opts)
elif self.options.batch:
batch = salt.cli.batch.Batch(self.config)
batch.run()
else:
if not 'timeout' in self.opts:
self.opts['timeout'] = local.opts['timeout']
args = [self.opts['tgt'],
self.opts['fun'],
self.opts['arg'],
self.opts['timeout'],
]
if self.opts['pcre']:
args.append('pcre')
elif self.opts['list']:
args.append('list')
elif self.opts['grain']:
args.append('grain')
elif self.opts['grain_pcre']:
args.append('grain_pcre')
elif self.opts['exsel']:
args.append('exsel')
elif self.opts['pillar']:
args.append('pillar')
elif self.opts['nodegroup']:
args.append('nodegroup')
elif self.opts['range']:
args.append('range')
elif self.opts['compound']:
args.append('compound')
if self.options.timeout <= 0:
self.options.timeout = local.opts['timeout']
args = [
self.config['tgt'],
self.config['fun'],
self.config['arg'],
self.options.timeout,
]
if self.selected_target_option:
args.append(self.selected_target_option)
else:
args.append('glob')
if self.opts['return']:
args.append(self.opts['return'])
if getattr(self.options, 'return'):
args.append(getattr(self.options, 'return'))
else:
args.append('')
try:
# local will be None when there was an error
if local:
if self.opts['static']:
if self.opts['verbose']:
if self.options.static:
if self.options.verbose:
args.append(True)
full_ret = local.cmd_full_return(*args)
ret, out = self._format_ret(full_ret)
self._output_ret(ret, out)
elif self.opts['fun'] == 'sys.doc':
elif self.config['fun'] == 'sys.doc':
ret = {}
out = ''
for full_ret in local.cmd_cli(*args):
@ -326,7 +89,7 @@ class SaltCMD(object):
ret.update(ret_)
self._output_ret(ret, out)
else:
if self.opts['verbose']:
if self.options.verbose:
args.append(True)
for full_ret in local.cmd_cli(*args):
ret, out = self._format_ret(full_ret)
@ -340,29 +103,11 @@ class SaltCMD(object):
Print the output from a single return to the terminal
'''
# Handle special case commands
if self.opts['fun'] == 'sys.doc':
if self.config['fun'] == 'sys.doc':
self._print_docs(ret)
else:
# Determine the proper output method and run it
get_outputter = salt.output.get_outputter
if isinstance(ret, list) or isinstance(ret, dict):
if self.opts['raw_out']:
printout = get_outputter('raw')
elif self.opts['json_out']:
printout = get_outputter('json')
elif self.opts['txt_out']:
printout = get_outputter('txt')
elif self.opts['yaml_out']:
printout = get_outputter('yaml')
elif out:
printout = get_outputter(out)
else:
printout = get_outputter(None)
# Pretty print any salt exceptions
elif isinstance(ret, SaltException):
printout = get_outputter("txt")
color = not bool(self.opts['no_color'])
printout(ret, color=color)
salt.output.display_output(ret, out, self.config)
def _format_ret(self, full_ret):
'''
@ -382,7 +127,8 @@ class SaltCMD(object):
'''
docs = {}
if not ret:
sys.stderr.write('No minions found to gather docs from\n')
self.exit(2, 'No minions found to gather docs from\n')
for host in ret:
for fun in ret[host]:
if fun not in docs:

View file

@ -21,32 +21,37 @@ import salt.utils
from salt._compat import string_types
from salt.exceptions import SaltException
__all__ = ('get_outputter',)
__all__ = ('get_outputter', 'get_printout')
log = logging.getLogger(__name__)
def get_printout(ret, out, opts):
"""
Return the proper printout
"""
if isinstance(ret, list) or isinstance(ret, dict):
if opts['raw_out']:
return get_outputter('raw')
elif opts['json_out']:
return get_outputter('json')
elif opts.get('txt_out', False):
return get_outputter('txt')
elif opts['yaml_out']:
return get_outputter('yaml')
elif out:
return get_outputter(out)
else:
return get_outputter(None)
# Pretty print any salt exceptions
elif isinstance(ret, SaltException):
return get_outputter("txt")
def display_output(ret, out, opts):
'''
Display the output of a command in the terminal
'''
if isinstance(ret, list) or isinstance(ret, dict):
if opts['raw_out']:
printout = get_outputter('raw')
elif opts['json_out']:
printout = get_outputter('json')
elif opts['txt_out']:
printout = get_outputter('txt')
elif opts['yaml_out']:
printout = get_outputter('yaml')
elif out:
printout = get_outputter(out)
else:
printout = get_outputter(None)
# Pretty print any salt exceptions
elif isinstance(ret, SaltException):
printout = get_outputter("txt")
printout(ret)
printout = get_printout(ret, out, opts)
printout(ret, color=not bool(opts['no_color']))
class Outputter(object):

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
salt.utils.parser
~~~~~~~~~~~~~~~~~
salt.utils.parsers
~~~~~~~~~~~~~~~~~~
:copyright: © 2012 UfSoft.org - :email:`Pedro Algarvio (pedro@algarvio.me)`
:license: Apache 2.0, see LICENSE for more details.
@ -10,8 +10,10 @@
import os
import sys
import optparse
from functools import partial
from salt import config, log, version
def _sorted(mixins_or_funcs):
return sorted(
mixins_or_funcs, key=lambda mf: getattr(mf, '_mixin_prio_', 1000)
@ -106,7 +108,12 @@ class OptionParser(optparse.OptionParser):
process_option_funcs.append(process_option_func)
for process_option_func in _sorted(process_option_funcs):
process_option_func()
try:
process_option_func()
except Exception, err:
self.error("Error while processing {0}: {1}".format(
process_option_func, err
))
# Run the functions on self._mixin_after_parsed_funcs
for mixin_after_parsed_func in self._mixin_after_parsed_funcs:
@ -155,6 +162,7 @@ class ConfigDirMixIn(DeprecatedConfigMessage):
)
def __merge_config_with_cli(self, *args):
# Merge parser options
for option in self.option_list:
if not option.dest:
# --version does not have dest attribute set for example.
@ -163,9 +171,18 @@ class ConfigDirMixIn(DeprecatedConfigMessage):
continue
value = getattr(self.options, option.dest, None)
if value:
if value is not None:
self.config[option.dest] = value
# Merge parser group options if any
for group in self.option_groups:
for option in group.option_list:
if not option.dest:
continue
value = getattr(self.options, option.dest, None)
if value is not None:
self.config[option.dest] = value
def process_config_dir(self):
# XXX: Remove deprecation warning in next release
if os.path.isfile(self.options.config_dir):
@ -289,6 +306,231 @@ class PidfileMixin(object):
set_pidfile(self.config['pidfile'])
class TargetOptionsMixIn(object):
__metaclass__ = MixInMeta
_mixin_prio_ = 20
selected_target_option = None
def _mixin_setup(self):
group = self.target_options_group = optparse.OptionGroup(
self, "Target Options", "Target Selection Options"
)
self.add_option_group(group)
group.add_option(
'-E', '--pcre',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'servers, use pcre regular expressions')
)
group.add_option(
'-L', '--list',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'servers, take a comma delimited list of servers.')
)
group.add_option(
'-G', '--grain',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a grain value to identify targets, the syntax '
'for the target is the grain key followed by a glob'
'expression:\n"os:Arch*"')
)
group.add_option(
'--grain-pcre',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a grain value to identify targets, the syntax '
'for the target is the grain key followed by a pcre '
'regular expression:\n"os:Arch.*"')
)
group.add_option(
'-N', '--nodegroup',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use one of the predefined nodegroups to identify a '
'list of targets.')
)
group.add_option(
'-R', '--range',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a range expression to identify targets. '
'Range expressions look like %cluster')
)
self._create_process_functions()
def _create_process_functions(self):
for option in self.target_options_group.option_list:
def process(opt):
if getattr(self.options, opt.dest):
self.selected_target_option = opt.dest
funcname = 'process_%s' % option.dest
if not hasattr(self, funcname):
setattr(self, funcname, partial(process, option))
def _mixin_after_parsed(self):
group_options_selected = filter(
lambda option: getattr(self.options, option.dest) is True,
self.target_options_group.option_list
)
if len(group_options_selected) > 1:
self.error(
"The options {0} are mutually exclusive. Please only choose "
"one of them".format('/'.join([
option.get_opt_string() for option in group_options_selected
]))
)
class ExtendedTargetOptionsMixIn(TargetOptionsMixIn):
def _mixin_setup(self):
TargetOptionsMixIn._mixin_setup(self)
group = self.target_options_group
group.add_option(
'-C', '--compound',
default=False,
action='store_true',
help=('The compound target option allows for multiple target types '
'to be evaluated, allowing for greater granularity in target '
'matching. The compound target is space delimited, targets '
'other than globs are preceded with an identifier matching '
'the specific targets argument type: salt \'G@os:RedHat and '
'webser* or E@database.*\'')
)
group.add_option(
'-X', '--exsel',
default=False,
action='store_true',
help=('Instead of using shell globs use the return code of '
'a function.')
)
group.add_option(
'-I', '--pillar',
default=False,
action='store_true',
help=('Instead of using shell globs to evaluate the target '
'use a pillar value to identify targets, the syntax '
'for the target is the pillar key followed by a glob'
'expression:\n"role:production*"')
)
self._create_process_functions()
class TimeoutMixIn(object):
__metaclass__ = MixInMeta
_mixin_prio_ = 10
def _mixin_setup(self):
if not hasattr(self, 'default_timeout'):
raise RuntimeError("You need to define the 'default_timeout' "
"attribute on %s" % self.__class__.__name__)
self.add_option(
'-t', '--timeout',
type=int,
default=self.default_timeout,
help=('Change the timeout, if applicable, for the running command; '
'default=%default')
)
class OutputOptionsMixIn(object):
__metaclass__ = MixInMeta
_mixin_prio_ = 40
_include_text_out_ = False
def _mixin_setup(self):
group = self.output_options_group = optparse.OptionGroup(
self, "Output Options", "Configure your preferred output format"
)
self.add_option_group(group)
group.add_option(
'--raw-out',
default=False,
action='store_true',
help=('Print the output from the salt-key command in raw python '
'form, this is suitable for re-reading the output into an '
'executing python script with eval.')
)
group.add_option(
'--yaml-out',
default=False,
action='store_true',
help='Print the output from the salt-key command in yaml.'
)
group.add_option(
'--json-out',
default=False,
action='store_true',
help='Print the output from the salt-key command in json.'
)
if self._include_text_out_:
group.add_option(
'--text-out',
default=False,
action='store_true',
help=('Print the output from the salt command in the same '
'form the shell would.')
)
group.add_option(
'--no-color',
default=False,
action='store_true',
help='Disable all colored output'
)
def _mixin_after_parsed(self):
group_options_selected = filter(
lambda option: getattr(self.options, option.dest) and
option.dest.endswith('_out'),
self.output_options_group.option_list
)
if len(group_options_selected) > 1:
self.error(
"The options {0} are mutually exclusive. Please only choose "
"one of them".format('/'.join([
option.get_opt_string() for option in group_options_selected
]))
)
# def get_printout(self, outputter=None):
# # Determine the proper output method and run it
# from salt.output import get_printout
# return get_outputter(outputter)
#
# if outputter is not None:
# return get_outputter(outputter)
#
# if self.options.raw_out:
# outputter = 'raw'
# elif self.options.json_out:
# outputter = 'json'
# elif self._include_text_out_ and self.options.text_out:
# outputter = 'txt'
# elif self.options.yaml_out:
# outputter = 'yaml'
#
# if outputter is not None:
# return get_outputter(outputter)
#
# return None
class OutputOptionsWithTextMixIn(OutputOptionsMixIn):
_include_text_out_ = True
class MasterOptionParser(OptionParser, ConfigDirMixIn, LogLevelMixIn,
DeprecatedMasterMinionMixIn, RunUserMixin,
DaemonMixIn, PidfileMixin):
@ -343,3 +585,91 @@ class SyndicOptionParser(OptionParser, DeprecatedSyndicOptionsMixIn,
opts['_master_conf_file'] = opts['conf_file']
opts.pop('conf_file')
return opts
class SaltCMDOptionParser(OptionParser, ConfigDirMixIn, TimeoutMixIn,
ExtendedTargetOptionsMixIn,
OutputOptionsWithTextMixIn):
__metaclass__ = OptionParserMeta
default_timeout = 5
usage = "%prog [options] '<target>' <function> [arguments]"
def _mixin_setup(self):
self.add_option(
'-s', '--static',
default=False,
action='store_true',
help=('Return the data from minions as a group after they '
'all return.')
)
self.add_option(
'-v', '--verbose',
default=False,
action='store_true',
help=('Turn on command verbosity, display jid and active job '
'queries')
)
self.add_option(
'-b', '--batch',
'--batch-size',
default='',
dest='batch',
help=('Execute the salt job in batch mode, pass either the number '
'of minions to batch at a time, or the percentage of '
'minions to have running')
)
self.add_option(
'--return',
default='',
metavar='RETURNER',
help=('Set an alternative return method. By default salt will '
'send the return data from the command back to the master, '
'but the return data can be redirected into any number of '
'systems, databases or applications.')
)
self.add_option(
'-Q', '--query',
action='store_true',
help=('This option is deprecated and will be removed in a future '
'release, please use salt-run jobs instead.\n'
'Execute a salt command query, this can be used to find '
'the results of a previous function call: -Q test.echo')
)
def _mixin_after_parsed(self):
if self.options.query:
if len(self.args) < 1:
self.error(
'Please pass in a command to query the old salt calls for.'
)
self.config['cmd'] = self.args[0]
else:
# Catch invalid invocations of salt such as: salt run
if len(self.args) <= 1:
self.print_help()
self.exit(1)
if self.options.list:
self.config['tgt'] = self.args[0].split(',')
else:
self.config['tgt'] = self.args[0]
# Detect compound command and set up the data for it
if ',' in self.args[1]:
self.config['fun'] = self.args[1].split(',')
self.config['arg'] = []
for comp in ' '.join(self.args[2:]).split(','):
self.config['arg'].append(comp.split())
if len(self.config['fun']) != len(self.config['arg']):
self.exit(42, 'Cannot execute compound command without '
'defining all arguments.')
else:
self.config['fun'] = self.args[1]
self.config['arg'] = self.args[2:]
def setup_config(self):
return config.master_config(self.get_config_file_path('master'))

View file

@ -345,7 +345,7 @@ class ShellCase(TestCase):
'''
Execute salt
'''
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
arg_str = '-c {0} {1}'.format(mconf, arg_str)
return self.run_script('salt', arg_str)

View file

@ -7,10 +7,12 @@ import integration
from integration import TestDaemon
class MatchTest(integration.ShellCase):
class MatchTest(integration.ShellCase, integration.ShellCaseCommonTestsMixIn):
'''
Test salt matchers
'''
_call_binary_ = 'salt'
def test_list(self):
'''
test salt -L matcher