mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-06 13:22:48 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user