Split up the base_parser function

The goal of breaking apart the base_parser() function is to get rid of
a bunch of conditionals and parameters in the code and, instead, make
code look like simple composition.

When splitting, a choice had to be made as to whether this would operate
by side effect (modifying a passed in parser) or side effect-free
(returning a new parser everytime).

Making a version that's side-effect-free appears to be fighting with the
optparse API (it wants to work by creating a parser object, configuring
the object, and then parsing the arguments with it) so instead, make it
clear that our helper functions are modifying the passed in parser by
(1) not returning the parser and (2) changing the function names to be
more clear that it is operating by side-effect.

Also move all of the generic optparse code, along with the argument
context classes, into a new subdirectory.
This commit is contained in:
Toshio Kuratomi 2018-12-19 00:28:33 -08:00
parent afdbb0d9d5
commit 7e92ff823e
21 changed files with 545 additions and 486 deletions

View file

@ -0,0 +1,5 @@
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

View file

@ -61,7 +61,16 @@ class _ABCSingleton(Singleton, ABCMeta):
class CLIArgs(ImmutableDict): class CLIArgs(ImmutableDict):
"""Hold a parsed copy of cli arguments""" """
Hold a parsed copy of cli arguments
We have both this non-Singleton version and the Singleton, GlobalCLIArgs, version to leave us
room to implement a Context object in the future. Whereas there should only be one set of args
in a global context, individual Context objects might want to pretend that they have different
command line switches to trigger different behaviour when they run. So if we support Contexts
in the future, they would use CLIArgs instead of GlobalCLIArgs to store their version of command
line flags.
"""
def __init__(self, mapping): def __init__(self, mapping):
toplevel = {} toplevel = {}
for key, value in mapping.items(): for key, value in mapping.items():

View file

