Commit fec9b8a5 by Brian Coca

Merge pull request #10885 from bcoca/galaxy_v2

revamped base CLI and moved galaxy to it's own classes/api
parents b851ce29 ccc9a33b
......@@ -27,6 +27,7 @@ New Modules:
* cloudstack: cs_securitygroup_rule
* cloudstack: cs_vmsnapshot
* maven_artifact
* openstack: os_server_facts
* pushover
* zabbix_host
* zabbix_hostmacro
......
......@@ -40,13 +40,15 @@ def get_config(p, section, key, env_var, default, boolean=False, integer=False,
''' return a configuration variable with casting '''
value = _get_config(p, section, key, env_var, default)
if boolean:
return mk_boolean(value)
if value and integer:
return int(value)
if value and floating:
return float(value)
if value and islist:
return [x.strip() for x in value.split(',')]
value = mk_boolean(value)
if value:
if integer:
value = int(value)
elif floating:
value = float(value)
elif islist:
if isinstance(value, basestring):
value = [x.strip() for x in value.split(',')]
return value
def _get_config(p, section, key, env_var, default):
......@@ -104,7 +106,7 @@ DEFAULTS='defaults'
# configurable things
DEFAULT_DEBUG = get_config(p, DEFAULTS, 'debug', 'ANSIBLE_DEBUG', False, boolean=True)
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'inventory', 'ANSIBLE_INVENTORY', get_config(p, DEFAULTS,'hostfile','ANSIBLE_HOSTS', '/etc/ansible/hosts')))
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'hostfile', 'ANSIBLE_HOSTS', get_config(p, DEFAULTS,'inventory','ANSIBLE_INVENTORY', '/etc/ansible/hosts')))
DEFAULT_MODULE_PATH = get_config(p, DEFAULTS, 'library', 'ANSIBLE_LIBRARY', None)
DEFAULT_ROLES_PATH = shell_expand_path(get_config(p, DEFAULTS, 'roles_path', 'ANSIBLE_ROLES_PATH', '/etc/ansible/roles'))
DEFAULT_REMOTE_TMP = get_config(p, DEFAULTS, 'remote_tmp', 'ANSIBLE_REMOTE_TEMP', '$HOME/.ansible/tmp')
......@@ -203,10 +205,16 @@ ACCELERATE_KEYS_FILE_PERMS = get_config(p, 'accelerate', 'accelerate_keys_fi
ACCELERATE_MULTI_KEY = get_config(p, 'accelerate', 'accelerate_multi_key', 'ACCELERATE_MULTI_KEY', False, boolean=True)
PARAMIKO_PTY = get_config(p, 'paramiko_connection', 'pty', 'ANSIBLE_PARAMIKO_PTY', True, boolean=True)
# galaxy related
DEFAULT_GALAXY_URI = get_config(p, 'galaxy', 'server_uri', 'ANSIBLE_GALAXY_SERVER_URI', 'https://galaxy.ansible.com')
# this can be configured to blacklist SCMS but cannot add new ones unless the code is also updated
GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY_SCMS', ['git','hg'], islist=True)
# characters included in auto-generated passwords
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
# non-configurable things
MODULE_REQUIRE_ARGS = ['command', 'shell', 'raw', 'script']
DEFAULT_BECOME_PASS = None
DEFAULT_SUDO_PASS = None
DEFAULT_REMOTE_PASS = None
......
......@@ -140,6 +140,10 @@ class AnsibleError(Exception):
return error_message
class AnsibleOptionsError(AnsibleError):
''' bad or incomplete options passed '''
pass
class AnsibleParserError(AnsibleError):
''' something was detected early that is wrong about a playbook or data file '''
pass
......@@ -164,6 +168,14 @@ class AnsibleFilterError(AnsibleRuntimeError):
''' a templating failure '''
pass
class AnsibleLookupError(AnsibleRuntimeError):
''' a lookup failure '''
pass
class AnsibleCallbackError(AnsibleRuntimeError):
''' a callback failure '''
pass
class AnsibleUndefinedVariable(AnsibleRuntimeError):
''' a templating failure '''
pass
......@@ -171,7 +183,3 @@ class AnsibleUndefinedVariable(AnsibleRuntimeError):
class AnsibleFileNotFound(AnsibleRuntimeError):
''' a file missing failure '''
pass
class AnsibleParserError(AnsibleRuntimeError):
''' a parser error '''
pass
########################################################################
#
# (C) 2015, Brian Coca <bcoca@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/>.
#
########################################################################
''' This manages remote shared Ansible objects, mainly roles'''
import os
from ansible.errors import AnsibleError
from ansible.utils.display import Display
# default_readme_template
# default_meta_template
class Galaxy(object):
''' Keeps global galaxy info '''
def __init__(self, options, display=None):
if display is None:
self.display = Display()
else:
self.display = display
self.options = options
self.roles_path = getattr(self.options, 'roles_path', None)
if self.roles_path:
self.roles_path = os.path.expanduser(self.roles_path)
self.roles = {}
# load data path for resource usage
this_dir, this_filename = os.path.split(__file__)
self.DATA_PATH = os.path.join(this_dir, "data")
#TODO: move to getter for lazy loading
self.default_readme = self._str_from_data_file('readme')
self.default_meta = self._str_from_data_file('metadata_template.j2')
def add_role(self, role):
self.roles[role.name] = role
def remove_role(self, role_name):
del self.roles[role_name]
def _str_from_data_file(self, filename):
myfile = os.path.join(self.DATA_PATH, filename)
try:
return open(myfile).read()
except Exception as e:
raise AnsibleError("Could not open %s: %s" % (filename, str(e)))
#!/usr/bin/env python
########################################################################
#
# (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 json
from urllib2 import urlopen, quote as urlquote
from urlparse import urlparse
from ansible.errors import AnsibleError
class GalaxyAPI(object):
''' This class is meant to be used as a API client for an Ansible Galaxy server '''
SUPPORTED_VERSIONS = ['v1']
def __init__(self, galaxy, api_server):
self.galaxy = galaxy
try:
urlparse(api_server, scheme='https')
except:
raise AnsibleError("Invalid server API url passed: %s" % api_server)
server_version = self.get_server_api_version('%s/api/' % (api_server))
if not server_version:
raise AnsibleError("Could not retrieve server API version: %s" % api_server)
if server_version in self.SUPPORTED_VERSIONS:
self.baseurl = '%s/api/%s' % (api_server, server_version)
self.version = server_version # for future use
self.galaxy.display.vvvvv("Base API: %s" % self.baseurl)
else:
raise AnsibleError("Unsupported Galaxy server API version: %s" % server_version)
def get_server_api_version(self, api_server):
"""
Fetches the Galaxy API current version to ensure
the API server is up and reachable.
"""
#TODO: fix galaxy server which returns current_version path (/api/v1) vs actual version (v1)
# also should set baseurl using supported_versions which has path
return 'v1'
try:
data = json.load(urlopen(api_server))
return data.get("current_version", 'v1')
except Exception as e:
# TODO: report error
return None
def lookup_role_by_name(self, role_name, notify=True):
"""
Find a role by name
"""
role_name = urlquote(role_name)
try:
parts = role_name.split(".")
user_name = ".".join(parts[0:-1])
role_name = parts[-1]
if notify:
self.galaxy.display.display("- downloading role '%s', owned by %s" % (role_name, user_name))
except:
raise AnsibleError("- invalid role name (%s). Specify role as format: username.rolename" % role_name)
url = '%s/roles/?owner__username=%s&name=%s' % (self.baseurl, user_name, role_name)
self.galaxy.display.vvvv("- %s" % (url))
try:
data = json.load(urlopen(url))
if len(data["results"]) != 0:
return data["results"][0]
except:
# TODO: report on connection/availability errors
pass
return None
def fetch_role_related(self, related, role_id):
"""
Fetch the list of related items for the given role.
The url comes from the 'related' field of the role.
"""
try:
url = '%s/roles/%d/%s/?page_size=50' % (self.baseurl, int(role_id), related)
data = json.load(urlopen(url))
results = data['results']
done = (data.get('next', None) == None)
while not done:
url = '%s%s' % (self.baseurl, data['next'])
self.galaxy.display.display(url)
data = json.load(urlopen(url))
results += data['results']
done = (data.get('next', None) == None)
return results
except:
return None
def get_list(self, what):
"""
Fetch the list of items specified.
"""
try:
url = '%s/%s/?page_size' % (self.baseurl, what)
data = json.load(urlopen(url))
if "results" in data:
results = data['results']
else:
results = data
done = True
if "next" in data:
done = (data.get('next', None) == None)
while not done:
url = '%s%s' % (self.baseurl, data['next'])
self.galaxy.display.display(url)
data = json.load(urlopen(url))
results += data['results']
done = (data.get('next', None) == None)
return results
except Exception as error:
raise AnsibleError("Failed to download the %s list: %s" % (what, str(error)))
galaxy_info:
author: {{ author }}
description: {{description}}
company: {{ company }}
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: {{ issue_tracker_url }}
# Some suggested licenses:
# - BSD (default)
# - MIT
# - GPLv2
# - GPLv3
# - Apache
# - CC-BY
license: {{ license }}
min_ansible_version: {{ min_ansible_version }}
#
# Below are all platforms currently available. Just uncomment
# the ones that apply to your role. If you don't see your
# platform on this list, let us know and we'll get it added!
#
#platforms:
{%- for platform,versions in platforms.iteritems() %}
#- name: {{ platform }}
# versions:
# - all
{%- for version in versions %}
# - {{ version }}
{%- endfor %}
{%- endfor %}
#
# Below are all categories currently available. Just as with
# the platforms above, uncomment those that apply to your role.
#
#categories:
{%- for category in categories %}
#- {{ category.name }}
{%- endfor %}
dependencies: []
# List your role dependencies here, one per line.
# Be sure to remove the '[]' above if you add dependencies
# to this list.
{% for dependency in dependencies %}
#- {{ dependency }}
{% endfor %}
Role Name
=========
A brief description of the role goes here.
Requirements
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
Role Variables
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
Dependencies
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
......@@ -203,7 +203,7 @@ class VaultEditor(object):
_, tmp_path = tempfile.mkstemp()
if existing_data:
self.write_data(data, tmp_path)
self.write_data(existing_data, tmp_path)
# drop the user into an editor on the tmp file
call(self._editor_shell_command(tmp_path))
......
......@@ -37,6 +37,8 @@ class CallbackModule(CallbackBase):
pass
def v2_runner_on_failed(self, result, ignore_errors=False):
if 'exception' in result._result and self._display.verbosity < 3:
del result._result['exception']
self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), json.dumps(result._result, ensure_ascii=False)), color='red')
def v2_runner_on_ok(self, result):
......
......@@ -38,6 +38,8 @@ class CallbackModule(CallbackBase):
pass
def v2_runner_on_failed(self, result, ignore_errors=False):
if 'exception' in result._result and self._display.verbosity < 3:
del result._result['exception']
self._display.display("%s | FAILED! => %s" % (result._host.get_name(), result._result), color='red')
def v2_runner_on_ok(self, result):
......
......@@ -28,6 +28,7 @@ import getpass
from ansible import __version__
from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.utils.unicode import to_bytes
# FIXME: documentation for methods here, which have mostly been
......@@ -40,11 +41,154 @@ class SortedOptParser(optparse.OptionParser):
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None)
def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False,
#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):
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):
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):
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 any ansible script '''
parser = SortedOptParser(usage, version=version("%prog"))
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)
......@@ -144,7 +288,8 @@ def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False,
return parser
def version(prog):
@staticmethod
def version(prog):
result = "{0} {1}".format(prog, __version__)
gitinfo = _gitinfo()
if gitinfo:
......@@ -152,7 +297,8 @@ def version(prog):
result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
return result
def version_info(gitinfo=False):
@staticmethod
def version_info(gitinfo=False):
if gitinfo:
# expensive call, user with care
ansible_version_string = version('')
......@@ -234,69 +380,3 @@ def _gitinfo():
result += "\n {0}: {1}".format(submodule_path, submodule_info)
f.close()
return result
def ask_passwords(options):
sshpass = None
becomepass = None
vaultpass = None
become_prompt = ''
if options.ask_pass:
sshpass = getpass.getpass(prompt="SSH password: ")
become_prompt = "%s password[defaults to SSH password]: " % options.become_method.upper()
if sshpass:
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
else:
become_prompt = "%s password: " % options.become_method.upper()
if options.become_ask_pass:
becomepass = getpass.getpass(prompt=become_prompt)
if options.ask_pass and becomepass == '':
becomepass = sshpass
if becomepass:
becomepass = to_bytes(becomepass)
if options.ask_vault_pass:
vaultpass = getpass.getpass(prompt="Vault password: ")
if vaultpass:
vaultpass = to_bytes(vaultpass, errors='strict', nonstring='simplerepr').strip()
return (sshpass, becomepass, vaultpass)
def normalize_become_options(options):
''' this keeps backwards compatibility with sudo/su options '''
options.become_ask_pass = options.become_ask_pass or options.ask_sudo_pass or options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
options.become_user = options.become_user or options.sudo_user or options.su_user or C.DEFAULT_BECOME_USER
if options.become:
pass
elif options.sudo:
options.become = True
options.become_method = 'sudo'
elif options.su:
options.become = True
options.become_method = 'su'
def validate_conflicts(parser, options):
# Check for vault related conflicts
if (options.ask_vault_pass and options.vault_password_file):
parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
# Check for privilege escalation conflicts
if (options.su or options.su_user or options.ask_su_pass) and \
(options.sudo or options.sudo_user or options.ask_sudo_pass) or \
(options.su or options.su_user or options.ask_su_pass) and \
(options.become or options.become_user or options.become_ask_pass) or \
(options.sudo or options.sudo_user or options.ask_sudo_pass) and \
(options.become or options.become_user or options.become_ask_pass):
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")
......@@ -23,7 +23,7 @@ import textwrap
import sys
from ansible import constants as C
from ansible.errors import *
from ansible.errors import AnsibleError
from ansible.utils.color import stringc
class Display:
......@@ -35,6 +35,7 @@ class Display:
# list of all deprecation messages to prevent duplicate display
self._deprecations = {}
self._warns = {}
self._errors = {}
def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False):
msg2 = msg
......@@ -83,7 +84,7 @@ class Display:
if host is None:
self.display(msg, color='blue')
else:
self.display("<%s> %s" % (host, msg), color='blue')
self.display("<%s> %s" % (host, msg), color='blue', screen_only=True)
def deprecated(self, msg, version, removed=False):
''' used to print out a deprecation message.'''
......@@ -130,3 +131,12 @@ class Display:
star_len = 3
stars = "*" * star_len
self.display("\n%s %s" % (msg, stars), color=color)
def error(self, msg):
new_msg = "\n[ERROR]: %s" % msg
wrapped = textwrap.wrap(new_msg, 79)
new_msg = "\n".join(wrapped) + "\n"
if new_msg not in self._errors:
self.display(new_msg, color='red', stderr=True)
self._errors[new_msg] = 1
......@@ -40,28 +40,20 @@ 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.cli import base_parser, validate_conflicts, normalize_become_options, ask_passwords
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 Cli(object):
''' code behind bin/ansible '''
def __init__(self, display=None):
if display is None:
self.display = Display()
else:
self.display = display
class AdHocCli(CLI):
''' code behind ansible ad-hoc cli'''
def parse(self):
''' create an options parser for bin/ansible '''
parser = base_parser(
self.parser = CLI.base_parser(
usage='%prog <host-pattern> [options]',
runas_opts=True,
async_opts=True,
......@@ -71,102 +63,110 @@ class Cli(object):
)
# options unique to ansible ad-hoc
parser.add_option('-a', '--args', dest='module_args',
self.parser.add_option('-a', '--args', dest='module_args',
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option('-m', '--module-name', dest='module_name',
self.parser.add_option('-m', '--module-name', dest='module_name',
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
default=C.DEFAULT_MODULE_NAME)
options, args = parser.parse_args()
self.options, self.args = self.parser.parse_args()
if len(args) == 0 or len(args) > 1:
parser.print_help()
if len(self.args) != 1:
self.parser.print_help()
sys.exit(1)
display.verbosity = options.verbosity
validate_conflicts(parser,options)
self.display.verbosity = self.options.verbosity
self.validate_conflicts()
return (options, args)
return True
# ----------------------------------------------
def run(self, options, args):
def run(self):
''' use Runner lib to do SSH things '''
pattern = args[0]
# only thing left should be host pattern
pattern = self.args[0]
if options.connection == "local":
options.ask_pass = False
# ignore connection password cause we are local
if self.options.connection == "local":
self.options.ask_pass = False
sshpass = None
becomepass = None
vault_pass = None
normalize_become_options(options)
(sshpass, becomepass, vault_pass) = ask_passwords(options)
self.normalize_become_options()
(sshpass, becomepass) = self.ask_passwords()
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
if options.vault_password_file:
if self.options.vault_password_file:
# read vault_pass from a file
vault_pass = read_vault_file(options.vault_password_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=options.inventory)
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
hosts = inventory.list_hosts(pattern)
if len(hosts) == 0:
d = Display()
d.warning("provided hosts list is empty, only localhost is available")
self.display.warning("provided hosts list is empty, only localhost is available")
if options.listhosts:
if self.options.listhosts:
for host in hosts:
self.display.display(' %s' % host.name)
sys.exit(0)
return 0
if ((options.module_name == 'command' or options.module_name == 'shell') and not options.module_args):
raise AnsibleError("No argument passed to %s module" % options.module_name)
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)
# FIXME: async support needed
#if options.seconds:
#TODO: implement async support
#if self.options.seconds:
# callbacks.display("background launch...\n\n", color='cyan')
# results, poller = runner.run_async(options.seconds)
# results = self.poll_while_needed(poller, options)
# 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=options.module_name, args=parse_kv(options.module_args))),
]
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
try:
display = Display()
tqm = TaskQueueManager(inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=passwords, stdout_callback='minimal')
tqm = TaskQueueManager(
inventory=inventory,
callback='minimal',
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()
except AnsibleError:
tqm.cleanup()
raise
return result
# ----------------------------------------------
def poll_while_needed(self, poller, options):
def poll_while_needed(self, poller):
''' summarize results from Runner '''
# BACKGROUND POLL LOGIC when -B and -P are specified
if options.seconds and options.poll_interval > 0:
poller.wait(options.seconds, options.poll_interval)
if self.options.seconds and self.options.poll_interval > 0:
poller.wait(self.options.seconds, self.options.poll_interval)
return poller.results
......@@ -176,15 +176,17 @@ class Cli(object):
if __name__ == '__main__':
display = Display()
#display.display(" ".join(sys.argv))
try:
cli = Cli(display=display)
(options, args) = cli.parse()
sys.exit(cli.run(options, args))
except AnsibleError as e:
display.display("[ERROR]: %s" % e, color='red', stderr=True)
cli = AdHocCli(sys.argv, display=display)
cli.parse()
sys.exit(cli.run())
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.display("[ERROR]: interrupted", color='red', stderr=True)
sys.exit(1)
display.error("interrupted")
sys.exit(4)
......@@ -44,7 +44,7 @@ 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 base_parser, validate_conflicts, normalize_become_options, ask_passwords
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,11 +53,13 @@ from ansible.vars import VariableManager
#---------------------------------------------------------------------------------------------------
def main(display, args):
''' run ansible-playbook operations '''
class PlaybookCLI(CLI):
''' code behind ansible playbook cli'''
def parse(self):
# create parser for CLI options
parser = base_parser(
parser = CLI.base_parser(
usage = "%prog playbook.yml",
connect_opts=True,
meta_opts=True,
......@@ -77,35 +79,42 @@ def main(display, args):
parser.add_option('--list-tags', dest='listtags', action='store_true',
help="list all available tags")
options, args = parser.parse_args(args)
self.options, self.args = parser.parse_args()
if len(args) == 0:
if len(self.args) == 0:
parser.print_help(file=sys.stderr)
return 1
raise AnsibleError("You must specify a playbook file to run")
self.parser = parser
self.display.verbosity = self.options.verbosity
self.validate_conflicts()
display.verbosity = options.verbosity
validate_conflicts(parser,options)
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 when we don't need to
if not options.listhosts and not options.listtasks and not options.listtags:
normalize_become_options(options)
(sshpass, becomepass, vault_pass) = ask_passwords(options)
# 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 options.vault_password_file:
if self.options.vault_password_file:
# read vault_pass from a file
vault_pass = read_vault_file(options.vault_password_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 options.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)
......@@ -119,14 +128,14 @@ def main(display, args):
extra_vars = combine_vars(extra_vars, data)
# FIXME: this should be moved inside the playbook executor code
only_tags = options.tags.split(",")
skip_tags = options.skip_tags
if options.skip_tags is not None:
skip_tags = options.skip_tags.split(",")
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 args:
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)):
......@@ -138,7 +147,7 @@ def main(display, args):
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=options.inventory)
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
......@@ -150,49 +159,56 @@ def main(display, args):
no_hosts = False
if len(inventory.list_hosts()) == 0:
# Empty inventory
display.warning("provided hosts list is empty, only localhost is available")
self.display.warning("provided hosts list is empty, only localhost is available")
no_hosts = True
inventory.subset(options.subset)
inventory.subset(self.options.subset)
if len(inventory.list_hosts()) == 0 and no_hosts is False:
# Invalid limit
raise errors.AnsibleError("Specified --limit does not match any hosts")
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=args, inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=passwords)
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:
display.display('\nplaybook: %s\n' % p['playbook'])
self.display.display('\nplaybook: %s\n' % p['playbook'])
for play in p['plays']:
if options.listhosts:
display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
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']:
display.display(" %s" % host)
if options.listtasks: #TODO: do we want to display block info?
display.display("\n %s" % (play['name']))
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']:
display.display(" %s" % task)
if options.listtags: #TODO: fix once we figure out block handling above
display.display("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
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']:
display.display(" %s" % tag)
self.display.display(" %s" % tag)
return 0
else:
return results
########################################################
if __name__ == "__main__":
display = Display()
#display.display(" ".join(sys.argv), log_only=True)
try:
sys.exit(main(display, sys.argv[1:]))
except AnsibleError as e:
display.display("[ERROR]: %s" % e, color='red', stderr=True)
cli = PlaybookCLI(sys.argv, display=display)
cli.parse()
sys.exit(cli.run())
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.display("[ERROR]: interrupted", color='red', stderr=True)
sys.exit(1)
display.error("interrupted")
sys.exit(4)
......@@ -33,143 +33,102 @@ import os
import sys
import traceback
from ansible.errors import AnsibleError
from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.parsing.vault import VaultEditor
from ansible.utils.cli import base_parser, ask_vault_passwords
from ansible.utils.cli import CLI
from ansible.utils.display import Display
#-------------------------------------------------------------------------------------
# Utility functions for parsing actions/options
#-------------------------------------------------------------------------------------
class Cli(object):
class VaultCli(CLI):
""" Vault command line class """
VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view")
CIPHER = 'AES256'
def __init__(self, display=None):
def __init__(self, args, display=None):
self.vault_pass = None
if display is None:
self.display = Display()
else:
self.display = display
super(VaultCli, self).__init__(args, display)
def parse(self):
# create parser for CLI options
parser = base_parser(
self.parser = CLI.base_parser(
usage = "%prog vaultfile.yml",
)
return parser.parse_args()
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")
def run(self, options, args):
self.options, self.args = self.parser.parse_args()
action = self.get_action(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")
if not action:
parser.print_help()
raise AnsibleError("missing required action")
def run(self):
# options specific to actions
if action == "create":
parser.set_usage("usage: %prog create [options] file_name")
elif action == "decrypt":
parser.set_usage("usage: %prog decrypt [options] file_name")
elif action == "edit":
parser.set_usage("usage: %prog edit [options] file_name")
elif action == "view":
parser.set_usage("usage: %prog view [options] file_name")
elif action == "encrypt":
parser.set_usage("usage: %prog encrypt [options] file_name")
elif action == "rekey":
parser.set_usage("usage: %prog rekey [options] file_name")
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)
if len(args) == 0 or len(args) > 1:
parser.print_help()
raise AnsibleError("Vault requires a single filename as a parameter")
self.execute()
if options.vault_password_file:
# read vault_pass from a file
self.vault_pass = read_vault_file(options.vault_password_file)
else:
self.vault_pass, _= ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
# execute the desired action
fn = getattr(self, "execute_%s" % action)
fn(args, options)
def get_action(self, args):
"""
Get the action the user wants to execute from the
sys argv list.
"""
for i in range(0,len(args)):
arg = args[i]
if arg in VALID_ACTIONS:
del args[i]
return arg
return None
def execute_create(args, options):
cipher = 'AES256'
if hasattr(options, 'cipher'):
cipher = options.cipher
this_editor = VaultEditor(cipher, self.vault_pass, args[0])
this_editor.create_file()
def execute_create(self):
def execute_decrypt(args, options):
cipher = getattr(self.options, 'cipher', self.CIPHER)
this_editor = VaultEditor(cipher, self.vault_pass, self.args[0])
this_editor.create_file()
cipher = 'AES256'
if hasattr(options, 'cipher'):
cipher = options.cipher
def execute_decrypt(self):
for f in args:
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(args, options):
cipher = None
def execute_edit(self):
for f in args:
this_editor = VaultEditor(cipher, self.vault_pass, f)
for f in self.args:
this_editor = VaultEditor(None, self.vault_pass, f)
this_editor.edit_file()
def execute_view(args, options):
cipher = None
def execute_view(self):
for f in args:
this_editor = VaultEditor(cipher, self.vault_pass, f)
for f in self.args:
this_editor = VaultEditor(None, self.vault_pass, f)
this_editor.view_file()
def execute_encrypt(args, options):
def execute_encrypt(self):
cipher = 'AES256'
if hasattr(options, 'cipher'):
cipher = options.cipher
for f in args:
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(args, options ):
__, new_password = ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
def execute_rekey(self):
__, new_password = self.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
cipher = None
for f in args:
this_editor = VaultEditor(cipher, self.vault_pass, f)
for f in self.args:
this_editor = VaultEditor(None, self.vault_pass, f)
this_editor.rekey_file(new_password)
self.display.display("Rekey successful")
......@@ -179,15 +138,17 @@ class Cli(object):
if __name__ == "__main__":
display = Display()
#display.display(" ".join(sys.argv), log_only=True)
try:
cli = Cli(display=display)
(options, args) = cli.parse()
sys.exit(cli.run(options, args))
except AnsibleError as e:
display.display("[ERROR]: %s" % e, color='red', stderr=True)
cli = VaultCli(sys.argv, display=display)
cli.parse()
sys.exit(cli.run())
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.display("[ERROR]: interrupted", color='red', stderr=True)
sys.exit(1)
display.error("interrupted")
sys.exit(4)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment