diff --git a/lib/ansible/arguments/__init__.py b/lib/ansible/arguments/__init__.py new file mode 100644 index 00000000000..7398e33fa30 --- /dev/null +++ b/lib/ansible/arguments/__init__.py @@ -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 diff --git a/lib/ansible/arguments.py b/lib/ansible/arguments/context_objects.py similarity index 82% rename from lib/ansible/arguments.py rename to lib/ansible/arguments/context_objects.py index 9ea2597aefa..b7ed0889ff4 100644 --- a/lib/ansible/arguments.py +++ b/lib/ansible/arguments/context_objects.py @@ -61,7 +61,16 @@ class _ABCSingleton(Singleton, ABCMeta): 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): toplevel = {} for key, value in mapping.items(): diff --git a/lib/ansible/arguments/optparse_helpers.py b/lib/ansible/arguments/optparse_helpers.py new file mode 100644 index 00000000000..e933af2133a --- /dev/null +++ b/lib/ansible/arguments/optparse_helpers.py @@ -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') diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 9a591a36170..b707ac15878 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -8,20 +8,17 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import getpass -import operator -import optparse import os -import subprocess +import os.path import re +import subprocess import sys -import time -import yaml from abc import ABCMeta, abstractmethod -import ansible from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.errors import AnsibleOptionsError, AnsibleError from ansible.inventory.manager import InventoryManager 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() -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)): ''' 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 # without knowing action, we only know of a subset of the options # 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) if not(hasattr(tmp_options, 'help') and tmp_options.help) or (hasattr(tmp_options, 'version') and tmp_options.version): raise AnsibleOptionsError("Missing required action") @@ -533,34 +336,8 @@ class CLI(with_metaclass(ABCMeta, object)): 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 - def init_parser(self, 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): + def init_parser(self, usage="", desc=None, epilog=None): """ 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:: 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') - return self.parser """ - self.parser = base_parser(usage=usage, output_opts=output_opts, runas_opts=runas_opts, - 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 + self.parser = opt_help.create_base_parser(usage=usage, desc=desc, epilog=epilog) @abstractmethod def post_process_args(self, options, args): @@ -654,30 +423,12 @@ class CLI(with_metaclass(ABCMeta, object)): options.args = args 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 def version_info(gitinfo=False): ''' return full ansible version info ''' if gitinfo: # expensive call, user with care - ansible_version_string = CLI.version('') + ansible_version_string = opt_help.version() else: ansible_version_string = __version__ ansible_version = ansible_version_string.split()[0] @@ -698,70 +449,6 @@ class CLI(with_metaclass(ABCMeta, object)): 'minor': ansible_versions[1], '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): ''' find reasonable way to display text ''' # this is a much simpler form of what is in pydoc.py diff --git a/lib/ansible/cli/adhoc.py b/lib/ansible/cli/adhoc.py index 4c7f5c199dd..cfd6c76477d 100644 --- a/lib/ansible/cli/adhoc.py +++ b/lib/ansible/cli/adhoc.py @@ -7,6 +7,7 @@ __metaclass__ = type from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.executor.task_queue_manager import TaskQueueManager @@ -27,22 +28,23 @@ class AdHocCLI(CLI): def init_parser(self): ''' create an options parser for bin/ansible ''' - self.parser = super(AdHocCLI, self).init_parser( - usage='%prog [options]', - runas_opts=True, - inventory_opts=True, - async_opts=True, - output_opts=True, - connect_opts=True, - check_opts=True, - runtask_opts=True, - vault_opts=True, - fork_opts=True, - module_opts=True, - basedir_opts=True, - desc="Define and run a single task 'playbook' against a set of hosts", - epilog="Some modules do not make sense in Ad-Hoc (include, meta, etc)", - ) + super(AdHocCLI, self).init_parser(usage='%prog [options]', + desc="Define and run a single task 'playbook' against" + " a set of hosts", + epilog="Some modules do not make sense in Ad-Hoc (include," + " meta, etc)") + + opt_help.add_runas_options(self.parser) + opt_help.add_inventory_options(self.parser) + opt_help.add_async_options(self.parser) + opt_help.add_output_options(self.parser) + opt_help.add_connect_options(self.parser) + opt_help.add_check_options(self.parser) + opt_help.add_runtask_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 ansible ad-hoc 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', help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME, default=C.DEFAULT_MODULE_NAME) - return self.parser def post_process_args(self, options, args): '''Post process and validate options for bin/ansible ''' diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py index f040d599f81..7b9fee201d0 100644 --- a/lib/ansible/cli/config.py +++ b/lib/ansible/cli/config.py @@ -36,29 +36,31 @@ class ConfigCLI(CLI): 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)), epilog="\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]), 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() # options specific to self.actions if self.action == "list": 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', help="Only show configurations that have changed from the default") + elif self.action == "update": 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'") + elif self.action == "search": self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] ") - return self.parser - def post_process_args(self, options, args): super(ConfigCLI, self).post_process_args(options, args) display.verbosity = options.verbosity diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index 8175c29e6cd..03edda1fa78 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -26,6 +26,7 @@ import sys from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.executor.task_queue_manager import TaskQueueManager from ansible.module_utils._text import to_native, to_text @@ -80,24 +81,22 @@ class ConsoleCLI(CLI, cmd.Cmd): def init_parser(self): super(ConsoleCLI, self).init_parser( usage='%prog [] [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.", 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 self.parser.add_option('--step', dest='step', action='store_true', help="one-step-at-a-time: confirm each task before running") - return self.parser - def post_process_args(self, options, args): options, args = super(ConsoleCLI, self).post_process_args(options, args) display.verbosity = options.verbosity diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index e2afedae3b8..2299751390e 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -16,6 +16,7 @@ import ansible.plugins.loader as plugin_loader from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.module_utils._text import to_native @@ -47,12 +48,12 @@ class DocCLI(CLI): 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]', - module_opts=True, desc="plugin documentation tool", 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", 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"). ' 'Available plugin types are : {0}'.format(C.DOCUMENTABLE_PLUGINS), choices=C.DOCUMENTABLE_PLUGINS) - return self.parser 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: diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index 6b68e799c17..c7edd053488 100644 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -14,8 +14,9 @@ import yaml from jinja2 import Environment, FileSystemLoader -from ansible import context 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.errors import AnsibleError, AnsibleOptionsError from ansible.galaxy import Galaxy @@ -109,7 +110,7 @@ class GalaxyCLI(CLI): if self.action not in ("delete", "import", "init", "login", "setup"): # NOTE: while the option type=str, the default is a list, and the # 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' ' file (/etc/ansible/roles if not configured)', type='str') if self.action in ("init", "install"): @@ -118,7 +119,7 @@ class GalaxyCLI(CLI): def init_parser(self): ''' 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)), epilog="\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]), desc="Perform various Role related operations.", @@ -130,8 +131,6 @@ class GalaxyCLI(CLI): help='Ignore SSL certificate validation errors.') self.set_action() - return self.parser - def post_process_args(self, options, args): options, args = super(GalaxyCLI, self).post_process_args(options, args) display.verbosity = options.verbosity diff --git a/lib/ansible/cli/inventory.py b/lib/ansible/cli/inventory.py index 93f8663c969..2aff7ebeea0 100644 --- a/lib/ansible/cli/inventory.py +++ b/lib/ansible/cli/inventory.py @@ -10,11 +10,11 @@ from operator import attrgetter from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.inventory.host import Host from ansible.plugins.loader import vars_loader -from ansible.parsing.dataloader import DataLoader from ansible.utils.vars import combine_vars from ansible.utils.display import Display @@ -57,13 +57,13 @@ class InventoryCLI(CLI): def init_parser(self): - self.parser = super(InventoryCLI, self).init_parser( + super(InventoryCLI, self).init_parser( usage='usage: %prog [options] [host|group]', - epilog='Show Ansible inventory information, by default it uses the inventory script JSON format', - inventory_opts=True, - vault_opts=True, - basedir_opts=True, - ) + epilog='Show Ansible inventory information, by default it uses the inventory script JSON format') + + opt_help.add_inventory_options(self.parser) + opt_help.add_vault_options(self.parser) + opt_help.add_basedir_options(self.parser) # remove unused default options 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', # 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): display.verbosity = options.verbosity self.validate_conflicts(options, vault_opts=True) diff --git a/lib/ansible/cli/playbook.py b/lib/ansible/cli/playbook.py index da074ef9090..c81688c23c4 100644 --- a/lib/ansible/cli/playbook.py +++ b/lib/ansible/cli/playbook.py @@ -1,19 +1,6 @@ # (c) 2012, Michael DeHaan -# -# This file is part of Ansible -# -# 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 . +# 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 @@ -22,6 +9,7 @@ import os import stat from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.executor.playbook_executor import PlaybookExecutor @@ -41,18 +29,18 @@ class PlaybookCLI(CLI): # create parser for CLI options super(PlaybookCLI, self).init_parser( usage="%prog [options] playbook.yml [playbook2 ...]", - connect_opts=True, - meta_opts=True, - runas_opts=True, - subset_opts=True, - check_opts=True, - inventory_opts=True, - runtask_opts=True, - vault_opts=True, - fork_opts=True, - module_opts=True, - desc="Runs Ansible playbooks, executing the defined tasks on the targeted hosts.", - ) + desc="Runs Ansible playbooks, executing the defined tasks on the targeted hosts.") + + opt_help.add_connect_options(self.parser) + opt_help.add_meta_options(self.parser) + opt_help.add_runas_options(self.parser) + opt_help.add_subset_options(self.parser) + opt_help.add_check_options(self.parser) + opt_help.add_inventory_options(self.parser) + opt_help.add_runtask_options(self.parser) + opt_help.add_vault_options(self.parser) + opt_help.add_fork_options(self.parser) + opt_help.add_module_options(self.parser) # ansible playbook specific opts 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', help="start the playbook at the task matching this name") - return self.parser - def post_process_args(self, 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. # # 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 if context.CLIARGS['flush_cache']: diff --git a/lib/ansible/cli/pull.py b/lib/ansible/cli/pull.py index b6f8b61d859..57d3c530c62 100644 --- a/lib/ansible/cli/pull.py +++ b/lib/ansible/cli/pull.py @@ -16,6 +16,7 @@ import time from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleOptionsError from ansible.module_utils._text import to_native, to_text @@ -68,18 +69,18 @@ class PullCLI(CLI): def init_parser(self): ''' create an options parser for bin/ansible ''' - self.parser = super(PullCLI, self).init_parser( + super(PullCLI, self).init_parser( usage='%prog -U [options] []', - connect_opts=True, - vault_opts=True, - runtask_opts=True, - subset_opts=True, - check_opts=False, # prevents conflict of --checkout/-C and --check/-C - inventory_opts=True, - module_opts=True, - runas_prompt_opts=True, - desc="pulls playbooks from a VCS repo and executes them for the local host", - ) + desc="pulls playbooks from a VCS repo and executes them for the local host") + + # Do not add check_options as there's a conflict with --checkout/-C + opt_help.add_connect_options(self.parser) + opt_help.add_vault_options(self.parser) + opt_help.add_runtask_options(self.parser) + opt_help.add_subset_options(self.parser) + opt_help.add_inventory_options(self.parser) + opt_help.add_module_options(self.parser) + opt_help.add_runas_prompt_options(self.parser) # options unique to pull 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', 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): options, args = super(PullCLI, self).post_process_args(options, args) diff --git a/lib/ansible/cli/vault.py b/lib/ansible/cli/vault.py index 8e561e284f2..89044809dfc 100644 --- a/lib/ansible/cli/vault.py +++ b/lib/ansible/cli/vault.py @@ -10,6 +10,7 @@ import sys from ansible import constants as C from ansible import context +from ansible.arguments import optparse_helpers as opt_help from ansible.cli import CLI from ansible.errors import AnsibleOptionsError from ansible.module_utils._text import to_text, to_bytes @@ -62,7 +63,7 @@ class VaultCLI(CLI): if self.action in self.can_output: self.parser.add_option('--output', default=None, dest='output_file', 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 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)') def init_parser(self): - - self.parser = super(VaultCLI, self).init_parser( - vault_opts=True, - vault_rekey_opts=True, + super(VaultCLI, self).init_parser( usage="usage: %%prog [%s] [options] [vaultfile.yml]" % "|".join(sorted(self.VALID_ACTIONS)), desc="encryption/decryption utility for Ansible data files", epilog="\nSee '%s --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() - return self.parser - def post_process_args(self, options, args): options, args = super(VaultCLI, self).post_process_args(options, args) self.validate_conflicts(options, vault_opts=True, vault_rekey_opts=True) diff --git a/lib/ansible/context.py b/lib/ansible/context.py index eafcbee064b..3b2c56a182f 100644 --- a/lib/ansible/context.py +++ b/lib/ansible/context.py @@ -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 """ -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 -# parsed the args -CLIARGS = arguments.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 +# Note: this is not the singleton version. The Singleton is only created once the program has +# actually parsed the args +CLIARGS = CLIArgs({}) def _init_global_context(cli_args): """Initialize the global context objects""" global CLIARGS - CLIARGS = arguments.GlobalCLIArgs.from_options(cli_args) + CLIARGS = GlobalCLIArgs.from_options(cli_args) diff --git a/test/units/test_arguments.py b/test/units/arguments/test_context_objects.py similarity index 80% rename from test/units/test_arguments.py rename to test/units/arguments/test_context_objects.py index 0bf293ca8c6..d843da2c988 100644 --- a/test/units/test_arguments.py +++ b/test/units/arguments/test_context_objects.py @@ -15,24 +15,25 @@ import optparse 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'くらとみ'), (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'くらとみ')), (set((1, u'café', u'くらとみ')), frozenset((1, u'café', u'くらとみ'))), ({u'café': [1, set(u'ñ')]}, - arguments.ImmutableDict({u'café': (1, frozenset(u'ñ'))})), + ImmutableDict({u'café': (1, frozenset(u'ñ'))})), ([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) def test_make_immutable(data, expected): - assert arguments._make_immutable(data) == expected + assert co._make_immutable(data) == expected def test_cliargs_from_dict(): @@ -43,7 +44,7 @@ def test_cliargs_from_dict(): ('check_mode', True), ('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(): @@ -58,7 +59,7 @@ def test_cliargs(): ('check_mode', True), ('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) @@ -73,7 +74,7 @@ def test_cliargs_argparse(): 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 @@ -87,4 +88,4 @@ def test_cliargs_optparse(): 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 diff --git a/test/units/arguments/test_optparse_helpers.py b/test/units/arguments/test_optparse_helpers.py new file mode 100644 index 00000000000..e4fdda33f84 --- /dev/null +++ b/test/units/arguments/test_optparse_helpers.py @@ -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 diff --git a/test/units/cli/test_cli.py b/test/units/cli/test_cli.py index dffb4e692fd..e9c18d1bbea 100644 --- a/test/units/cli/test_cli.py +++ b/test/units/cli/test_cli.py @@ -31,11 +31,6 @@ from ansible import cli 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): version_info = cli.CLI.version_info() self.assertEqual(version_info['string'], __version__) diff --git a/test/units/cli/test_galaxy.py b/test/units/cli/test_galaxy.py index 02acf350609..56d3c0c22df 100644 --- a/test/units/cli/test_galaxy.py +++ b/test/units/cli/test_galaxy.py @@ -26,8 +26,9 @@ import tarfile import tempfile import yaml -from ansible import arguments 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 units.compat import unittest from units.compat.mock import call, patch @@ -98,12 +99,12 @@ class TestGalaxy(unittest.TestCase): def setUp(self): # Reset the stored command line args - arguments.GlobalCLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None self.default_args = ['ansible-galaxy'] def tearDown(self): # Reset the stored command line args - arguments.GlobalCLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None def test_init(self): galaxy_cli = GalaxyCLI(args=self.default_args) @@ -147,7 +148,7 @@ class TestGalaxy(unittest.TestCase): # removing role # Have to reset the arguments in the context object manually since we're doing the # 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.run() @@ -172,11 +173,11 @@ class TestGalaxy(unittest.TestCase): self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by ")) 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() # 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 = { 'import': 'usage: %prog import [options] github_user github_repo', 'delete': 'usage: %prog delete [options] github_user github_repo', diff --git a/test/units/executor/test_playbook_executor.py b/test/units/executor/test_playbook_executor.py index a963693d157..bd2c678fb5f 100644 --- a/test/units/executor/test_playbook_executor.py +++ b/test/units/executor/test_playbook_executor.py @@ -22,7 +22,7 @@ __metaclass__ = type from units.compat import unittest 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.playbook import Playbook from ansible.template import Templar @@ -34,11 +34,11 @@ class TestPlaybookExecutor(unittest.TestCase): def setUp(self): # Reset command line args for every test - arguments.CLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None def tearDown(self): # And cleanup after ourselves too - arguments.CLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None def test_get_serialized_batches(self): fake_loader = DictDataLoader({ diff --git a/test/units/executor/test_task_queue_manager_callbacks.py b/test/units/executor/test_task_queue_manager_callbacks.py index 8e0a0414f69..044fe159108 100644 --- a/test/units/executor/test_task_queue_manager_callbacks.py +++ b/test/units/executor/test_task_queue_manager_callbacks.py @@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function) from units.compat import unittest from units.compat.mock import MagicMock -from ansible import arguments from ansible import context +from ansible.arguments import context_objects as co from ansible.executor.task_queue_manager import TaskQueueManager from ansible.playbook import Playbook from ansible.plugins.callback import CallbackBase @@ -38,7 +38,7 @@ class TestTaskQueueManagerCallbacks(unittest.TestCase): passwords = [] # 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._playbook = Playbook(loader) @@ -51,7 +51,7 @@ class TestTaskQueueManagerCallbacks(unittest.TestCase): def tearDown(self): # 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): """ diff --git a/test/units/playbook/test_play_context.py b/test/units/playbook/test_play_context.py index 2c8be52d6ac..c7e51cd8a41 100644 --- a/test/units/playbook/test_play_context.py +++ b/test/units/playbook/test_play_context.py @@ -11,10 +11,10 @@ import os import pytest -from ansible import arguments from ansible import constants as C 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 ansible.errors import AnsibleError, AnsibleParserError from ansible.module_utils.six.moves import shlex_quote @@ -25,19 +25,26 @@ from units.mock.loader import DictDataLoader @pytest.fixture def parser(): - parser = cli.base_parser(runas_opts=True, meta_opts=True, - runtask_opts=True, vault_opts=True, - async_opts=True, connect_opts=True, - subset_opts=True, check_opts=True, - inventory_opts=True,) + parser = opt_help.create_base_parser() + + opt_help.add_runas_options(parser) + opt_help.add_meta_options(parser) + 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 @pytest.fixture def reset_cli_args(): - arguments.GlobalCLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None yield - arguments.GlobalCLIArgs._Singleton__instance = None + co.GlobalCLIArgs._Singleton__instance = None def test_play_context(mocker, parser, reset_cli_args):