@ -0,0 +1,377 @@
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import operator
import optparse
import os
import os.path
import sys
import time
import yaml
import ansible
from ansible import constants as C
from ansible.module_utils.six import string_types
from ansible.release import __version__
from ansible.utils.path import unfrackpath
#
# Special purpose OptionParsers
#
class SortedOptParser(optparse.OptionParser):
"""Optparser which sorts the options by opt before outputting --help"""
def format_help(self, formatter=None, epilog=None):
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None)
# Note: Inherit from SortedOptParser so that we get our format_help method
class InvalidOptsParser(SortedOptParser):
"""Ignore invalid options.
Meant for the special case where we need to take care of help and version but may not know the
full range of options yet.
.. seealso::
See it in use in ansible.cli.CLI.set_action
"""
def __init__(self, parser):
# Since this is special purposed to just handle help and version, we
# take a pre-existing option parser here and set our options from
# that. This allows us to give accurate help based on the given
# option parser.
SortedOptParser.__init__(self, usage=parser.usage,
option_list=parser.option_list,
option_class=parser.option_class,
conflict_handler=parser.conflict_handler,
description=parser.description,
formatter=parser.formatter,
add_help_option=False,
prog=parser.prog,
epilog=parser.epilog)
self.version = parser.version
def _process_long_opt(self, rargs, values):
try:
optparse.OptionParser._process_long_opt(self, rargs, values)
except optparse.BadOptionError:
pass
def _process_short_opts(self, rargs, values):
try:
optparse.OptionParser._process_short_opts(self, rargs, values)
except optparse.BadOptionError:
pass
#
# Callbacks to validate and normalize Options
#
def unfrack_paths(option, opt, value, parser):
"""Turn an Option's value into a list of paths in Ansible locations"""
paths = getattr(parser.values, option.dest)
if paths is None:
paths = []
if isinstance(value, string_types):
paths[:0] = [unfrackpath(x) for x in value.split(os.pathsep) if x]
elif isinstance(value, list):
paths[:0] = [unfrackpath(x) for x in value if x]
else:
pass # FIXME: should we raise options error?
setattr(parser.values, option.dest, paths)
def unfrack_path(option, opt, value, parser):
"""Turn an Option's data into a single path in Ansible locations"""
if value != '-':
setattr(parser.values, option.dest, unfrackpath(value))
else:
setattr(parser.values, option.dest, value)
def _git_repo_info(repo_path):
""" returns a string containing git branch, commit id and commit date """
result = None
if os.path.exists(repo_path):
# Check if the .git is a file. If it is a file, it means that we are in a submodule structure.
if os.path.isfile(repo_path):
try:
gitdir = yaml.safe_load(open(repo_path)).get('gitdir')
# There is a possibility the .git file to have an absolute path.
if os.path.isabs(gitdir):
repo_path = gitdir
else:
repo_path = os.path.join(repo_path[:-4], gitdir)
except (IOError, AttributeError):
return ''
with open(os.path.join(repo_path, "HEAD")) as f:
line = f.readline().rstrip("\n")
if line.startswith("ref:"):
branch_path = os.path.join(repo_path, line[5:])
else:
branch_path = None
if branch_path and os.path.exists(branch_path):
branch = '/'.join(line.split('/')[2:])
with open(branch_path) as f:
commit = f.readline()[:10]
else:
# detached HEAD
commit = line[:10]
branch = 'detached HEAD'
branch_path = os.path.join(repo_path, "HEAD")
date = time.localtime(os.stat(branch_path).st_mtime)
if time.daylight == 0:
offset = time.timezone
else:
offset = time.altzone
result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit, time.strftime("%Y/%m/%d %H:%M:%S", date), int(offset / -36))
else:
result = ''
return result
def _gitinfo():
basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..')
repo_path = os.path.join(basedir, '.git')
result = _git_repo_info(repo_path)
submodules = os.path.join(basedir, '.gitmodules')
if not os.path.exists(submodules):
return result
with open(submodules) as f:
for line in f:
tokens = line.strip().split(' ')
if tokens[0] == 'path':
submodule_path = tokens[2]
submodule_info = _git_repo_info(os.path.join(basedir, submodule_path, '.git'))
if not submodule_info:
submodule_info = ' not found - use git submodule update --init ' + submodule_path
result += "\n {0}: {1}".format(submodule_path, submodule_info)
return result
def version(prog=None):
""" return ansible version """
if prog:
result = " ".join((prog, __version__))
else:
result = __version__
gitinfo = _gitinfo()
if gitinfo:
result = result + " {0}".format(gitinfo)
result += "\n config file = %s" % C.CONFIG_FILE
if C.DEFAULT_MODULE_PATH is None:
cpath = "Default w/o overrides"
else:
cpath = C.DEFAULT_MODULE_PATH
result = result + "\n configured module search path = %s" % cpath
result = result + "\n ansible python module location = %s" % ':'.join(ansible.__path__)
result = result + "\n executable location = %s" % sys.argv[0]
result = result + "\n python version = %s" % ''.join(sys.version.splitlines())
return result
#
# Functions to add pre-canned options to an OptionParser
#
def create_base_parser(usage="", desc=None, epilog=None):
"""
Create an options parser for all ansible scripts
"""
# base opts
parser = SortedOptParser(usage, version=version("%prog"), description=desc, epilog=epilog)
parser.remove_option('--version')
version_help = "show program's version number, config file location, configured module search path," \
" module location, executable location and exit"
parser.add_option('--version', action="version", help=version_help)
parser.add_option('-v', '--verbose', dest='verbosity', default=C.DEFAULT_VERBOSITY, action="count",
help="verbose mode (-vvv for more, -vvvv to enable connection debugging)")
return parser
def add_async_options(parser):
"""Add options for commands which can launch async tasks"""
parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int', dest='poll_interval',
help="set the poll interval if using -B (default=%s)" % C.DEFAULT_POLL_INTERVAL)
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
help='run asynchronously, failing after X seconds (default=N/A)')
def add_basedir_options(parser):
"""Add options for commands which can set a playbook basedir"""
parser.add_option('--playbook-dir', default=None, dest='basedir', action='store',
help="Since this tool does not use playbooks, use this as a subsitute playbook directory."
"This sets the relative path for many features including roles/ group_vars/ etc.")
def add_check_options(parser):
"""Add options for commands which can run with diagnostic information of tasks"""
parser.add_option("-C", "--check", default=False, dest='check', action='store_true',
help="don't make any changes; instead, try to predict some of the changes that may occur")
parser.add_option('--syntax-check', dest='syntax', action='store_true',
help="perform a syntax check on the playbook, but do not execute it")
parser.add_option("-D", "--diff", default=C.DIFF_ALWAYS, dest='diff', action='store_true',
help="when changing (small) files and templates, show the differences in those"
" files; works great with --check")
def add_connect_options(parser):
"""Add options for commands which need to connection to other hosts"""
connect_group = optparse.OptionGroup(parser, "Connection Options", "control as whom and how to connect to hosts")
connect_group.add_option('-k', '--ask-pass', default=C.DEFAULT_ASK_PASS, dest='ask_pass', action='store_true',
help='ask for connection password')
connect_group.add_option('--private-key', '--key-file', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
help='use this file to authenticate the connection', action="callback", callback=unfrack_path, type='string')
connect_group.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
connect_group.add_option('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT,
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
connect_group.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout',
help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
connect_group.add_option('--ssh-common-args', default='', dest='ssh_common_args',
help="specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand)")
connect_group.add_option('--sftp-extra-args', default='', dest='sftp_extra_args',
help="specify extra arguments to pass to sftp only (e.g. -f, -l)")
connect_group.add_option('--scp-extra-args', default='', dest='scp_extra_args',
help="specify extra arguments to pass to scp only (e.g. -l)")
connect_group.add_option('--ssh-extra-args', default='', dest='ssh_extra_args',
help="specify extra arguments to pass to ssh only (e.g. -R)")
parser.add_option_group(connect_group)
def add_fork_options(parser):
"""Add options for commands that can fork worker processes"""
parser.add_option('-f', '--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
help="specify number of parallel processes to use (default=%s)" % C.DEFAULT_FORKS)
def add_inventory_options(parser):
"""Add options for commands that utilize inventory"""
parser.add_option('-i', '--inventory', '--inventory-file', dest='inventory', action="append",
help="specify inventory host path or comma separated host list. --inventory-file is deprecated")
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
help='outputs a list of matching hosts; does not execute anything else')
parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset',
help='further limit selected hosts to an additional pattern')
def add_meta_options(parser):
"""Add options for commands which can launch meta tasks from the command line"""
parser.add_option('--force-handlers', default=C.DEFAULT_FORCE_HANDLERS, dest='force_handlers', action='store_true',
help="run handlers even if a task fails")
parser.add_option('--flush-cache', dest='flush_cache', action='store_true',
help="clear the fact cache for every host in inventory")
def add_module_options(parser):
"""Add options for commands that load modules"""
parser.add_option('-M', '--module-path', dest='module_path', default=None,
help="prepend colon-separated path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH,
action="callback", callback=unfrack_paths, type='str')
def add_output_options(parser):
"""Add options for commands which can change their output"""
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
help='condense output')
parser.add_option('-t', '--tree', dest='tree', default=None,
help='log output to this directory')
def add_runas_options(parser):
"""
Add options for commands which can run tasks as another user
Note that this includes the options from add_runas_prompt_options(). Only one of these
functions should be used.
"""
runas_group = optparse.OptionGroup(parser, "Privilege Escalation Options", "control how and which user you become as on target hosts")
# priv user defaults to root later on to enable detecting when this option was given here
runas_group.add_option("-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo',
help="run operations with sudo (nopasswd) (deprecated, use become)")
runas_group.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
help='desired sudo user (default=root) (deprecated, use become)')
runas_group.add_option('-S', '--su', default=C.DEFAULT_SU, action='store_true',
help='run operations with su (deprecated, use become)')
runas_group.add_option('-R', '--su-user', default=None,
help='run operations with su as this user (default=%s) (deprecated, use become)' % C.DEFAULT_SU_USER)
# consolidated privilege escalation (become)
runas_group.add_option("-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become',
help="run operations with become (does not imply password prompting)")
runas_group.add_option('--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='choice', choices=C.BECOME_METHODS,
help="privilege escalation method to use (default=%s), valid choices: [ %s ]" %
(C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS)))
runas_group.add_option('--become-user', default=None, dest='become_user', type='string',
help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER)
add_runas_prompt_options(parser, runas_group=runas_group)
def add_runas_prompt_options(parser, runas_group=None):
"""
Add options for commands which need to prompt for privilege escalation credentials
Note that add_runas_options() includes these options already. Only one of the two functions
should be used.
"""
if runas_group is None:
runas_group = optparse.OptionGroup(parser, "Privilege Escalation Options",
"control how and which user you become as on target hosts")
runas_group.add_option('--ask-sudo-pass', default=C.DEFAULT_ASK_SUDO_PASS, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password (deprecated, use become)')
runas_group.add_option('--ask-su-pass', default=C.DEFAULT_ASK_SU_PASS, dest='ask_su_pass', action='store_true',
help='ask for su password (deprecated, use become)')
runas_group.add_option('-K', '--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
help='ask for privilege escalation password')
parser.add_option_group(runas_group)
def add_runtask_options(parser):
"""Add options for commands that run a task"""
parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
help="set additional variables as key=value or YAML/JSON, if filename prepend with @", default=[])
def add_subset_options(parser):
"""Add options for commands which can run a subset of tasks"""
parser.add_option('-t', '--tags', dest='tags', default=C.TAGS_RUN, action='append',
help="only run plays and tasks tagged with these values")
parser.add_option('--skip-tags', dest='skip_tags', default=C.TAGS_SKIP, action='append',
help="only run plays and tasks whose tags do not match these values")
def add_vault_options(parser):
"""Add options for loading vault files"""
parser.add_option('--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true',
help='ask for vault password')
parser.add_option('--vault-password-file', default=[], dest='vault_password_files',
help="vault password file", action="callback", callback=unfrack_paths, type='string')
parser.add_option('--vault-id', default=[], dest='vault_ids', action='append', type='string',
help='the vault identity to use')
def add_vault_rekey_options(parser):
"""Add options for commands which can edit/rekey a vault file"""
parser.add_option('--new-vault-password-file', default=None, dest='new_vault_password_file',
help="new vault password file for rekey", action="callback", callback=unfrack_path, type='string')
parser.add_option('--new-vault-id', default=None, dest='new_vault_id', type='string',
help='the new vault identity to use for rekey')

View file

