mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 13:52:54 +00:00
there will be only one cli
This commit is contained in:
386
v2/ansible/cli/__init__.py
Normal file
386
v2/ansible/cli/__init__.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import operator
|
||||
import optparse
|
||||
import os
|
||||
import time
|
||||
import yaml
|
||||
import getpass
|
||||
|
||||
from ansible import __version__
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
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):
|
||||
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 '''
|
||||
|
||||
VALID_ACTIONS = ['No Actions']
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
"""
|
||||
Base init method for all command line programs
|
||||
"""
|
||||
|
||||
self.args = args
|
||||
self.options = None
|
||||
self.parser = None
|
||||
self.action = None
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
|
||||
def set_action(self):
|
||||
"""
|
||||
Get the action the user wants to execute from the sys argv list.
|
||||
"""
|
||||
for i in range(0,len(self.args)):
|
||||
arg = self.args[i]
|
||||
if arg in self.VALID_ACTIONS:
|
||||
self.action = arg
|
||||
del self.args[i]
|
||||
break
|
||||
|
||||
if not self.action:
|
||||
self.parser.print_help()
|
||||
raise AnsibleError("Missing required action")
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
Actually runs a child defined method using the execute_<action> pattern
|
||||
"""
|
||||
fn = getattr(self, "execute_%s" % self.action)
|
||||
fn()
|
||||
|
||||
def parse(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
def run(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
@staticmethod
|
||||
def ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=False, confirm_vault=False, confirm_new=False):
|
||||
''' prompt for vault password and/or password change '''
|
||||
|
||||
vault_pass = None
|
||||
new_vault_pass = None
|
||||
|
||||
if ask_vault_pass:
|
||||
vault_pass = getpass.getpass(prompt="Vault password: ")
|
||||
|
||||
if ask_vault_pass and confirm_vault:
|
||||
vault_pass2 = getpass.getpass(prompt="Confirm Vault password: ")
|
||||
if vault_pass != vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
if ask_new_vault_pass:
|
||||
new_vault_pass = getpass.getpass(prompt="New Vault password: ")
|
||||
|
||||
if ask_new_vault_pass and confirm_new:
|
||||
new_vault_pass2 = getpass.getpass(prompt="Confirm New Vault password: ")
|
||||
if new_vault_pass != new_vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
# enforce no newline chars at the end of passwords
|
||||
if vault_pass:
|
||||
vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
if new_vault_pass:
|
||||
new_vault_pass = to_bytes(new_vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
|
||||
return vault_pass, new_vault_pass
|
||||
|
||||
|
||||
def ask_passwords(self):
|
||||
''' prompt for connection and become passwords if needed '''
|
||||
|
||||
op = self.options
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
become_prompt = ''
|
||||
|
||||
if op.ask_pass:
|
||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||
become_prompt = "%s password[defaults to SSH password]: " % op.become_method.upper()
|
||||
if sshpass:
|
||||
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
|
||||
else:
|
||||
become_prompt = "%s password: " % op.become_method.upper()
|
||||
|
||||
if op.become_ask_pass:
|
||||
becomepass = getpass.getpass(prompt=become_prompt)
|
||||
if op.ask_pass and becomepass == '':
|
||||
becomepass = sshpass
|
||||
if becomepass:
|
||||
becomepass = to_bytes(becomepass)
|
||||
|
||||
return (sshpass, becomepass)
|
||||
|
||||
|
||||
def normalize_become_options(self):
|
||||
''' this keeps backwards compatibility with sudo/su self.options '''
|
||||
self.options.become_ask_pass = self.options.become_ask_pass or self.options.ask_sudo_pass or self.options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
|
||||
self.options.become_user = self.options.become_user or self.options.sudo_user or self.options.su_user or C.DEFAULT_BECOME_USER
|
||||
|
||||
if self.options.become:
|
||||
pass
|
||||
elif self.options.sudo:
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
elif self.options.su:
|
||||
self.options.become = True
|
||||
options.become_method = 'su'
|
||||
|
||||
|
||||
def validate_conflicts(self):
|
||||
''' check for conflicting options '''
|
||||
|
||||
op = self.options
|
||||
|
||||
# Check for vault related conflicts
|
||||
if (op.ask_vault_pass and op.vault_password_file):
|
||||
self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
|
||||
|
||||
|
||||
# Check for privilege escalation conflicts
|
||||
if (op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) or \
|
||||
(op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass) or \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass):
|
||||
|
||||
self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||
"and su arguments ('-su', '--su-user', and '--ask-su-pass') "
|
||||
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
|
||||
" 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):
|
||||
''' create an options parser for most ansible scripts '''
|
||||
|
||||
parser = SortedOptParser(usage, version=CLI.version("%prog"))
|
||||
|
||||
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('-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 subset_opts:
|
||||
parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset',
|
||||
help='further limit selected hosts to an additional pattern')
|
||||
parser.add_option('-t', '--tags', dest='tags', default='all',
|
||||
help="only run plays and tasks tagged with these values")
|
||||
parser.add_option('--skip-tags', dest='skip_tags',
|
||||
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 runas_opts:
|
||||
# priv user defaults to root later on to enable detecting when this option was given here
|
||||
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
|
||||
help='ask for sudo password (deprecated, use become)')
|
||||
parser.add_option('--ask-su-pass', default=False, dest='ask_su_pass', action='store_true',
|
||||
help='ask for su password (deprecated, use become)')
|
||||
parser.add_option("-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo',
|
||||
help="run operations with sudo (nopasswd) (deprecated, use become)")
|
||||
parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
|
||||
help='desired sudo user (default=root) (deprecated, use become)')
|
||||
parser.add_option('-S', '--su', default=C.DEFAULT_SU, action='store_true',
|
||||
help='run operations with su (deprecated, use become)')
|
||||
parser.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)
|
||||
parser.add_option("-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become',
|
||||
help="run operations with become (nopasswd implied)")
|
||||
parser.add_option('--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='string',
|
||||
help="privilege escalation method to use (default=%s), valid choices: [ %s ]" % (C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS)))
|
||||
parser.add_option('--become-user', default=None, dest='become_user', type='string',
|
||||
help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER)
|
||||
parser.add_option('--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
|
||||
help='ask for privilege escalation password')
|
||||
|
||||
|
||||
if connect_opts:
|
||||
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',
|
||||
help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
if diff_opts:
|
||||
parser.add_option("-D", "--diff", default=False, 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', 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")
|
||||
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def version(prog):
|
||||
''' return ansible version '''
|
||||
result = "{0} {1}".format(prog, __version__)
|
||||
gitinfo = _gitinfo()
|
||||
if gitinfo:
|
||||
result = result + " {0}".format(gitinfo)
|
||||
result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def version_info(gitinfo=False):
|
||||
''' return full ansible version info '''
|
||||
if gitinfo:
|
||||
# expensive call, user with care
|
||||
ansible_version_string = version('')
|
||||
else:
|
||||
ansible_version_string = __version__
|
||||
ansible_version = ansible_version_string.split()[0]
|
||||
ansible_versions = ansible_version.split('.')
|
||||
for counter in range(len(ansible_versions)):
|
||||
if ansible_versions[counter] == "":
|
||||
ansible_versions[counter] = 0
|
||||
try:
|
||||
ansible_versions[counter] = int(ansible_versions[counter])
|
||||
except:
|
||||
pass
|
||||
if len(ansible_versions) < 3:
|
||||
for counter in range(len(ansible_versions), 3):
|
||||
ansible_versions.append(0)
|
||||
return {'string': ansible_version_string.strip(),
|
||||
'full': ansible_version,
|
||||
'major': ansible_versions[0],
|
||||
'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"))
|
||||
branch = f.readline().split('/')[-1].rstrip("\n")
|
||||
f.close()
|
||||
branch_path = os.path.join(repo_path, "refs", "heads", branch)
|
||||
if os.path.exists(branch_path):
|
||||
f = open(branch_path)
|
||||
commit = f.readline()[:10]
|
||||
f.close()
|
||||
else:
|
||||
# detached HEAD
|
||||
commit = branch[: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 = _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 =_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
|
||||
157
v2/ansible/cli/adhoc.py
Normal file
157
v2/ansible/cli/adhoc.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
########################################################
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
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.utils.display import Display
|
||||
from ansible.utils.vault import read_vault_file
|
||||
from ansible.vars import VariableManager
|
||||
|
||||
########################################################
|
||||
|
||||
class AdHocCLI(CLI):
|
||||
''' code behind ansible ad-hoc cli'''
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
usage='%prog <host-pattern> [options]',
|
||||
runas_opts=True,
|
||||
async_opts=True,
|
||||
output_opts=True,
|
||||
connect_opts=True,
|
||||
check_opts=True,
|
||||
)
|
||||
|
||||
# options unique to ansible ad-hoc
|
||||
self.parser.add_option('-a', '--args', dest='module_args',
|
||||
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
|
||||
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)
|
||||
|
||||
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 '''
|
||||
|
||||
# only thing left should be host pattern
|
||||
pattern = self.args[0]
|
||||
|
||||
# ignore connection password cause we are local
|
||||
if self.options.connection == "local":
|
||||
self.options.ask_pass = False
|
||||
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
variable_manager = VariableManager()
|
||||
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
|
||||
hosts = inventory.list_hosts(pattern)
|
||||
if len(hosts) == 0:
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
|
||||
if self.options.listhosts:
|
||||
for host in hosts:
|
||||
self.display.display(' %s' % host.name)
|
||||
return 0
|
||||
|
||||
if self.options.module_name in C.MODULE_REQUIRE_ARGS and not self.options.module_args:
|
||||
raise AnsibleError("No argument passed to %s module" % self.options.module_name)
|
||||
|
||||
#TODO: implement async support
|
||||
#if self.options.seconds:
|
||||
# callbacks.display("background launch...\n\n", color='cyan')
|
||||
# results, poller = runner.run_async(self.options.seconds)
|
||||
# results = self.poll_while_needed(poller)
|
||||
#else:
|
||||
# results = runner.run()
|
||||
|
||||
# create a pseudo-play to execute the specified module via a single task
|
||||
play_ds = dict(
|
||||
name = "Ansible Ad-Hoc",
|
||||
hosts = pattern,
|
||||
gather_facts = 'no',
|
||||
tasks = [ dict(action=dict(module=self.options.module_name, args=parse_kv(self.options.module_args))), ]
|
||||
)
|
||||
|
||||
play = Play().load(play_ds, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
# now create a task queue manager to execute the play
|
||||
tqm = None
|
||||
try:
|
||||
tqm = TaskQueueManager(
|
||||
inventory=inventory,
|
||||
variable_manager=variable_manager,
|
||||
loader=loader,
|
||||
display=self.display,
|
||||
options=self.options,
|
||||
passwords=passwords,
|
||||
stdout_callback='minimal',
|
||||
)
|
||||
result = tqm.run(play)
|
||||
finally:
|
||||
if tqm:
|
||||
tqm.cleanup()
|
||||
|
||||
return result
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
def poll_while_needed(self, poller):
|
||||
''' summarize results from Runner '''
|
||||
|
||||
# BACKGROUND POLL LOGIC when -B and -P are specified
|
||||
if self.options.seconds and self.options.poll_interval > 0:
|
||||
poller.wait(self.options.seconds, self.options.poll_interval)
|
||||
|
||||
return poller.results
|
||||
|
||||
495
v2/ansible/cli/galaxy.py
Normal file
495
v2/ansible/cli/galaxy.py
Normal file
@@ -0,0 +1,495 @@
|
||||
########################################################################
|
||||
#
|
||||
# (C) 2013, James Cammarata <jcammarata@ansible.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib
|
||||
import urllib2
|
||||
import yaml
|
||||
|
||||
from collections import defaultdict
|
||||
from distutils.version import LooseVersion
|
||||
from jinja2 import Environment
|
||||
from optparse import OptionParser
|
||||
|
||||
import ansible.constants as C
|
||||
import ansible.utils
|
||||
import ansible.galaxy
|
||||
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):
|
||||
|
||||
VALID_ACTIONS = ("init", "info", "install", "list", "remove")
|
||||
SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.api = None
|
||||
self.galaxy = None
|
||||
super(GalaxyCLI, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS)
|
||||
epilog = "\nSee '%s <command> --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 = 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]")
|
||||
elif self.action == "init":
|
||||
self.parser.set_usage("usage: %prog init [options] role_name")
|
||||
self.parser.add_option(
|
||||
'-p', '--init-path', dest='init_path', default="./",
|
||||
help='The path in which the skeleton role will be created. '
|
||||
'The default is the current working directory.')
|
||||
self.parser.add_option(
|
||||
'--offline', dest='offline', default=False, action='store_true',
|
||||
help="Don't query the galaxy API when creating roles")
|
||||
elif self.action == "install":
|
||||
self.parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
|
||||
self.parser.add_option(
|
||||
'-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
||||
help='Ignore errors and continue with the next specified role.')
|
||||
self.parser.add_option(
|
||||
'-n', '--no-deps', dest='no_deps', action='store_true', default=False,
|
||||
help='Don\'t download roles listed as dependencies')
|
||||
self.parser.add_option(
|
||||
'-r', '--role-file', dest='role_file',
|
||||
help='A file containing a list of roles to be imported')
|
||||
elif self.action == "remove":
|
||||
self.parser.set_usage("usage: %prog remove role1 role2 ...")
|
||||
elif self.action == "list":
|
||||
self.parser.set_usage("usage: %prog list [role_name]")
|
||||
|
||||
# options that apply to more than one action
|
||||
if self.action != "init":
|
||||
self.parser.add_option(
|
||||
'-p', '--roles-path', dest='roles_path', 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)')
|
||||
|
||||
if self.action in ("info","init","install"):
|
||||
self.parser.add_option( '-s', '--server', dest='api_server', default="https://galaxy.ansible.com",
|
||||
help='The API server destination')
|
||||
|
||||
if self.action in ("init","install"):
|
||||
self.parser.add_option(
|
||||
'-f', '--force', dest='force', action='store_true', default=False,
|
||||
help='Force overwriting an existing role')
|
||||
|
||||
# get options, args and galaxy object
|
||||
self.options, self.args =self.parser.parse_args()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.galaxy = Galaxy(self.options, self.display)
|
||||
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
|
||||
# if not offline, get connect to galaxy api
|
||||
if self.action in ("info","install") or (self.action == 'init' and not self.options.offline):
|
||||
api_server = self.options.api_server
|
||||
self.api = GalaxyAPI(self.galaxy, api_server)
|
||||
if not self.api:
|
||||
raise AnsibleError("The API server (%s) is not responding, please try again later." % api_server)
|
||||
|
||||
self.execute()
|
||||
|
||||
def get_opt(self, k, defval=""):
|
||||
"""
|
||||
Returns an option from an Optparse values instance.
|
||||
"""
|
||||
try:
|
||||
data = getattr(self.options, k)
|
||||
except:
|
||||
return defval
|
||||
if k == "roles_path":
|
||||
if os.pathsep in data:
|
||||
data = data.split(os.pathsep)[0]
|
||||
return data
|
||||
|
||||
def exit_without_ignore(self, rc=1):
|
||||
"""
|
||||
Exits with the specified return code unless the
|
||||
option --ignore-errors was specified
|
||||
"""
|
||||
if not self.get_opt("ignore_errors", False):
|
||||
self.display.error('- you can use --ignore-errors to skip failed roles and finish processing the list.')
|
||||
return rc
|
||||
|
||||
def execute_init(self):
|
||||
"""
|
||||
Executes the init action, which creates the skeleton framework
|
||||
of a role that complies with the galaxy metadata format.
|
||||
"""
|
||||
|
||||
init_path = self.get_opt('init_path', './')
|
||||
force = self.get_opt('force', False)
|
||||
offline = self.get_opt('offline', False)
|
||||
|
||||
role_name = self.args.pop(0).strip()
|
||||
if role_name == "":
|
||||
raise AnsibleOptionsError("- no role name specified for init")
|
||||
role_path = os.path.join(init_path, role_name)
|
||||
if os.path.exists(role_path):
|
||||
if os.path.isfile(role_path):
|
||||
raise AnsibleError("- the path %s already exists, but is a file - aborting" % role_path)
|
||||
elif not force:
|
||||
raise AnsibleError("- the directory %s already exists." % role_path + \
|
||||
"you can use --force to re-initialize this directory,\n" + \
|
||||
"however it will reset any main.yml files that may have\n" + \
|
||||
"been modified there already.")
|
||||
|
||||
# create the default README.md
|
||||
if not os.path.exists(role_path):
|
||||
os.makedirs(role_path)
|
||||
readme_path = os.path.join(role_path, "README.md")
|
||||
f = open(readme_path, "wb")
|
||||
f.write(self.galaxy.default_readme)
|
||||
f.close
|
||||
|
||||
for dir in GalaxyRole.ROLE_DIRS:
|
||||
dir_path = os.path.join(init_path, role_name, dir)
|
||||
main_yml_path = os.path.join(dir_path, 'main.yml')
|
||||
# create the directory if it doesn't exist already
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
# now create the main.yml file for that directory
|
||||
if dir == "meta":
|
||||
# create a skeleton meta/main.yml with a valid galaxy_info
|
||||
# datastructure in place, plus with all of the available
|
||||
# tags/platforms included (but commented out) and the
|
||||
# dependencies section
|
||||
platforms = []
|
||||
if not offline and self.api:
|
||||
platforms = self.api.get_list("platforms") or []
|
||||
categories = []
|
||||
if not offline and self.api:
|
||||
categories = self.api.get_list("categories") or []
|
||||
|
||||
# group the list of platforms from the api based
|
||||
# on their names, with the release field being
|
||||
# appended to a list of versions
|
||||
platform_groups = defaultdict(list)
|
||||
for platform in platforms:
|
||||
platform_groups[platform['name']].append(platform['release'])
|
||||
platform_groups[platform['name']].sort()
|
||||
|
||||
inject = dict(
|
||||
author = 'your name',
|
||||
company = 'your company (optional)',
|
||||
license = 'license (GPLv2, CC-BY, etc)',
|
||||
issue_tracker_url = 'http://example.com/issue/tracker',
|
||||
min_ansible_version = '1.2',
|
||||
platforms = platform_groups,
|
||||
categories = categories,
|
||||
)
|
||||
rendered_meta = Environment().from_string(self.galaxy.default_meta).render(inject)
|
||||
f = open(main_yml_path, 'w')
|
||||
f.write(rendered_meta)
|
||||
f.close()
|
||||
pass
|
||||
elif dir not in ('files','templates'):
|
||||
# just write a (mostly) empty YAML file for main.yml
|
||||
f = open(main_yml_path, 'w')
|
||||
f.write('---\n# %s file for %s\n' % (dir,role_name))
|
||||
f.close()
|
||||
self.display.display("- %s was created successfully" % role_name)
|
||||
|
||||
def execute_info(self):
|
||||
"""
|
||||
Executes the info action. This action prints out detailed
|
||||
information about an installed role as well as info available
|
||||
from the galaxy API.
|
||||
"""
|
||||
|
||||
if len(self.args) == 0:
|
||||
# the user needs to specify a role
|
||||
raise AnsibleOptionsError("- you must specify a user/role name")
|
||||
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
for role in self.args:
|
||||
|
||||
role_info = {}
|
||||
gr = GalaxyRole(self.galaxy, role)
|
||||
#self.galaxy.add_role(gr)
|
||||
|
||||
install_info = gr.install_info
|
||||
if install_info:
|
||||
if 'version' in install_info:
|
||||
install_info['intalled_version'] = install_info['version']
|
||||
del install_info['version']
|
||||
role_info.update(install_info)
|
||||
|
||||
remote_data = False
|
||||
if self.api:
|
||||
remote_data = self.api.lookup_role_by_name(role, False)
|
||||
|
||||
if remote_data:
|
||||
role_info.update(remote_data)
|
||||
|
||||
if gr.metadata:
|
||||
role_info.update(gr.metadata)
|
||||
|
||||
req = RoleRequirement()
|
||||
__, __, role_spec= req.parse({'role': role})
|
||||
if role_spec:
|
||||
role_info.update(role_spec)
|
||||
|
||||
if role_info:
|
||||
self.display.display("- %s:" % (role))
|
||||
for k in sorted(role_info.keys()):
|
||||
|
||||
if k in self.SKIP_INFO_KEYS:
|
||||
continue
|
||||
|
||||
if isinstance(role_info[k], dict):
|
||||
self.display.display("\t%s: " % (k))
|
||||
for key in sorted(role_info[k].keys()):
|
||||
if key in self.SKIP_INFO_KEYS:
|
||||
continue
|
||||
self.display.display("\t\t%s: %s" % (key, role_info[k][key]))
|
||||
else:
|
||||
self.display.display("\t%s: %s" % (k, role_info[k]))
|
||||
else:
|
||||
self.display.display("- the role %s was not found" % role)
|
||||
|
||||
def execute_install(self):
|
||||
"""
|
||||
Executes the installation action. The args list contains the
|
||||
roles to be installed, unless -f was specified. The list of roles
|
||||
can be a name (which will be downloaded via the galaxy API and github),
|
||||
or it can be a local .tar.gz file.
|
||||
"""
|
||||
|
||||
role_file = self.get_opt("role_file", None)
|
||||
|
||||
if len(self.args) == 0 and role_file is None:
|
||||
# the user needs to specify one of either --role-file
|
||||
# or specify a single user/role name
|
||||
raise AnsibleOptionsError("- you must specify a user/role name or a roles file")
|
||||
elif len(self.args) == 1 and not role_file is None:
|
||||
# using a role file is mutually exclusive of specifying
|
||||
# the role name on the command line
|
||||
raise AnsibleOptionsError("- please specify a user/role name, or a roles file, but not both")
|
||||
|
||||
no_deps = self.get_opt("no_deps", False)
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
roles_done = []
|
||||
roles_left = []
|
||||
role_name = self.args.pop(0).strip()
|
||||
|
||||
gr = GalaxyRole(self.galaxy, role_name)
|
||||
if role_file:
|
||||
f = open(role_file, 'r')
|
||||
if role_file.endswith('.yaml') or role_file.endswith('.yml'):
|
||||
roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f))
|
||||
else:
|
||||
# roles listed in a file, one per line
|
||||
for rname in f.readlines():
|
||||
roles_left.append(GalaxyRole(self.galaxy, rname))
|
||||
f.close()
|
||||
else:
|
||||
# roles were specified directly, so we'll just go out grab them
|
||||
# (and their dependencies, unless the user doesn't want us to).
|
||||
for rname in self.args:
|
||||
roles_left.append(GalaxyRole(self.galaxy, rname))
|
||||
|
||||
while len(roles_left) > 0:
|
||||
# query the galaxy API for the role data
|
||||
role_data = None
|
||||
role = roles_left.pop(0)
|
||||
role_src = role.src
|
||||
role_scm = role.scm
|
||||
role_path = role.path
|
||||
|
||||
if role_path:
|
||||
self.options.roles_path = role_path
|
||||
else:
|
||||
self.options.roles_path = roles_path
|
||||
|
||||
tmp_file = None
|
||||
if role_src and os.path.isfile(role_src):
|
||||
# installing a local tar.gz
|
||||
tmp_file = role_src
|
||||
else:
|
||||
if role_scm:
|
||||
# create tar file from scm url
|
||||
tmp_file = scm_archive_role(role_scm, role_src, role.version, role.name)
|
||||
if role_src:
|
||||
if '://' in role_src:
|
||||
# just download a URL - version will probably be in the URL
|
||||
tmp_file = gr.fetch()
|
||||
else:
|
||||
role_data = self.api.lookup_role_by_name(role_src)
|
||||
if not role_data:
|
||||
self.display.warning("- sorry, %s was not found on %s." % (role_src, self.options.api_server))
|
||||
self.exit_without_ignore()
|
||||
continue
|
||||
|
||||
role_versions = self.api.fetch_role_related('versions', role_data['id'])
|
||||
if not role.version:
|
||||
# convert the version names to LooseVersion objects
|
||||
# and sort them to get the latest version. If there
|
||||
# are no versions in the list, we'll grab the head
|
||||
# of the master branch
|
||||
if len(role_versions) > 0:
|
||||
loose_versions = [LooseVersion(a.get('name',None)) for a in role_versions]
|
||||
loose_versions.sort()
|
||||
role["version"] = str(loose_versions[-1])
|
||||
else:
|
||||
role["version"] = 'master'
|
||||
elif role['version'] != 'master':
|
||||
if role_versions and role.version not in [a.get('name', None) for a in role_versions]:
|
||||
self.display.warning('role is %s' % role)
|
||||
self.display.warning("- the specified version (%s) was not found in the list of available versions (%s)." % (role.version, role_versions))
|
||||
self.exit_without_ignore()
|
||||
continue
|
||||
|
||||
# download the role. if --no-deps was specified, we stop here,
|
||||
# otherwise we recursively grab roles and all of their deps.
|
||||
tmp_file = gr.fetch(role_data)
|
||||
installed = False
|
||||
if tmp_file:
|
||||
installed = install_role(role.name, role.version, tmp_file, options)
|
||||
# we're done with the temp file, clean it up
|
||||
if tmp_file != role_src:
|
||||
os.unlink(tmp_file)
|
||||
# install dependencies, if we want them
|
||||
|
||||
# this should use new roledepenencies code
|
||||
#if not no_deps and installed:
|
||||
# if not role_data:
|
||||
# role_data = gr.get_metadata(role.get("name"), options)
|
||||
# role_dependencies = role_data['dependencies']
|
||||
# else:
|
||||
# role_dependencies = role_data['summary_fields']['dependencies'] # api_fetch_role_related(api_server, 'dependencies', role_data['id'])
|
||||
# for dep in role_dependencies:
|
||||
# if isinstance(dep, basestring):
|
||||
# dep = ansible.utils.role_spec_parse(dep)
|
||||
# else:
|
||||
# dep = ansible.utils.role_yaml_parse(dep)
|
||||
# if not get_role_metadata(dep["name"], options):
|
||||
# if dep not in roles_left:
|
||||
# print '- adding dependency: %s' % dep["name"]
|
||||
# roles_left.append(dep)
|
||||
# else:
|
||||
# print '- dependency %s already pending installation.' % dep["name"]
|
||||
# else:
|
||||
# print '- dependency %s is already installed, skipping.' % dep["name"]
|
||||
|
||||
if not tmp_file or not installed:
|
||||
self.display.warning("- %s was NOT installed successfully." % role.name)
|
||||
self.exit_without_ignore()
|
||||
return 0
|
||||
|
||||
def execute_remove(self):
|
||||
"""
|
||||
Executes the remove action. The args list contains the list
|
||||
of roles to be removed. This list can contain more than one role.
|
||||
"""
|
||||
|
||||
if len(self.args) == 0:
|
||||
raise AnsibleOptionsError('- you must specify at least one role to remove.')
|
||||
|
||||
for role_name in self.args:
|
||||
role = GalaxyRole(self.galaxy, role_name)
|
||||
try:
|
||||
if role.remove():
|
||||
self.display.display('- successfully removed %s' % role_name)
|
||||
else:
|
||||
self.display.display('- %s is not installed, skipping.' % role_name)
|
||||
except Exception as e:
|
||||
raise AnsibleError("Failed to remove role %s: %s" % (role_name, str(e)))
|
||||
|
||||
return 0
|
||||
|
||||
def execute_list(self):
|
||||
"""
|
||||
Executes the list action. The args list can contain zero
|
||||
or one role. If one is specified, only that role will be
|
||||
shown, otherwise all roles in the specified directory will
|
||||
be shown.
|
||||
"""
|
||||
|
||||
if len(self.args) > 1:
|
||||
raise AnsibleOptionsError("- please specify only one role to list, or specify no roles to see a full list")
|
||||
|
||||
if len(self.args) == 1:
|
||||
# show only the request role, if it exists
|
||||
gr = GalaxyRole(self.galaxy, self.name)
|
||||
if gr.metadata:
|
||||
install_info = gr.install_info
|
||||
version = None
|
||||
if install_info:
|
||||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
# show some more info about single roles here
|
||||
self.display.display("- %s, %s" % (self.name, version))
|
||||
else:
|
||||
self.display.display("- the role %s was not found" % self.name)
|
||||
else:
|
||||
# show all valid roles in the roles_path directory
|
||||
roles_path = self.get_opt('roles_path')
|
||||
roles_path = os.path.expanduser(roles_path)
|
||||
if not os.path.exists(roles_path):
|
||||
raise AnsibleOptionsError("- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path)
|
||||
elif not os.path.isdir(roles_path):
|
||||
raise AnsibleOptionsError("- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path)
|
||||
path_files = os.listdir(roles_path)
|
||||
for path_file in path_files:
|
||||
if gr.metadata:
|
||||
install_info = gr.metadata
|
||||
version = None
|
||||
if install_info:
|
||||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
self.display.display("- %s, %s" % (path_file, version))
|
||||
return 0
|
||||
179
v2/ansible/cli/playbook.py
Normal file
179
v2/ansible/cli/playbook.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
########################################################
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.executor.playbook_executor import PlaybookExecutor
|
||||
from ansible.inventory import Inventory
|
||||
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
|
||||
from ansible.utils.vault import read_vault_file
|
||||
from ansible.vars import VariableManager
|
||||
|
||||
#---------------------------------------------------------------------------------------------------
|
||||
|
||||
class PlaybookCLI(CLI):
|
||||
''' code behind ansible playbook cli'''
|
||||
|
||||
def parse(self):
|
||||
|
||||
# create parser for CLI options
|
||||
parser = CLI.base_parser(
|
||||
usage = "%prog playbook.yml",
|
||||
connect_opts=True,
|
||||
meta_opts=True,
|
||||
runas_opts=True,
|
||||
subset_opts=True,
|
||||
check_opts=True,
|
||||
diff_opts=True,
|
||||
)
|
||||
|
||||
# ansible playbook specific opts
|
||||
parser.add_option('--list-tasks', dest='listtasks', action='store_true',
|
||||
help="list all tasks that would be executed")
|
||||
parser.add_option('--step', dest='step', action='store_true',
|
||||
help="one-step-at-a-time: confirm each task before running")
|
||||
parser.add_option('--start-at-task', dest='start_at',
|
||||
help="start the playbook at the task matching this name")
|
||||
parser.add_option('--list-tags', dest='listtags', action='store_true',
|
||||
help="list all available tags")
|
||||
|
||||
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")
|
||||
|
||||
self.parser = parser
|
||||
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
def run(self):
|
||||
|
||||
# Note: slightly wrong, this is written so that implicit localhost
|
||||
# Manage passwords
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
passwords = {}
|
||||
|
||||
# don't deal with privilege escalation or passwords when we don't need to
|
||||
if not self.options.listhosts and not self.options.listtasks and not self.options.listtags:
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
|
||||
extra_vars = {}
|
||||
for extra_vars_opt in self.options.extra_vars:
|
||||
extra_vars_opt = to_unicode(extra_vars_opt, errors='strict')
|
||||
if extra_vars_opt.startswith(u"@"):
|
||||
# Argument is a YAML file (JSON is a subset of YAML)
|
||||
data = loader.load_from_file(extra_vars_opt[1:])
|
||||
elif extra_vars_opt and extra_vars_opt[0] in u'[{':
|
||||
# Arguments as YAML
|
||||
data = loader.load(extra_vars_opt)
|
||||
else:
|
||||
# Arguments as Key-value
|
||||
data = parse_kv(extra_vars_opt)
|
||||
extra_vars = combine_vars(extra_vars, data)
|
||||
|
||||
# FIXME: this should be moved inside the playbook executor code
|
||||
only_tags = self.options.tags.split(",")
|
||||
skip_tags = self.options.skip_tags
|
||||
if self.options.skip_tags is not None:
|
||||
skip_tags = self.ptions.skip_tags.split(",")
|
||||
|
||||
# initial error check, to make sure all specified playbooks are accessible
|
||||
# before we start running anything through the playbook executor
|
||||
for playbook in self.args:
|
||||
if not os.path.exists(playbook):
|
||||
raise AnsibleError("the playbook: %s could not be found" % playbook)
|
||||
if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)):
|
||||
raise AnsibleError("the playbook: %s does not appear to be a file" % playbook)
|
||||
|
||||
# create the variable manager, which will be shared throughout
|
||||
# the code, ensuring a consistent view of global variables
|
||||
variable_manager = VariableManager()
|
||||
variable_manager.set_extra_vars(extra_vars)
|
||||
|
||||
# create the inventory, and filter it based on the subset specified (if any)
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
variable_manager.set_inventory(inventory)
|
||||
|
||||
# (which is not returned in list_hosts()) is taken into account for
|
||||
# warning if inventory is empty. But it can't be taken into account for
|
||||
# checking if limit doesn't match any hosts. Instead we don't worry about
|
||||
# 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())
|
||||
no_hosts = False
|
||||
if len(inventory.list_hosts()) == 0:
|
||||
# Empty inventory
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
no_hosts = True
|
||||
inventory.subset(self.options.subset)
|
||||
if len(inventory.list_hosts()) == 0 and no_hosts is False:
|
||||
# Invalid limit
|
||||
raise AnsibleError("Specified --limit does not match any hosts")
|
||||
|
||||
# create the playbook executor, which manages running the plays via a task queue manager
|
||||
pbex = PlaybookExecutor(playbooks=self.args, inventory=inventory, variable_manager=variable_manager, loader=loader, display=self.display, options=self.options, passwords=passwords)
|
||||
|
||||
results = pbex.run()
|
||||
|
||||
if isinstance(results, list):
|
||||
for p in results:
|
||||
|
||||
self.display.display('\nplaybook: %s\n' % p['playbook'])
|
||||
for play in p['plays']:
|
||||
if self.options.listhosts:
|
||||
self.display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
|
||||
for host in play['hosts']:
|
||||
self.display.display(" %s" % host)
|
||||
if self.options.listtasks: #TODO: do we want to display block info?
|
||||
self.display.display("\n %s" % (play['name']))
|
||||
for task in play['tasks']:
|
||||
self.display.display(" %s" % task)
|
||||
if self.options.listtags: #TODO: fix once we figure out block handling above
|
||||
self.display.display("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
|
||||
for tag in play['tags']:
|
||||
self.display.display(" %s" % tag)
|
||||
return 0
|
||||
else:
|
||||
return results
|
||||
121
v2/ansible/cli/vault.py
Normal file
121
v2/ansible/cli/vault.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# (c) 2014, James Tanner <tanner.jc@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.parsing.vault import VaultEditor
|
||||
from ansible.utils.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
|
||||
class VaultCLI(CLI):
|
||||
""" Vault command line class """
|
||||
|
||||
VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view")
|
||||
CIPHER = 'AES256'
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.vault_pass = None
|
||||
super(VaultCli, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
|
||||
# create parser for CLI options
|
||||
self.parser = CLI.base_parser(
|
||||
usage = "%prog vaultfile.yml",
|
||||
)
|
||||
|
||||
self.set_action()
|
||||
|
||||
# options specific to self.actions
|
||||
if self.action == "create":
|
||||
self.parser.set_usage("usage: %prog create [options] file_name")
|
||||
elif self.action == "decrypt":
|
||||
self.parser.set_usage("usage: %prog decrypt [options] file_name")
|
||||
elif self.action == "edit":
|
||||
self.parser.set_usage("usage: %prog edit [options] file_name")
|
||||
elif self.action == "view":
|
||||
self.parser.set_usage("usage: %prog view [options] file_name")
|
||||
elif self.action == "encrypt":
|
||||
self.parser.set_usage("usage: %prog encrypt [options] file_name")
|
||||
elif action == "rekey":
|
||||
self.parser.set_usage("usage: %prog rekey [options] file_name")
|
||||
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
|
||||
if len(self.args) == 0 or len(self.args) > 1:
|
||||
self.parser.print_help()
|
||||
raise AnsibleError("Vault requires a single filename as a parameter")
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
self.vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
self.vault_pass, _= self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
|
||||
|
||||
self.execute()
|
||||
|
||||
def execute_create(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, self.args[0])
|
||||
this_editor.create_file()
|
||||
|
||||
def execute_decrypt(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.decrypt_file()
|
||||
|
||||
self.display.display("Decryption successful")
|
||||
|
||||
def execute_edit(self):
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.edit_file()
|
||||
|
||||
def execute_view(self):
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.view_file()
|
||||
|
||||
def execute_encrypt(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.encrypt_file()
|
||||
|
||||
self.display.display("Encryption successful")
|
||||
|
||||
def execute_rekey(self):
|
||||
__, new_password = self.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.rekey_file(new_password)
|
||||
|
||||
self.display.display("Rekey successful")
|
||||
Reference in New Issue
Block a user