From 9de6fea2fab5f9cc576fffa9c86f583122b389a9 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 30 Apr 2015 21:22:23 -0400 Subject: [PATCH] one cli to bind them all --- v2/ansible/cli/__init__.py | 71 +++++++++++++++++--------------- v2/ansible/cli/adhoc.py | 11 +++-- v2/ansible/cli/doc.py | 83 ++++++++++++++++++++++++++++++++++++++ v2/ansible/cli/galaxy.py | 16 +++----- v2/ansible/cli/playbook.py | 7 ++-- v2/ansible/cli/pull.py | 69 +++++++++++++++++++++++++++++++ v2/ansible/cli/vault.py | 14 ++++--- v2/bin/ansible | 79 ++++++++++++++++++++++++++++++++++++ v2/bin/ansible-doc | 1 + v2/bin/ansible-galaxy | 1 + v2/bin/ansible-playbook | 1 + v2/bin/ansible-pull | 1 + v2/bin/ansible-vault | 1 + 13 files changed, 298 insertions(+), 57 deletions(-) create mode 100644 v2/ansible/cli/doc.py create mode 100644 v2/ansible/cli/pull.py create mode 100755 v2/bin/ansible create mode 120000 v2/bin/ansible-doc create mode 120000 v2/bin/ansible-galaxy create mode 120000 v2/bin/ansible-playbook create mode 120000 v2/bin/ansible-pull create mode 120000 v2/bin/ansible-vault diff --git a/v2/ansible/cli/__init__.py b/v2/ansible/cli/__init__.py index e1ea576301..115a2176f5 100644 --- a/v2/ansible/cli/__init__.py +++ b/v2/ansible/cli/__init__.py @@ -34,11 +34,12 @@ from ansible.utils.unicode import to_bytes class SortedOptParser(optparse.OptionParser): '''Optparser which sorts the options by opt before outputting --help''' - def format_help(self, formatter=None): + #FIXME: epilog parsing: OptionParser.format_epilog = lambda self, formatter: self.epilog + + 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) -#TODO: move many cli only functions in this file into the CLI class class CLI(object): ''' code behind bin/ansible* programs ''' @@ -71,8 +72,7 @@ class CLI(object): break if not self.action: - self.parser.print_help() - raise AnsibleError("Missing required action") + raise AnsibleOptionsError("Missing required action") def execute(self): """ @@ -184,36 +184,37 @@ class CLI(object): " are exclusive of each other") @staticmethod - def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, - async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False): + def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False, vault_opts=False, + async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False, epilog=None): ''' create an options parser for most ansible scripts ''' - parser = SortedOptParser(usage, version=CLI.version("%prog")) + #FIXME: implemente epilog parsing + #OptionParser.format_epilog = lambda self, formatter: self.epilog - parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user', - help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER) + # base opts + parser = SortedOptParser(usage, version=CLI.version("%prog")) parser.add_option('-v','--verbose', dest='verbosity', default=0, action="count", help="verbose mode (-vvv for more, -vvvv to enable connection debugging)") - 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) - parser.add_option('-i', '--inventory-file', dest='inventory', - help="specify inventory host file (default=%s)" % C.DEFAULT_HOST_LIST, - default=C.DEFAULT_HOST_LIST) - parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true', - help='ask for connection password') - parser.add_option('--private-key', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file', - help='use this file to authenticate the connection') - parser.add_option('--ask-vault-pass', default=False, dest='ask_vault_pass', action='store_true', - help='ask for vault password') - parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE, - dest='vault_password_file', help="vault password file") - 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('-M', '--module-path', dest='module_path', - help="specify path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH, - default=None) - parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append", - help="set additional variables as key=value or YAML/JSON", default=[]) + + if runtask_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) + parser.add_option('-i', '--inventory-file', dest='inventory', + help="specify inventory host file (default=%s)" % C.DEFAULT_HOST_LIST, + default=C.DEFAULT_HOST_LIST) + 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('-M', '--module-path', dest='module_path', + help="specify path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH, default=None) + parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append", + help="set additional variables as key=value or YAML/JSON", default=[]) + + if vault_opts: + parser.add_option('--ask-vault-pass', default=False, dest='ask_vault_pass', action='store_true', + help='ask for vault password') + parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE, + dest='vault_password_file', help="vault password file") + if subset_opts: parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset', @@ -256,6 +257,12 @@ class CLI(object): if connect_opts: + parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true', + help='ask for connection password') + parser.add_option('--private-key', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file', + help='use this file to authenticate the connection') + parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user', + help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER) parser.add_option('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT, help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT) parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout', @@ -292,7 +299,7 @@ class CLI(object): def version(prog): ''' return ansible version ''' result = "{0} {1}".format(prog, __version__) - gitinfo = _gitinfo() + gitinfo = CLI._gitinfo() if gitinfo: result = result + " {0}".format(gitinfo) result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH @@ -369,7 +376,7 @@ class CLI(object): def _gitinfo(): basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..') repo_path = os.path.join(basedir, '.git') - result = _git_repo_info(repo_path) + result = CLI._git_repo_info(repo_path) submodules = os.path.join(basedir, '.gitmodules') if not os.path.exists(submodules): return result @@ -378,7 +385,7 @@ class CLI(object): tokens = line.strip().split(' ') if tokens[0] == 'path': submodule_path = tokens[2] - submodule_info =_git_repo_info(os.path.join(basedir, submodule_path, '.git')) + 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) diff --git a/v2/ansible/cli/adhoc.py b/v2/ansible/cli/adhoc.py index 5b34acf13e..16c2dc9e42 100644 --- a/v2/ansible/cli/adhoc.py +++ b/v2/ansible/cli/adhoc.py @@ -16,17 +16,14 @@ # along with Ansible. If not, see . ######################################################## -import os -import sys - from ansible import constants as C -from ansible.errors import * +from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.executor.task_queue_manager import TaskQueueManager from ansible.inventory import Inventory from ansible.parsing import DataLoader from ansible.parsing.splitter import parse_kv from ansible.playbook.play import Play -from ansible.utils.cli import CLI +from ansible.cli import CLI from ansible.utils.display import Display from ansible.utils.vault import read_vault_file from ansible.vars import VariableManager @@ -46,6 +43,8 @@ class AdHocCLI(CLI): output_opts=True, connect_opts=True, check_opts=True, + runtask_opts=True, + vault_opts=True, ) # options unique to ansible ad-hoc @@ -101,7 +100,7 @@ class AdHocCLI(CLI): if self.options.listhosts: for host in hosts: - self.display.display(' %s' % host.name) + self.display.display(' %s' % host) return 0 if self.options.module_name in C.MODULE_REQUIRE_ARGS and not self.options.module_args: diff --git a/v2/ansible/cli/doc.py b/v2/ansible/cli/doc.py new file mode 100644 index 0000000000..ec09cb158d --- /dev/null +++ b/v2/ansible/cli/doc.py @@ -0,0 +1,83 @@ +# (c) 2014, James Tanner +# +# 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 . +# +# ansible-vault is a script that encrypts/decrypts YAML files. See +# http://docs.ansible.com/playbooks_vault.html for more details. + +import os +import sys +import traceback + +from ansible import constants as C +from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.cli import CLI +#from ansible.utils import module_docs + +class DocCLI(CLI): + """ Vault command line class """ + + BLACKLIST_EXTS = ('.pyc', '.swp', '.bak', '~', '.rpm') + IGNORE_FILES = [ "COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION"] + + _ITALIC = re.compile(r"I\(([^)]+)\)") + _BOLD = re.compile(r"B\(([^)]+)\)") + _MODULE = re.compile(r"M\(([^)]+)\)") + _URL = re.compile(r"U\(([^)]+)\)") + _CONST = re.compile(r"C\(([^)]+)\)") + + PAGER = 'less' + LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars) + # -S (chop long lines) -X (disable termcap init and de-init) + + + def parse(self): + + self.parser = optparse.OptionParser( + version=version("%prog"), + usage='usage: %prog [options] [module...]', + description='Show Ansible module documentation', + ) + + self.parser.add_option("-M", "--module-path", action="store", dest="module_path", default=C.DEFAULT_MODULE_PATH, + help="Ansible modules/ directory") + self.parser.add_option("-l", "--list", action="store_true", default=False, dest='list_dir', + help='List available modules') + self.parser.add_option("-s", "--snippet", action="store_true", default=False, dest='show_snippet', + help='Show playbook snippet for specified module(s)') + self.parser.add_option('-v', action='version', help='Show version number and exit') + + + self.options, self.args = self.parser.parse_args() + self.display.verbosity = self.options.verbosity + + + def run(self): + + if options.module_path is not None: + for i in options.module_path.split(os.pathsep): + utils.plugins.module_finder.add_directory(i) + + if options.list_dir: + # list modules + paths = utils.plugins.module_finder._get_paths() + module_list = [] + for path in paths: + find_modules(path, module_list) + + pager(get_module_list_text(module_list)) + + if len(args) == 0: + raise AnsibleOptionsError("Incorrect options passed") + diff --git a/v2/ansible/cli/galaxy.py b/v2/ansible/cli/galaxy.py index 76633162ed..abe85e0af8 100644 --- a/v2/ansible/cli/galaxy.py +++ b/v2/ansible/cli/galaxy.py @@ -40,13 +40,13 @@ from optparse import OptionParser import ansible.constants as C import ansible.utils import ansible.galaxy +from ansible.cli import CLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.galaxy import Galaxy from ansible.galaxy.api import GalaxyAPI from ansible.galaxy.role import GalaxyRole from ansible.playbook.role.requirement import RoleRequirement from ansible.utils.display import Display -from ansible.utils.cli import CLI class GalaxyCLI(CLI): @@ -62,18 +62,14 @@ class GalaxyCLI(CLI): def parse(self): ''' create an options parser for bin/ansible ''' - usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS) - epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) - OptionParser.format_epilog = lambda self, formatter: self.epilog - parser = OptionParser(usage=usage, epilog=epilog) + self.parser = CLI.base_parser( + usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS), + epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) + ) + - self.parser = parser self.set_action() - # verbose - self.parser.add_option('-v','--verbose', dest='verbosity', default=0, action="count", - help="verbose mode (-vvv for more, -vvvv to enable connection debugging)") - # options specific to actions if self.action == "info": self.parser.set_usage("usage: %prog info [options] role_name[,version]") diff --git a/v2/ansible/cli/playbook.py b/v2/ansible/cli/playbook.py index e7666682e3..c2b881d2b6 100644 --- a/v2/ansible/cli/playbook.py +++ b/v2/ansible/cli/playbook.py @@ -23,6 +23,7 @@ import stat import sys from ansible import constants as C +from ansible.cli import CLI from ansible.errors import AnsibleError from ansible.executor.playbook_executor import PlaybookExecutor from ansible.inventory import Inventory @@ -30,7 +31,6 @@ from ansible.parsing import DataLoader from ansible.parsing.splitter import parse_kv from ansible.playbook import Playbook from ansible.playbook.task import Task -from ansible.utils.cli import CLI from ansible.utils.display import Display from ansible.utils.unicode import to_unicode from ansible.utils.vars import combine_vars @@ -53,6 +53,8 @@ class PlaybookCLI(CLI): subset_opts=True, check_opts=True, diff_opts=True, + runtask_opts=True, + vault_opts=True, ) # ansible playbook specific opts @@ -68,8 +70,7 @@ class PlaybookCLI(CLI): self.options, self.args = parser.parse_args() if len(self.args) == 0: - parser.print_help(file=sys.stderr) - raise AnsibleError("You must specify a playbook file to run") + raise AnsibleOptionsError("You must specify a playbook file to run") self.parser = parser diff --git a/v2/ansible/cli/pull.py b/v2/ansible/cli/pull.py new file mode 100644 index 0000000000..65741e9544 --- /dev/null +++ b/v2/ansible/cli/pull.py @@ -0,0 +1,69 @@ +# (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 . + +######################################################## +import os +import sys + +from ansible import constants as C +from ansible.errors import * +from ansible.cli import CLI +from ansible.executor.task_queue_manager import TaskQueueManager +from ansible.inventory import Inventory +from ansible.parsing import DataLoader +from ansible.parsing.splitter import parse_kv +from ansible.playbook.play import Play +from ansible.utils.display import Display +from ansible.utils.vault import read_vault_file +from ansible.vars import VariableManager + +######################################################## + +class PullCLI(CLI): + ''' code behind ansible ad-hoc cli''' + + def parse(self): + ''' create an options parser for bin/ansible ''' + + self.parser = CLI.base_parser( + usage='%prog [options]', + runas_opts=True, + async_opts=True, + output_opts=True, + connect_opts=True, + check_opts=True, + runtask_opts=True, + vault_opts=True, + ) + + # options unique to pull + + self.options, self.args = self.parser.parse_args() + + if len(self.args) != 1: + raise AnsibleOptionsError("Missing target hosts") + + self.display.verbosity = self.options.verbosity + self.validate_conflicts() + + return True + + + def run(self): + ''' use Runner lib to do SSH things ''' + + raise AnsibleError("Not ported to v2 yet") diff --git a/v2/ansible/cli/vault.py b/v2/ansible/cli/vault.py index 62ec5a373b..6231f74332 100644 --- a/v2/ansible/cli/vault.py +++ b/v2/ansible/cli/vault.py @@ -20,9 +20,10 @@ import os import sys import traceback +from ansible import constants as C from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.parsing.vault import VaultEditor -from ansible.utils.cli import CLI +from ansible.cli import CLI from ansible.utils.display import Display class VaultCLI(CLI): @@ -34,13 +35,14 @@ class VaultCLI(CLI): def __init__(self, args, display=None): self.vault_pass = None - super(VaultCli, self).__init__(args, display) + super(VaultCLI, self).__init__(args, display) def parse(self): - # create parser for CLI options self.parser = CLI.base_parser( - usage = "%prog vaultfile.yml", + vault_opts=True, + usage = "usage: %%prog [%s] [--help] [options] vaultfile.yml" % "|".join(self.VALID_ACTIONS), + epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) ) self.set_action() @@ -60,10 +62,10 @@ class VaultCLI(CLI): self.parser.set_usage("usage: %prog rekey [options] file_name") self.options, self.args = self.parser.parse_args() + self.display.verbosity = self.options.verbosity if len(self.args) == 0 or len(self.args) > 1: - self.parser.print_help() - raise AnsibleError("Vault requires a single filename as a parameter") + raise AnsibleOptionsError("Vault requires a single filename as a parameter") def run(self): diff --git a/v2/bin/ansible b/v2/bin/ansible new file mode 100755 index 0000000000..467dd505a2 --- /dev/null +++ b/v2/bin/ansible @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +# (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 . + +######################################################## +from __future__ import (absolute_import) +__metaclass__ = type + +__requires__ = ['ansible'] +try: + import pkg_resources +except Exception: + # Use pkg_resources to find the correct versions of libraries and set + # sys.path appropriately when there are multiversion installs. But we + # have code that better expresses the errors in the places where the code + # is actually used (the deps are optional for many code paths) so we don't + # want to fail here. + pass + +import os +import sys + +from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.utils.display import Display + +######################################################## + +if __name__ == '__main__': + + cli = None + display = Display() + me = os.path.basename(__file__) + + try: + if me == 'ansible-playbook': + from ansible.cli.playbook import PlaybookCLI as mycli + elif me == 'ansible': + from ansible.cli.adhoc import AdHocCLI as mycli + elif me == 'ansible-pull': + from ansible.cli.pull import PullCLI as mycli + elif me == 'ansible-doc': + from ansible.cli.doc import DocCLI as mycli + elif me == 'ansible-vault': + from ansible.cli.vault import VaultCLI as mycli + elif me == 'ansible-galaxy': + from ansible.cli.galaxy import GalaxyCLI as mycli + + cli = mycli(sys.argv, display=display) + if cli: + cli.parse() + sys.exit(cli.run()) + else: + raise AnsibleError("Program not implemented: %s" % me) + + except AnsibleOptionsError as e: + cli.parser.print_help() + display.display(str(e), stderr=True, color='red') + sys.exit(1) + except AnsibleError as e: + display.display(str(e), stderr=True, color='red') + sys.exit(2) + except KeyboardInterrupt: + display.error("interrupted") + sys.exit(4) diff --git a/v2/bin/ansible-doc b/v2/bin/ansible-doc new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/v2/bin/ansible-doc @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/v2/bin/ansible-galaxy b/v2/bin/ansible-galaxy new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/v2/bin/ansible-galaxy @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/v2/bin/ansible-playbook b/v2/bin/ansible-playbook new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/v2/bin/ansible-playbook @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/v2/bin/ansible-pull b/v2/bin/ansible-pull new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/v2/bin/ansible-pull @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/v2/bin/ansible-vault b/v2/bin/ansible-vault new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/v2/bin/ansible-vault @@ -0,0 +1 @@ +ansible \ No newline at end of file