@ -8,20 +8,17 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import getpass import getpass
import operator
import optparse
import os import os
import subprocess import os.path
import re import re
import subprocess
import sys import sys
import time
import yaml
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import ansible
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.errors import AnsibleOptionsError, AnsibleError from ansible.errors import AnsibleOptionsError, AnsibleError
from ansible.inventory.manager import InventoryManager from ansible.inventory.manager import InventoryManager
from ansible.module_utils.six import with_metaclass, string_types from ansible.module_utils.six import with_metaclass, string_types
@ -38,200 +35,6 @@ from ansible.parsing.vault import PromptVaultSecret, get_file_vault_secret
display = Display() display = Display()
class SortedOptParser(optparse.OptionParser):
'''Optparser which sorts the options by opt before outputting --help'''
def format_help(self, formatter=None, epilog=None):
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None)
# Note: Inherit from SortedOptParser so that we get our format_help method
class InvalidOptsParser(SortedOptParser):
'''Ignore invalid options.
Meant for the special case where we need to take care of help and version
but may not know the full range of options yet. (See it in use in set_action)
'''
def __init__(self, parser):
# Since this is special purposed to just handle help and version, we
# take a pre-existing option parser here and set our options from
# that. This allows us to give accurate help based on the given
# option parser.
SortedOptParser.__init__(self, usage=parser.usage,
option_list=parser.option_list,
option_class=parser.option_class,
conflict_handler=parser.conflict_handler,
description=parser.description,
formatter=parser.formatter,
add_help_option=False,
prog=parser.prog,
epilog=parser.epilog)
self.version = parser.version
def _process_long_opt(self, rargs, values):
try:
optparse.OptionParser._process_long_opt(self, rargs, values)
except optparse.BadOptionError:
pass
def _process_short_opts(self, rargs, values):
try:
optparse.OptionParser._process_short_opts(self, rargs, values)
except optparse.BadOptionError:
pass
def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False,
vault_opts=False, module_opts=False, async_opts=False, connect_opts=False,
subset_opts=False, check_opts=False, inventory_opts=False, epilog=None,
fork_opts=False, runas_prompt_opts=False, desc=None, basedir_opts=False,
vault_rekey_opts=False):
"""
Create an options parser for most ansible scripts
"""
# base opts
parser = SortedOptParser(usage, version=CLI.version("%prog"), description=desc, epilog=epilog)
parser.remove_option('--version')
version_help = "show program's version number, config file location, configured module search path," \
" module location, executable location and exit"
parser.add_option('--version', action="version", help=version_help)
parser.add_option('-v', '--verbose', dest='verbosity', default=C.DEFAULT_VERBOSITY, action="count",
help="verbose mode (-vvv for more, -vvvv to enable connection debugging)")
if inventory_opts:
parser.add_option('-i', '--inventory', '--inventory-file', dest='inventory', action="append",
help="specify inventory host path or comma separated host list. --inventory-file is deprecated")
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
help='outputs a list of matching hosts; does not execute anything else')
parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset',
help='further limit selected hosts to an additional pattern')
if module_opts:
parser.add_option('-M', '--module-path', dest='module_path', default=None,
help="prepend colon-separated path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH,
action="callback", callback=CLI.unfrack_paths, type='str')
if runtask_opts:
parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
help="set additional variables as key=value or YAML/JSON, if filename prepend with @", default=[])
if fork_opts:
parser.add_option('-f', '--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
help="specify number of parallel processes to use (default=%s)" % C.DEFAULT_FORKS)
if vault_opts:
parser.add_option('--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true',
help='ask for vault password')
parser.add_option('--vault-password-file', default=[], dest='vault_password_files',
help="vault password file", action="callback", callback=CLI.unfrack_paths, type='string')
parser.add_option('--vault-id', default=[], dest='vault_ids', action='append', type='string',
help='the vault identity to use')
if vault_rekey_opts:
parser.add_option('--new-vault-password-file', default=None, dest='new_vault_password_file',
help="new vault password file for rekey", action="callback", callback=CLI.unfrack_path, type='string')
parser.add_option('--new-vault-id', default=None, dest='new_vault_id', type='string',
help='the new vault identity to use for rekey')
if subset_opts:
parser.add_option('-t', '--tags', dest='tags', default=C.TAGS_RUN, action='append',
help="only run plays and tasks tagged with these values")
parser.add_option('--skip-tags', dest='skip_tags', default=C.TAGS_SKIP, action='append',
help="only run plays and tasks whose tags do not match these values")
if output_opts:
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
help='condense output')
parser.add_option('-t', '--tree', dest='tree', default=None,
help='log output to this directory')
if connect_opts:
connect_group = optparse.OptionGroup(parser, "Connection Options", "control as whom and how to connect to hosts")
connect_group.add_option('-k', '--ask-pass', default=C.DEFAULT_ASK_PASS, dest='ask_pass', action='store_true',
help='ask for connection password')
connect_group.add_option('--private-key', '--key-file', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
help='use this file to authenticate the connection', action="callback", callback=CLI.unfrack_path, type='string')
connect_group.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
connect_group.add_option('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT,
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
connect_group.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout',
help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
connect_group.add_option('--ssh-common-args', default='', dest='ssh_common_args',
help="specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand)")
connect_group.add_option('--sftp-extra-args', default='', dest='sftp_extra_args',
help="specify extra arguments to pass to sftp only (e.g. -f, -l)")
connect_group.add_option('--scp-extra-args', default='', dest='scp_extra_args',
help="specify extra arguments to pass to scp only (e.g. -l)")
connect_group.add_option('--ssh-extra-args', default='', dest='ssh_extra_args',
help="specify extra arguments to pass to ssh only (e.g. -R)")
parser.add_option_group(connect_group)
runas_group = None
rg = optparse.OptionGroup(parser, "Privilege Escalation Options", "control how and which user you become as on target hosts")
if runas_opts:
runas_group = rg
# priv user defaults to root later on to enable detecting when this option was given here
runas_group.add_option("-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo',
help="run operations with sudo (nopasswd) (deprecated, use become)")
runas_group.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
help='desired sudo user (default=root) (deprecated, use become)')
runas_group.add_option('-S', '--su', default=C.DEFAULT_SU, action='store_true',
help='run operations with su (deprecated, use become)')
runas_group.add_option('-R', '--su-user', default=None,
help='run operations with su as this user (default=%s) (deprecated, use become)' % C.DEFAULT_SU_USER)
# consolidated privilege escalation (become)
runas_group.add_option("-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become',
help="run operations with become (does not imply password prompting)")
runas_group.add_option('--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='choice', choices=C.BECOME_METHODS,
help="privilege escalation method to use (default=%s), valid choices: [ %s ]" %
(C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS)))
runas_group.add_option('--become-user', default=None, dest='become_user', type='string',
help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER)
if runas_opts or runas_prompt_opts:
if not runas_group:
runas_group = rg
runas_group.add_option('--ask-sudo-pass', default=C.DEFAULT_ASK_SUDO_PASS, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password (deprecated, use become)')
runas_group.add_option('--ask-su-pass', default=C.DEFAULT_ASK_SU_PASS, dest='ask_su_pass', action='store_true',
help='ask for su password (deprecated, use become)')
runas_group.add_option('-K', '--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
help='ask for privilege escalation password')
if runas_group:
parser.add_option_group(runas_group)
if async_opts:
parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int', dest='poll_interval',
help="set the poll interval if using -B (default=%s)" % C.DEFAULT_POLL_INTERVAL)
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
help='run asynchronously, failing after X seconds (default=N/A)')
if check_opts:
parser.add_option("-C", "--check", default=False, dest='check', action='store_true',
help="don't make any changes; instead, try to predict some of the changes that may occur")
parser.add_option('--syntax-check', dest='syntax', action='store_true',
help="perform a syntax check on the playbook, but do not execute it")
parser.add_option("-D", "--diff", default=C.DIFF_ALWAYS, dest='diff', action='store_true',
help="when changing (small) files and templates, show the differences in those files; works great with --check")
if meta_opts:
parser.add_option('--force-handlers', default=C.DEFAULT_FORCE_HANDLERS, dest='force_handlers', action='store_true',
help="run handlers even if a task fails")
parser.add_option('--flush-cache', dest='flush_cache', action='store_true',
help="clear the fact cache for every host in inventory")
if basedir_opts:
parser.add_option('--playbook-dir', default=None, dest='basedir', action='store',
help="Since this tool does not use playbooks, use this as a subsitute playbook directory."
"This sets the relative path for many features including roles/ group_vars/ etc.")
return parser
class CLI(with_metaclass(ABCMeta, object)): class CLI(with_metaclass(ABCMeta, object)):
''' code behind bin/ansible* programs ''' ''' code behind bin/ansible* programs '''
@ -277,7 +80,7 @@ class CLI(with_metaclass(ABCMeta, object)):
# the standard OptionParser throws an error for unknown options and # the standard OptionParser throws an error for unknown options and
# without knowing action, we only know of a subset of the options # without knowing action, we only know of a subset of the options
# that could be legal for this command # that could be legal for this command
tmp_parser = InvalidOptsParser(self.parser) tmp_parser = opt_help.InvalidOptsParser(self.parser)
tmp_options, tmp_args = tmp_parser.parse_args(self.args) tmp_options, tmp_args = tmp_parser.parse_args(self.args)
if not(hasattr(tmp_options, 'help') and tmp_options.help) or (hasattr(tmp_options, 'version') and tmp_options.version): if not(hasattr(tmp_options, 'help') and tmp_options.help) or (hasattr(tmp_options, 'version') and tmp_options.version):
raise AnsibleOptionsError("Missing required action") raise AnsibleOptionsError("Missing required action")
@ -533,34 +336,8 @@ class CLI(with_metaclass(ABCMeta, object)):
return op return op
@staticmethod
def unfrack_paths(option, opt, value, parser):
paths = getattr(parser.values, option.dest)
if paths is None:
paths = []
if isinstance(value, string_types):
paths[:0] = [unfrackpath(x) for x in value.split(os.pathsep) if x]
elif isinstance(value, list):
paths[:0] = [unfrackpath(x) for x in value if x]
else:
pass # FIXME: should we raise options error?
setattr(parser.values, option.dest, paths)
@staticmethod
def unfrack_path(option, opt, value, parser):
if value != '-':
setattr(parser.values, option.dest, unfrackpath(value))
else:
setattr(parser.values, option.dest, value)
@abstractmethod @abstractmethod
def init_parser(self, usage="", output_opts=False, runas_opts=False, meta_opts=False, def init_parser(self, usage="", desc=None, epilog=None):
runtask_opts=False, vault_opts=False, module_opts=False, async_opts=False,
connect_opts=False, subset_opts=False, check_opts=False, inventory_opts=False,
epilog=None, fork_opts=False, runas_prompt_opts=False, desc=None,
basedir_opts=False, vault_rekey_opts=False):
""" """
Create an options parser for most ansible scripts Create an options parser for most ansible scripts
@ -570,19 +347,11 @@ class CLI(with_metaclass(ABCMeta, object)):
An implementation will look something like this:: An implementation will look something like this::
def init_parser(self): def init_parser(self):
self.parser = super(MyCLI, self).init__parser(usage="My Ansible CLI", inventory_opts=True) super(MyCLI, self).init__parser(usage="My Ansible CLI", inventory_opts=True)
ansible.arguments.optparse_helpers.add_runas_options(self.parser)
self.parser.add_option('--my-option', dest='my_option', action='store') self.parser.add_option('--my-option', dest='my_option', action='store')
return self.parser
""" """
self.parser = base_parser(usage=usage, output_opts=output_opts, runas_opts=runas_opts, self.parser = opt_help.create_base_parser(usage=usage, desc=desc, epilog=epilog)
meta_opts=meta_opts, runtask_opts=runtask_opts,
vault_opts=vault_opts, module_opts=module_opts,
async_opts=async_opts, connect_opts=connect_opts,
subset_opts=subset_opts, check_opts=check_opts,
inventory_opts=inventory_opts, epilog=epilog, fork_opts=fork_opts,
runas_prompt_opts=runas_prompt_opts, desc=desc,
basedir_opts=basedir_opts, vault_rekey_opts=vault_rekey_opts)
return self.parser
@abstractmethod @abstractmethod
def post_process_args(self, options, args): def post_process_args(self, options, args):
@ -654,30 +423,12 @@ class CLI(with_metaclass(ABCMeta, object)):
options.args = args options.args = args
context._init_global_context(options) context._init_global_context(options)
@staticmethod
def version(prog):
''' return ansible version '''
result = "{0} {1}".format(prog, __version__)
gitinfo = CLI._gitinfo()
if gitinfo:
result = result + " {0}".format(gitinfo)
result += "\n config file = %s" % C.CONFIG_FILE
if C.DEFAULT_MODULE_PATH is None:
cpath = "Default w/o overrides"
else:
cpath = C.DEFAULT_MODULE_PATH
result = result + "\n configured module search path = %s" % cpath
result = result + "\n ansible python module location = %s" % ':'.join(ansible.__path__)
result = result + "\n executable location = %s" % sys.argv[0]
result = result + "\n python version = %s" % ''.join(sys.version.splitlines())
return result
@staticmethod @staticmethod
def version_info(gitinfo=False): def version_info(gitinfo=False):
''' return full ansible version info ''' ''' return full ansible version info '''
if gitinfo: if gitinfo:
# expensive call, user with care # expensive call, user with care
ansible_version_string = CLI.version('') ansible_version_string = opt_help.version()
else: else:
ansible_version_string = __version__ ansible_version_string = __version__
ansible_version = ansible_version_string.split()[0] ansible_version = ansible_version_string.split()[0]
@ -698,70 +449,6 @@ class CLI(with_metaclass(ABCMeta, object)):
'minor': ansible_versions[1], 'minor': ansible_versions[1],
'revision': ansible_versions[2]} 'revision': ansible_versions[2]}
@staticmethod
def _git_repo_info(repo_path):
''' returns a string containing git branch, commit id and commit date '''
result = None
if os.path.exists(repo_path):
# Check if the .git is a file. If it is a file, it means that we are in a submodule structure.
if os.path.isfile(repo_path):
try:
gitdir = yaml.safe_load(open(repo_path)).get('gitdir')
# There is a possibility the .git file to have an absolute path.
if os.path.isabs(gitdir):
repo_path = gitdir
else:
repo_path = os.path.join(repo_path[:-4], gitdir)
except (IOError, AttributeError):
return ''
f = open(os.path.join(repo_path, "HEAD"))
line = f.readline().rstrip("\n")
if line.startswith("ref:"):
branch_path = os.path.join(repo_path, line[5:])
else:
branch_path = None
f.close()
if branch_path and os.path.exists(branch_path):
branch = '/'.join(line.split('/')[2:])
f = open(branch_path)
commit = f.readline()[:10]
f.close()
else:
# detached HEAD
commit = line[:10]
branch = 'detached HEAD'
branch_path = os.path.join(repo_path, "HEAD")
date = time.localtime(os.stat(branch_path).st_mtime)
if time.daylight == 0:
offset = time.timezone
else:
offset = time.altzone
result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit, time.strftime("%Y/%m/%d %H:%M:%S", date), int(offset / -36))
else:
result = ''
return result
@staticmethod
def _gitinfo():
basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..')
repo_path = os.path.join(basedir, '.git')
result = CLI._git_repo_info(repo_path)
submodules = os.path.join(basedir, '.gitmodules')
if not os.path.exists(submodules):
return result
f = open(submodules)
for line in f:
tokens = line.strip().split(' ')
if tokens[0] == 'path':
submodule_path = tokens[2]
submodule_info = CLI._git_repo_info(os.path.join(basedir, submodule_path, '.git'))
if not submodule_info:
submodule_info = ' not found - use git submodule update --init ' + submodule_path
result += "\n {0}: {1}".format(submodule_path, submodule_info)
f.close()
return result
def pager(self, text): def pager(self, text):
''' find reasonable way to display text ''' ''' find reasonable way to display text '''
# this is a much simpler form of what is in pydoc.py # this is a much simpler form of what is in pydoc.py

View file

@ -7,6 +7,7 @@ __metaclass__ = type
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.executor.task_queue_manager import TaskQueueManager from ansible.executor.task_queue_manager import TaskQueueManager
@ -27,22 +28,23 @@ class AdHocCLI(CLI):
def init_parser(self): def init_parser(self):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
self.parser = super(AdHocCLI, self).init_parser( super(AdHocCLI, self).init_parser(usage='%prog <host-pattern> [options]',
usage='%prog <host-pattern> [options]', desc="Define and run a single task 'playbook' against"
runas_opts=True, " a set of hosts",
inventory_opts=True, epilog="Some modules do not make sense in Ad-Hoc (include,"
async_opts=True, " meta, etc)")
output_opts=True,
connect_opts=True, opt_help.add_runas_options(self.parser)
check_opts=True, opt_help.add_inventory_options(self.parser)
runtask_opts=True, opt_help.add_async_options(self.parser)
vault_opts=True, opt_help.add_output_options(self.parser)
fork_opts=True, opt_help.add_connect_options(self.parser)
module_opts=True, opt_help.add_check_options(self.parser)
basedir_opts=True, opt_help.add_runtask_options(self.parser)
desc="Define and run a single task 'playbook' against a set of hosts", opt_help.add_vault_options(self.parser)
epilog="Some modules do not make sense in Ad-Hoc (include, meta, etc)", opt_help.add_fork_options(self.parser)
) opt_help.add_module_options(self.parser)
opt_help.add_basedir_options(self.parser)
# options unique to ansible ad-hoc # options unique to ansible ad-hoc
self.parser.add_option('-a', '--args', dest='module_args', self.parser.add_option('-a', '--args', dest='module_args',
@ -50,7 +52,6 @@ class AdHocCLI(CLI):
self.parser.add_option('-m', '--module-name', dest='module_name', self.parser.add_option('-m', '--module-name', dest='module_name',
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME, help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
default=C.DEFAULT_MODULE_NAME) default=C.DEFAULT_MODULE_NAME)
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
'''Post process and validate options for bin/ansible ''' '''Post process and validate options for bin/ansible '''

View file

@ -36,29 +36,31 @@ class ConfigCLI(CLI):
def init_parser(self): def init_parser(self):
self.parser = super(ConfigCLI, self).init_parser( super(ConfigCLI, self).init_parser(
usage="usage: %%prog [%s] [--help] [options] [ansible.cfg]" % "|".join(sorted(self.VALID_ACTIONS)), usage="usage: %%prog [%s] [--help] [options] [ansible.cfg]" % "|".join(sorted(self.VALID_ACTIONS)),
epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]), epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]),
desc="View, edit, and manage ansible configuration.", desc="View, edit, and manage ansible configuration.",
) )
self.parser.add_option('-c', '--config', dest='config_file', help="path to configuration file, defaults to first file found in precedence.") self.parser.add_option('-c', '--config', dest='config_file',
help="path to configuration file, defaults to first file found in precedence.")
self.set_action() self.set_action()
# options specific to self.actions # options specific to self.actions
if self.action == "list": if self.action == "list":
self.parser.set_usage("usage: %prog list [options] ") self.parser.set_usage("usage: %prog list [options] ")
if self.action == "dump":
elif self.action == "dump":
self.parser.add_option('--only-changed', dest='only_changed', action='store_true', self.parser.add_option('--only-changed', dest='only_changed', action='store_true',
help="Only show configurations that have changed from the default") help="Only show configurations that have changed from the default")
elif self.action == "update": elif self.action == "update":
self.parser.add_option('-s', '--setting', dest='setting', help="config setting, the section defaults to 'defaults'") self.parser.add_option('-s', '--setting', dest='setting', help="config setting, the section defaults to 'defaults'")
self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] -s '[section.]setting=value'") self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] -s '[section.]setting=value'")
elif self.action == "search": elif self.action == "search":
self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] <search term>") self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] <search term>")
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
super(ConfigCLI, self).post_process_args(options, args) super(ConfigCLI, self).post_process_args(options, args)
display.verbosity = options.verbosity display.verbosity = options.verbosity

View file

@ -26,6 +26,7 @@ import sys
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.executor.task_queue_manager import TaskQueueManager from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.module_utils._text import to_native, to_text from ansible.module_utils._text import to_native, to_text
@ -80,24 +81,22 @@ class ConsoleCLI(CLI, cmd.Cmd):
def init_parser(self): def init_parser(self):
super(ConsoleCLI, self).init_parser( super(ConsoleCLI, self).init_parser(
usage='%prog [<host-pattern>] [options]', usage='%prog [<host-pattern>] [options]',
runas_opts=True,
inventory_opts=True,
connect_opts=True,
check_opts=True,
vault_opts=True,
fork_opts=True,
module_opts=True,
basedir_opts=True,
desc="REPL console for executing Ansible tasks.", desc="REPL console for executing Ansible tasks.",
epilog="This is not a live session/connection, each task executes in the background and returns it's results." epilog="This is not a live session/connection, each task executes in the background and returns it's results."
) )
opt_help.add_runas_options(self.parser)
opt_help.add_inventory_options(self.parser)
opt_help.add_connect_options(self.parser)
opt_help.add_check_options(self.parser)
opt_help.add_vault_options(self.parser)
opt_help.add_fork_options(self.parser)
opt_help.add_module_options(self.parser)
opt_help.add_basedir_options(self.parser)
# options unique to shell # options unique to shell
self.parser.add_option('--step', dest='step', action='store_true', self.parser.add_option('--step', dest='step', action='store_true',
help="one-step-at-a-time: confirm each task before running") help="one-step-at-a-time: confirm each task before running")
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
options, args = super(ConsoleCLI, self).post_process_args(options, args) options, args = super(ConsoleCLI, self).post_process_args(options, args)
display.verbosity = options.verbosity display.verbosity = options.verbosity

View file

@ -16,6 +16,7 @@ import ansible.plugins.loader as plugin_loader
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
@ -47,12 +48,12 @@ class DocCLI(CLI):
def init_parser(self): def init_parser(self):
self.parser = super(DocCLI, self).init_parser( super(DocCLI, self).init_parser(
usage='usage: %prog [-l|-F|-s] [options] [-t <plugin type> ] [plugin]', usage='usage: %prog [-l|-F|-s] [options] [-t <plugin type> ] [plugin]',
module_opts=True,
desc="plugin documentation tool", desc="plugin documentation tool",
epilog="See man pages for Ansible CLI options or website for tutorials https://docs.ansible.com" epilog="See man pages for Ansible CLI options or website for tutorials https://docs.ansible.com"
) )
opt_help.add_module_options(self.parser)
self.parser.add_option("-F", "--list_files", action="store_true", default=False, dest="list_files", self.parser.add_option("-F", "--list_files", action="store_true", default=False, dest="list_files",
help='Show plugin names and their source files without summaries (implies --list)') help='Show plugin names and their source files without summaries (implies --list)')
@ -68,7 +69,6 @@ class DocCLI(CLI):
help='Choose which plugin type (defaults to "module"). ' help='Choose which plugin type (defaults to "module"). '
'Available plugin types are : {0}'.format(C.DOCUMENTABLE_PLUGINS), 'Available plugin types are : {0}'.format(C.DOCUMENTABLE_PLUGINS),
choices=C.DOCUMENTABLE_PLUGINS) choices=C.DOCUMENTABLE_PLUGINS)
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
if [options.all_plugins, options.json_dump, options.list_dir, options.list_files, options.show_snippet].count(True) > 1: if [options.all_plugins, options.json_dump, options.list_dir, options.list_files, options.show_snippet].count(True) > 1:

View file

@ -14,8 +14,9 @@ import yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from ansible import context
import ansible.constants as C import ansible.constants as C
from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.galaxy import Galaxy from ansible.galaxy import Galaxy
@ -109,7 +110,7 @@ class GalaxyCLI(CLI):
if self.action not in ("delete", "import", "init", "login", "setup"): if self.action not in ("delete", "import", "init", "login", "setup"):
# NOTE: while the option type=str, the default is a list, and the # NOTE: while the option type=str, the default is a list, and the
# callback will set the value to a list. # callback will set the value to a list.
self.parser.add_option('-p', '--roles-path', dest='roles_path', action="callback", callback=CLI.unfrack_paths, default=C.DEFAULT_ROLES_PATH, self.parser.add_option('-p', '--roles-path', dest='roles_path', action="callback", callback=opt_help.unfrack_paths, default=C.DEFAULT_ROLES_PATH,
help='The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg' help='The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg'
' file (/etc/ansible/roles if not configured)', type='str') ' file (/etc/ansible/roles if not configured)', type='str')
if self.action in ("init", "install"): if self.action in ("init", "install"):
@ -118,7 +119,7 @@ class GalaxyCLI(CLI):
def init_parser(self): def init_parser(self):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
self.parser = super(GalaxyCLI, self).init_parser( super(GalaxyCLI, self).init_parser(
usage="usage: %%prog [%s] [--help] [options] ..." % "|".join(sorted(self.VALID_ACTIONS)), usage="usage: %%prog [%s] [--help] [options] ..." % "|".join(sorted(self.VALID_ACTIONS)),
epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]), epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]),
desc="Perform various Role related operations.", desc="Perform various Role related operations.",
@ -130,8 +131,6 @@ class GalaxyCLI(CLI):
help='Ignore SSL certificate validation errors.') help='Ignore SSL certificate validation errors.')
self.set_action() self.set_action()
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
options, args = super(GalaxyCLI, self).post_process_args(options, args) options, args = super(GalaxyCLI, self).post_process_args(options, args)
display.verbosity = options.verbosity display.verbosity = options.verbosity

View file

@ -10,11 +10,11 @@ from operator import attrgetter
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.inventory.host import Host from ansible.inventory.host import Host
from ansible.plugins.loader import vars_loader from ansible.plugins.loader import vars_loader
from ansible.parsing.dataloader import DataLoader
from ansible.utils.vars import combine_vars from ansible.utils.vars import combine_vars
from ansible.utils.display import Display from ansible.utils.display import Display
@ -57,13 +57,13 @@ class InventoryCLI(CLI):
def init_parser(self): def init_parser(self):
self.parser = super(InventoryCLI, self).init_parser( super(InventoryCLI, self).init_parser(
usage='usage: %prog [options] [host|group]', usage='usage: %prog [options] [host|group]',
epilog='Show Ansible inventory information, by default it uses the inventory script JSON format', epilog='Show Ansible inventory information, by default it uses the inventory script JSON format')
inventory_opts=True,
vault_opts=True, opt_help.add_inventory_options(self.parser)
basedir_opts=True, opt_help.add_vault_options(self.parser)
) opt_help.add_basedir_options(self.parser)
# remove unused default options # remove unused default options
self.parser.remove_option('--limit') self.parser.remove_option('--limit')
@ -92,8 +92,6 @@ class InventoryCLI(CLI):
# self.parser.add_option("--ignore-vars-plugins", action="store_true", default=False, dest='ignore_vars_plugins', # self.parser.add_option("--ignore-vars-plugins", action="store_true", default=False, dest='ignore_vars_plugins',
# help="When doing an --list, skip vars data from vars plugins, by default, this would include group_vars/ and host_vars/") # help="When doing an --list, skip vars data from vars plugins, by default, this would include group_vars/ and host_vars/")
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
display.verbosity = options.verbosity display.verbosity = options.verbosity
self.validate_conflicts(options, vault_opts=True) self.validate_conflicts(options, vault_opts=True)

View file

@ -1,19 +1,6 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# # Copyright: (c) 2018, Ansible Project
# This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
@ -22,6 +9,7 @@ import os
import stat import stat
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.executor.playbook_executor import PlaybookExecutor from ansible.executor.playbook_executor import PlaybookExecutor
@ -41,18 +29,18 @@ class PlaybookCLI(CLI):
# create parser for CLI options # create parser for CLI options
super(PlaybookCLI, self).init_parser( super(PlaybookCLI, self).init_parser(
usage="%prog [options] playbook.yml [playbook2 ...]", usage="%prog [options] playbook.yml [playbook2 ...]",
connect_opts=True, desc="Runs Ansible playbooks, executing the defined tasks on the targeted hosts.")
meta_opts=True,
runas_opts=True, opt_help.add_connect_options(self.parser)
subset_opts=True, opt_help.add_meta_options(self.parser)
check_opts=True, opt_help.add_runas_options(self.parser)
inventory_opts=True, opt_help.add_subset_options(self.parser)
runtask_opts=True, opt_help.add_check_options(self.parser)
vault_opts=True, opt_help.add_inventory_options(self.parser)
fork_opts=True, opt_help.add_runtask_options(self.parser)
module_opts=True, opt_help.add_vault_options(self.parser)
desc="Runs Ansible playbooks, executing the defined tasks on the targeted hosts.", opt_help.add_fork_options(self.parser)
) opt_help.add_module_options(self.parser)
# ansible playbook specific opts # ansible playbook specific opts
self.parser.add_option('--list-tasks', dest='listtasks', action='store_true', self.parser.add_option('--list-tasks', dest='listtasks', action='store_true',
@ -64,8 +52,6 @@ class PlaybookCLI(CLI):
self.parser.add_option('--start-at-task', dest='start_at_task', self.parser.add_option('--start-at-task', dest='start_at_task',
help="start the playbook at the task matching this name") help="start the playbook at the task matching this name")
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
options, args = super(PlaybookCLI, self).post_process_args(options, args) options, args = super(PlaybookCLI, self).post_process_args(options, args)
@ -111,7 +97,7 @@ class PlaybookCLI(CLI):
# limit if only implicit localhost was in inventory to start with. # limit if only implicit localhost was in inventory to start with.
# #
# Fix this when we rewrite inventory by making localhost a real host (and thus show up in list_hosts()) # Fix this when we rewrite inventory by making localhost a real host (and thus show up in list_hosts())
hosts = super(PlaybookCLI, self).get_host_list(inventory, context.CLIARGS['subset']) hosts = self.get_host_list(inventory, context.CLIARGS['subset'])
# flush fact cache if requested # flush fact cache if requested
if context.CLIARGS['flush_cache']: if context.CLIARGS['flush_cache']:

View file

@ -16,6 +16,7 @@ import time
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleOptionsError from ansible.errors import AnsibleOptionsError
from ansible.module_utils._text import to_native, to_text from ansible.module_utils._text import to_native, to_text
@ -68,18 +69,18 @@ class PullCLI(CLI):
def init_parser(self): def init_parser(self):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
self.parser = super(PullCLI, self).init_parser( super(PullCLI, self).init_parser(
usage='%prog -U <repository> [options] [<playbook.yml>]', usage='%prog -U <repository> [options] [<playbook.yml>]',
connect_opts=True, desc="pulls playbooks from a VCS repo and executes them for the local host")
vault_opts=True,
runtask_opts=True, # Do not add check_options as there's a conflict with --checkout/-C
subset_opts=True, opt_help.add_connect_options(self.parser)
check_opts=False, # prevents conflict of --checkout/-C and --check/-C opt_help.add_vault_options(self.parser)
inventory_opts=True, opt_help.add_runtask_options(self.parser)
module_opts=True, opt_help.add_subset_options(self.parser)
runas_prompt_opts=True, opt_help.add_inventory_options(self.parser)
desc="pulls playbooks from a VCS repo and executes them for the local host", opt_help.add_module_options(self.parser)
) opt_help.add_runas_prompt_options(self.parser)
# options unique to pull # options unique to pull
self.parser.add_option('--purge', default=False, action='store_true', help='purge checkout after playbook run') self.parser.add_option('--purge', default=False, action='store_true', help='purge checkout after playbook run')
@ -114,8 +115,6 @@ class PullCLI(CLI):
self.parser.add_option("--diff", default=C.DIFF_ALWAYS, dest='diff', action='store_true', self.parser.add_option("--diff", default=C.DIFF_ALWAYS, dest='diff', action='store_true',
help="when changing (small) files and templates, show the differences in those files; works great with --check") help="when changing (small) files and templates, show the differences in those files; works great with --check")
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
options, args = super(PullCLI, self).post_process_args(options, args) options, args = super(PullCLI, self).post_process_args(options, args)

View file

@ -10,6 +10,7 @@ import sys
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleOptionsError from ansible.errors import AnsibleOptionsError
from ansible.module_utils._text import to_text, to_bytes from ansible.module_utils._text import to_text, to_bytes
@ -62,7 +63,7 @@ class VaultCLI(CLI):
if self.action in self.can_output: if self.action in self.can_output:
self.parser.add_option('--output', default=None, dest='output_file', self.parser.add_option('--output', default=None, dest='output_file',
help='output file name for encrypt or decrypt; use - for stdout', help='output file name for encrypt or decrypt; use - for stdout',
action="callback", callback=self.unfrack_path, type='string') action="callback", callback=opt_help.unfrack_path, type='string')
# options specific to self.actions # options specific to self.actions
if self.action == "create": if self.action == "create":
@ -97,19 +98,16 @@ class VaultCLI(CLI):
help='the vault id used to encrypt (required if more than vault-id is provided)') help='the vault id used to encrypt (required if more than vault-id is provided)')
def init_parser(self): def init_parser(self):
super(VaultCLI, self).init_parser(
self.parser = super(VaultCLI, self).init_parser(
vault_opts=True,
vault_rekey_opts=True,
usage="usage: %%prog [%s] [options] [vaultfile.yml]" % "|".join(sorted(self.VALID_ACTIONS)), usage="usage: %%prog [%s] [options] [vaultfile.yml]" % "|".join(sorted(self.VALID_ACTIONS)),
desc="encryption/decryption utility for Ansible data files", desc="encryption/decryption utility for Ansible data files",
epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
) )
opt_help.add_vault_options(self.parser)
opt_help.add_vault_rekey_options(self.parser)
self.set_action() self.set_action()
return self.parser
def post_process_args(self, options, args): def post_process_args(self, options, args):
options, args = super(VaultCLI, self).post_process_args(options, args) options, args = super(VaultCLI, self).post_process_args(options, args)
self.validate_conflicts(options, vault_opts=True, vault_rekey_opts=True) self.validate_conflicts(options, vault_opts=True, vault_rekey_opts=True)

View file

@ -15,39 +15,15 @@ running the ansible command line tools.
These APIs are still in flux so do not use them unless you are willing to update them with every Ansible release These APIs are still in flux so do not use them unless you are willing to update them with every Ansible release
""" """
from ansible import arguments from ansible.arguments.context_objects import CLIArgs, GlobalCLIArgs
# Note: this is not the singleton version. That is only created once the program has actually # Note: this is not the singleton version. The Singleton is only created once the program has
# parsed the args # actually parsed the args
CLIARGS = arguments.CLIArgs({}) CLIARGS = CLIArgs({})
class _Context:
"""
Not yet ready for Prime Time
Eventually this may allow for code which needs to run under different contexts (for instance, as
if they were run with different command line args or from different current working directories)
to exist in the same process. But at the moment, we don't need that so this code has not been
tested for suitability.
"""
def __init__(self):
global CLIARGS
self._CLIARGS = arguments.CLIArgs(CLIARGS)
@property
def CLIARGS(self):
return self._CLIARGS
@CLIARGS.setter
def CLIARGS_set(self, new_cli_args):
if not isinstance(new_cli_args, arguments.CLIArgs):
raise TypeError('CLIARGS must be of type (ansible.arguments.CLIArgs)')
self._CLIARGS = new_cli_args
def _init_global_context(cli_args): def _init_global_context(cli_args):
"""Initialize the global context objects""" """Initialize the global context objects"""
global CLIARGS global CLIARGS
CLIARGS = arguments.GlobalCLIArgs.from_options(cli_args) CLIARGS = GlobalCLIArgs.from_options(cli_args)

View file

@ -15,24 +15,25 @@ import optparse
import pytest import pytest
from ansible import arguments from ansible.arguments import context_objects as co
from ansible.module_utils.common.collections import ImmutableDict
MAKE_IMMUTABLE_DATA = ((u'くらとみ', u'くらとみ'), MAKE_IMMUTABLE_DATA = ((u'くらとみ', u'くらとみ'),
(42, 42), (42, 42),
({u'café': u'くらとみ'}, arguments.ImmutableDict({u'café': u'くらとみ'})), ({u'café': u'くらとみ'}, ImmutableDict({u'café': u'くらとみ'})),
([1, u'café', u'くらとみ'], (1, u'café', u'くらとみ')), ([1, u'café', u'くらとみ'], (1, u'café', u'くらとみ')),
(set((1, u'café', u'くらとみ')), frozenset((1, u'café', u'くらとみ'))), (set((1, u'café', u'くらとみ')), frozenset((1, u'café', u'くらとみ'))),
({u'café': [1, set(u'ñ')]}, ({u'café': [1, set(u'ñ')]},
arguments.ImmutableDict({u'café': (1, frozenset(u'ñ'))})), ImmutableDict({u'café': (1, frozenset(u'ñ'))})),
([set((1, 2)), {u'くらとみ': 3}], ([set((1, 2)), {u'くらとみ': 3}],
(frozenset((1, 2)), arguments.ImmutableDict({u'くらとみ': 3}))), (frozenset((1, 2)), ImmutableDict({u'くらとみ': 3}))),
) )
@pytest.mark.parametrize('data, expected', MAKE_IMMUTABLE_DATA) @pytest.mark.parametrize('data, expected', MAKE_IMMUTABLE_DATA)
def test_make_immutable(data, expected): def test_make_immutable(data, expected):
assert arguments._make_immutable(data) == expected assert co._make_immutable(data) == expected
def test_cliargs_from_dict(): def test_cliargs_from_dict():
@ -43,7 +44,7 @@ def test_cliargs_from_dict():
('check_mode', True), ('check_mode', True),
('start_at_task', u'Start with くらとみ'))) ('start_at_task', u'Start with くらとみ')))
assert frozenset(arguments.CLIArgs(old_dict).items()) == expected assert frozenset(co.CLIArgs(old_dict).items()) == expected
def test_cliargs(): def test_cliargs():
@ -58,7 +59,7 @@ def test_cliargs():
('check_mode', True), ('check_mode', True),
('start_at_task', u'Start with くらとみ'))) ('start_at_task', u'Start with くらとみ')))
assert frozenset(arguments.CLIArgs.from_options(options).items()) == expected assert frozenset(co.CLIArgs.from_options(options).items()) == expected
@pytest.mark.skipIf(argparse is None) @pytest.mark.skipIf(argparse is None)
@ -73,7 +74,7 @@ def test_cliargs_argparse():
expected = frozenset((('accumulate', sum), ('integers', (1, 2)))) expected = frozenset((('accumulate', sum), ('integers', (1, 2))))
assert frozenset(arguments.CLIArgs.from_options(args).items()) == expected assert frozenset(co.CLIArgs.from_options(args).items()) == expected
# Can get rid of this test when we port ansible.cli from optparse to argparse # Can get rid of this test when we port ansible.cli from optparse to argparse
@ -87,4 +88,4 @@ def test_cliargs_optparse():
expected = frozenset((('accumulate', sum), ('integers', (u'1', u'2')))) expected = frozenset((('accumulate', sum), ('integers', (u'1', u'2'))))
assert frozenset(arguments.CLIArgs.from_options(opts).items()) == expected assert frozenset(co.CLIArgs.from_options(opts).items()) == expected

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
import pytest
from ansible.arguments import optparse_helpers as opt_help
class TestOptparseHelpersVersion:
def test_version(self):
ver = opt_help.version('ansible-cli-test')
assert 'ansible-cli-test' in ver
assert 'python version' in ver

View file

@ -31,11 +31,6 @@ from ansible import cli
class TestCliVersion(unittest.TestCase): class TestCliVersion(unittest.TestCase):
def test_version(self):
ver = cli.CLI.version('ansible-cli-test')
self.assertIn('ansible-cli-test', ver)
self.assertIn('python version', ver)
def test_version_info(self): def test_version_info(self):
version_info = cli.CLI.version_info() version_info = cli.CLI.version_info()
self.assertEqual(version_info['string'], __version__) self.assertEqual(version_info['string'], __version__)

View file

@ -26,8 +26,9 @@ import tarfile
import tempfile import tempfile
import yaml import yaml
from ansible import arguments
from ansible import context from ansible import context
from ansible.arguments import context_objects as co
from ansible.arguments import optparse_helpers as opt_help
from ansible.cli.galaxy import GalaxyCLI from ansible.cli.galaxy import GalaxyCLI
from units.compat import unittest from units.compat import unittest
from units.compat.mock import call, patch from units.compat.mock import call, patch
@ -98,12 +99,12 @@ class TestGalaxy(unittest.TestCase):
def setUp(self): def setUp(self):
# Reset the stored command line args # Reset the stored command line args
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
self.default_args = ['ansible-galaxy'] self.default_args = ['ansible-galaxy']
def tearDown(self): def tearDown(self):
# Reset the stored command line args # Reset the stored command line args
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
def test_init(self): def test_init(self):
galaxy_cli = GalaxyCLI(args=self.default_args) galaxy_cli = GalaxyCLI(args=self.default_args)
@ -147,7 +148,7 @@ class TestGalaxy(unittest.TestCase):
# removing role # removing role
# Have to reset the arguments in the context object manually since we're doing the # Have to reset the arguments in the context object manually since we're doing the
# equivalent of running the command line program twice # equivalent of running the command line program twice
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
gc = GalaxyCLI(args=["ansible-galaxy", "remove", role_file, self.role_name]) gc = GalaxyCLI(args=["ansible-galaxy", "remove", role_file, self.role_name])
gc.run() gc.run()
@ -172,11 +173,11 @@ class TestGalaxy(unittest.TestCase):
self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by ")) self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by "))
def run_parse_common(self, galaxycli_obj, action): def run_parse_common(self, galaxycli_obj, action):
with patch.object(ansible.cli.SortedOptParser, "set_usage") as mocked_usage: with patch.object(opt_help.SortedOptParser, "set_usage") as mocked_usage:
galaxycli_obj.parse() galaxycli_obj.parse()
# checking that the common results of parse() for all possible actions have been created/called # checking that the common results of parse() for all possible actions have been created/called
self.assertIsInstance(galaxycli_obj.parser, ansible.cli.SortedOptParser) self.assertIsInstance(galaxycli_obj.parser, opt_help.SortedOptParser)
formatted_call = { formatted_call = {
'import': 'usage: %prog import [options] github_user github_repo', 'import': 'usage: %prog import [options] github_user github_repo',
'delete': 'usage: %prog delete [options] github_user github_repo', 'delete': 'usage: %prog delete [options] github_user github_repo',

View file

@ -22,7 +22,7 @@ __metaclass__ = type
from units.compat import unittest from units.compat import unittest
from units.compat.mock import MagicMock from units.compat.mock import MagicMock
from ansible import arguments from ansible.arguments import context_objects as co
from ansible.executor.playbook_executor import PlaybookExecutor from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.playbook import Playbook from ansible.playbook import Playbook
from ansible.template import Templar from ansible.template import Templar
@ -34,11 +34,11 @@ class TestPlaybookExecutor(unittest.TestCase):
def setUp(self): def setUp(self):
# Reset command line args for every test # Reset command line args for every test
arguments.CLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
def tearDown(self): def tearDown(self):
# And cleanup after ourselves too # And cleanup after ourselves too
arguments.CLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
def test_get_serialized_batches(self): def test_get_serialized_batches(self):
fake_loader = DictDataLoader({ fake_loader = DictDataLoader({

View file

@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function)
from units.compat import unittest from units.compat import unittest
from units.compat.mock import MagicMock from units.compat.mock import MagicMock
from ansible import arguments
from ansible import context from ansible import context
from ansible.arguments import context_objects as co
from ansible.executor.task_queue_manager import TaskQueueManager from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.playbook import Playbook from ansible.playbook import Playbook
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
@ -38,7 +38,7 @@ class TestTaskQueueManagerCallbacks(unittest.TestCase):
passwords = [] passwords = []
# Reset the stored command line args # Reset the stored command line args
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
self._tqm = TaskQueueManager(inventory, variable_manager, loader, passwords) self._tqm = TaskQueueManager(inventory, variable_manager, loader, passwords)
self._playbook = Playbook(loader) self._playbook = Playbook(loader)
@ -51,7 +51,7 @@ class TestTaskQueueManagerCallbacks(unittest.TestCase):
def tearDown(self): def tearDown(self):
# Reset the stored command line args # Reset the stored command line args
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
def test_task_queue_manager_callbacks_v2_playbook_on_start(self): def test_task_queue_manager_callbacks_v2_playbook_on_start(self):
""" """

View file

@ -11,10 +11,10 @@ import os
import pytest import pytest
from ansible import arguments
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
from ansible import cli from ansible.arguments import context_objects as co
from ansible.arguments import optparse_helpers as opt_help
from units.compat import unittest from units.compat import unittest
from ansible.errors import AnsibleError, AnsibleParserError from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils.six.moves import shlex_quote
@ -25,19 +25,26 @@ from units.mock.loader import DictDataLoader
@pytest.fixture @pytest.fixture
def parser(): def parser():
parser = cli.base_parser(runas_opts=True, meta_opts=True, parser = opt_help.create_base_parser()
runtask_opts=True, vault_opts=True,
async_opts=True, connect_opts=True, opt_help.add_runas_options(parser)
subset_opts=True, check_opts=True, opt_help.add_meta_options(parser)
inventory_opts=True,) opt_help.add_runtask_options(parser)
opt_help.add_vault_options(parser)
opt_help.add_async_options(parser)
opt_help.add_connect_options(parser)
opt_help.add_subset_options(parser)
opt_help.add_check_options(parser)
opt_help.add_inventory_options(parser)
return parser return parser
@pytest.fixture @pytest.fixture
def reset_cli_args(): def reset_cli_args():
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
yield yield
arguments.GlobalCLIArgs._Singleton__instance = None co.GlobalCLIArgs._Singleton__instance = None
def test_play_context(mocker, parser, reset_cli_args): def test_play_context(mocker, parser, reset_cli_args):