Commit cdefeb6d by Brian Coca

refactored most binaries

added AnsibleOptionsError
removed pulicate parser error class
parent 900b992b
...@@ -40,13 +40,15 @@ def get_config(p, section, key, env_var, default, boolean=False, integer=False, ...@@ -40,13 +40,15 @@ def get_config(p, section, key, env_var, default, boolean=False, integer=False,
''' return a configuration variable with casting ''' ''' return a configuration variable with casting '''
value = _get_config(p, section, key, env_var, default) value = _get_config(p, section, key, env_var, default)
if boolean: if boolean:
return mk_boolean(value) value = mk_boolean(value)
if value and integer: if value:
return int(value) if integer:
if value and floating: value = int(value)
return float(value) if floating:
if value and islist: value = float(value)
return [x.strip() for x in value.split(',')] if islist:
if isinstance(value, basestring):
value = [x.strip() for x in value.split(',')]
return value return value
def _get_config(p, section, key, env_var, default): def _get_config(p, section, key, env_var, default):
...@@ -104,7 +106,7 @@ DEFAULTS='defaults' ...@@ -104,7 +106,7 @@ DEFAULTS='defaults'
# configurable things # configurable things
DEFAULT_DEBUG = get_config(p, DEFAULTS, 'debug', 'ANSIBLE_DEBUG', False, boolean=True) 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_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_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') DEFAULT_REMOTE_TMP = get_config(p, DEFAULTS, 'remote_tmp', 'ANSIBLE_REMOTE_TEMP', '$HOME/.ansible/tmp')
...@@ -212,6 +214,7 @@ GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY ...@@ -212,6 +214,7 @@ GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_" DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
# non-configurable things # non-configurable things
MODULE_REQUIRE_ARGS = ['command', 'shell', 'raw', 'script']
DEFAULT_BECOME_PASS = None DEFAULT_BECOME_PASS = None
DEFAULT_SUDO_PASS = None DEFAULT_SUDO_PASS = None
DEFAULT_REMOTE_PASS = None DEFAULT_REMOTE_PASS = None
......
...@@ -140,6 +140,10 @@ class AnsibleError(Exception): ...@@ -140,6 +140,10 @@ class AnsibleError(Exception):
return error_message return error_message
class AnsibleOptionsError(AnsibleError):
''' bad or incomplete options passed '''
pass
class AnsibleParserError(AnsibleError): class AnsibleParserError(AnsibleError):
''' something was detected early that is wrong about a playbook or data file ''' ''' something was detected early that is wrong about a playbook or data file '''
pass pass
...@@ -164,6 +168,14 @@ class AnsibleFilterError(AnsibleRuntimeError): ...@@ -164,6 +168,14 @@ class AnsibleFilterError(AnsibleRuntimeError):
''' a templating failure ''' ''' a templating failure '''
pass pass
class AnsibleLookupError(AnsibleRuntimeError):
''' a lookup failure '''
pass
class AnsibleCallbackError(AnsibleRuntimeError):
''' a callback failure '''
pass
class AnsibleUndefinedVariable(AnsibleRuntimeError): class AnsibleUndefinedVariable(AnsibleRuntimeError):
''' a templating failure ''' ''' a templating failure '''
pass pass
...@@ -171,7 +183,3 @@ class AnsibleUndefinedVariable(AnsibleRuntimeError): ...@@ -171,7 +183,3 @@ class AnsibleUndefinedVariable(AnsibleRuntimeError):
class AnsibleFileNotFound(AnsibleRuntimeError): class AnsibleFileNotFound(AnsibleRuntimeError):
''' a file missing failure ''' ''' a file missing failure '''
pass pass
class AnsibleParserError(AnsibleRuntimeError):
''' a parser error '''
pass
...@@ -36,6 +36,8 @@ class GalaxyRole(object): ...@@ -36,6 +36,8 @@ class GalaxyRole(object):
SUPPORTED_SCMS = set(['git', 'hg']) SUPPORTED_SCMS = set(['git', 'hg'])
META_MAIN = os.path.join('meta', 'main.yml') META_MAIN = os.path.join('meta', 'main.yml')
META_INSTALL = os.path.join('meta', '.galaxy_install_info') META_INSTALL = os.path.join('meta', '.galaxy_install_info')
ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars')
def __init__(self, galaxy, role_name, role_version=None, role_url=None): def __init__(self, galaxy, role_name, role_version=None, role_url=None):
...@@ -45,13 +47,13 @@ class GalaxyRole(object): ...@@ -45,13 +47,13 @@ class GalaxyRole(object):
self.name = role_name self.name = role_name
self.meta_data = None self.meta_data = None
self.install_info = None self.install_info = None
self.role_path = (os.path.join(self.roles_path, self.name)) self.path = (os.path.join(galaxy.roles_path, self.name))
# TODO: possibly parse version and url from role_name # TODO: possibly parse version and url from role_name
self.version = role_version self.version = role_version
self.url = role_url self.url = role_url
if self.url is None and '://' in self.name: if self.url is None:
self.url = self.name self._spec_parse()
if C.GALAXY_SCMS: if C.GALAXY_SCMS:
self.scms = self.SUPPORTED_SCMS.intersection(set(C.GALAXY_SCMS)) self.scms = self.SUPPORTED_SCMS.intersection(set(C.GALAXY_SCMS))
...@@ -62,7 +64,7 @@ class GalaxyRole(object): ...@@ -62,7 +64,7 @@ class GalaxyRole(object):
self.display.warning("No valid SCMs configured for Galaxy.") self.display.warning("No valid SCMs configured for Galaxy.")
def fetch_from_scm_archive(self, scm, role_url, role_version): def fetch_from_scm_archive(self):
# this can be configured to prevent unwanted SCMS but cannot add new ones unless the code is also updated # this can be configured to prevent unwanted SCMS but cannot add new ones unless the code is also updated
if scm not in self.scms: if scm not in self.scms:
...@@ -111,12 +113,21 @@ class GalaxyRole(object): ...@@ -111,12 +113,21 @@ class GalaxyRole(object):
return temp_file.name return temp_file.name
def get_metadata(self):
"""
Returns role metadata
"""
if self.meta_data is None:
self._read_metadata
return self.meta_data
def read_metadata(self): def _read_metadata(self):
""" """
Reads the metadata as YAML, if the file 'meta/main.yml' exists Reads the metadata as YAML, if the file 'meta/main.yml' exists
""" """
meta_path = os.path.join(self.role_path, self.META_MAIN) meta_path = os.path.join(self.path, self.META_MAIN)
if os.path.isfile(meta_path): if os.path.isfile(meta_path):
try: try:
f = open(meta_path, 'r') f = open(meta_path, 'r')
...@@ -127,15 +138,24 @@ class GalaxyRole(object): ...@@ -127,15 +138,24 @@ class GalaxyRole(object):
finally: finally:
f.close() f.close()
return True
def read_galaxy_install_info(self): def get_galaxy_install_info(self):
"""
Returns role install info
"""
if self.install_info is None:
self._read_galaxy_isntall_info()
return self.install_info
def _read_galaxy_install_info(self):
""" """
Returns the YAML data contained in 'meta/.galaxy_install_info', Returns the YAML data contained in 'meta/.galaxy_install_info',
if it exists. if it exists.
""" """
info_path = os.path.join(self.role_path, self.META_INSTALL) info_path = os.path.join(self.path, self.META_INSTALL)
if os.path.isfile(info_path): if os.path.isfile(info_path):
try: try:
f = open(info_path, 'r') f = open(info_path, 'r')
...@@ -146,9 +166,7 @@ class GalaxyRole(object): ...@@ -146,9 +166,7 @@ class GalaxyRole(object):
finally: finally:
f.close() f.close()
return True def _write_galaxy_install_info(self):
def write_galaxy_install_info(self):
""" """
Writes a YAML-formatted file to the role's meta/ directory Writes a YAML-formatted file to the role's meta/ directory
(named .galaxy_install_info) which contains some information (named .galaxy_install_info) which contains some information
...@@ -159,7 +177,7 @@ class GalaxyRole(object): ...@@ -159,7 +177,7 @@ class GalaxyRole(object):
version=self.version, version=self.version,
install_date=datetime.datetime.utcnow().strftime("%c"), install_date=datetime.datetime.utcnow().strftime("%c"),
) )
info_path = os.path.join(self.role_path, self.META_INSTALL) info_path = os.path.join(self.path, self.META_INSTALL)
try: try:
f = open(info_path, 'w+') f = open(info_path, 'w+')
self.install_info = yaml.safe_dump(info, f) self.install_info = yaml.safe_dump(info, f)
...@@ -178,7 +196,7 @@ class GalaxyRole(object): ...@@ -178,7 +196,7 @@ class GalaxyRole(object):
""" """
if self.read_metadata(): if self.read_metadata():
try: try:
rmtree(self.role_path) rmtree(self.path)
return True return True
except: except:
pass pass
...@@ -213,7 +231,7 @@ class GalaxyRole(object): ...@@ -213,7 +231,7 @@ class GalaxyRole(object):
self.display.error("failed to download the file.") self.display.error("failed to download the file.")
return False return False
def install(self, role_version, role_filename): def install(self, role_filename):
# the file is a tar, so open it that way and extract it # the file is a tar, so open it that way and extract it
# to the specified (or default) roles directory # to the specified (or default) roles directory
...@@ -246,10 +264,10 @@ class GalaxyRole(object): ...@@ -246,10 +264,10 @@ class GalaxyRole(object):
# we strip off the top-level directory for all of the files contained within # we strip off the top-level directory for all of the files contained within
# the tar file here, since the default is 'github_repo-target', and change it # the tar file here, since the default is 'github_repo-target', and change it
# to the specified role's name # to the specified role's name
self.display.display("- extracting %s to %s" % (self.name, self.role_path)) self.display.display("- extracting %s to %s" % (self.name, self.path))
try: try:
if os.path.exists(self.role_path): if os.path.exists(self.path):
if not os.path.isdir(self.role_path): if not os.path.isdir(self.path):
self.display.error("the specified roles path exists and is not a directory.") self.display.error("the specified roles path exists and is not a directory.")
return False return False
elif not getattr(self.options, "force", False): elif not getattr(self.options, "force", False):
...@@ -258,13 +276,13 @@ class GalaxyRole(object): ...@@ -258,13 +276,13 @@ class GalaxyRole(object):
else: else:
# using --force, remove the old path # using --force, remove the old path
if not self.remove(): if not self.remove():
self.display.error("%s doesn't appear to contain a role." % self.role_path) self.display.error("%s doesn't appear to contain a role." % self.path)
self.display.error(" please remove this directory manually if you really want to put the role here.") self.display.error(" please remove this directory manually if you really want to put the role here.")
return False return False
else: else:
os.makedirs(self.role_path) os.makedirs(self.path)
# now we do the actual extraction to the role_path # now we do the actual extraction to the path
for member in members: for member in members:
# we only extract files, and remove any relative path # we only extract files, and remove any relative path
# bits that might be in the file for security purposes # bits that might be in the file for security purposes
...@@ -276,15 +294,62 @@ class GalaxyRole(object): ...@@ -276,15 +294,62 @@ class GalaxyRole(object):
if part != '..' and '~' not in part and '$' not in part: if part != '..' and '~' not in part and '$' not in part:
final_parts.append(part) final_parts.append(part)
member.name = os.path.join(*final_parts) member.name = os.path.join(*final_parts)
role_tar_file.extract(member, self.role_path) role_tar_file.extract(member, self.path)
# write out the install info file for later use # write out the install info file for later use
self.version = role_version self._write_galaxy_install_info()
self.write_galaxy_install_info()
except OSError as e: except OSError as e:
self.display.error("Could not update files in %s: %s" % (self.role_path, str(e))) self.display.error("Could not update files in %s: %s" % (self.path, str(e)))
return False return False
# return the parsed yaml metadata # return the parsed yaml metadata
self.display.display("- %s was installed successfully" % self.role_name) self.display.display("- %s was installed successfully" % self.name)
return True return True
def get_spec(self):
"""
Returns role spec info
{
'scm': 'git',
'src': 'http://git.example.com/repos/repo.git',
'version': 'v1.0',
'name': 'repo'
}
"""
if self.scm is None and self.url is None:
self._read_galaxy_isntall_info()
return dict(scm=self.scm, src=self.url, version=self.version, role_name=self.name)
def _spec_parse(self):
''' creates separated parts of role spec '''
default_role_versions = dict(git='master', hg='tip')
if not self.url and '://' in self.name:
role_spec = self.name.strip()
if role_spec == "" or role_spec.startswith("#"):
return
tokens = [s.strip() for s in role_spec.split(',')]
# assume https://github.com URLs are git+https:// URLs and not tarballs unless they end in '.zip'
if 'github.com/' in tokens[0] and not tokens[0].startswith("git+") and not tokens[0].endswith('.tar.gz'):
tokens[0] = 'git+' + tokens[0]
if '+' in tokens[0]:
(self.scm, self.url) = tokens[0].split('+')
else:
self.scm = None
self.url = tokens[0]
if len(tokens) >= 2:
self.version = tokens[1]
if len(tokens) == 3:
self.name = tokens[2]
else:
self.name = self._repo_url_to_role_name(tokens[0])
if self.scm and not self.version:
self.version = default_role_versions.get(scm, '')
...@@ -28,6 +28,7 @@ import getpass ...@@ -28,6 +28,7 @@ import getpass
from ansible import __version__ from ansible import __version__
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.utils.unicode import to_bytes from ansible.utils.unicode import to_bytes
# FIXME: documentation for methods here, which have mostly been # FIXME: documentation for methods here, which have mostly been
...@@ -40,11 +41,154 @@ class SortedOptParser(optparse.OptionParser): ...@@ -40,11 +41,154 @@ class SortedOptParser(optparse.OptionParser):
self.option_list.sort(key=operator.methodcaller('get_opt_string')) self.option_list.sort(key=operator.methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None) 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): async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False):
''' create an options parser for any ansible script ''' ''' 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', parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
help='connect as this user (default=%s)' % C.DEFAULT_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, ...@@ -144,7 +288,8 @@ def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False,
return parser return parser
def version(prog): @staticmethod
def version(prog):
result = "{0} {1}".format(prog, __version__) result = "{0} {1}".format(prog, __version__)
gitinfo = _gitinfo() gitinfo = _gitinfo()
if gitinfo: if gitinfo:
...@@ -152,7 +297,8 @@ def version(prog): ...@@ -152,7 +297,8 @@ def version(prog):
result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
return result return result
def version_info(gitinfo=False): @staticmethod
def version_info(gitinfo=False):
if gitinfo: if gitinfo:
# expensive call, user with care # expensive call, user with care
ansible_version_string = version('') ansible_version_string = version('')
...@@ -234,69 +380,3 @@ def _gitinfo(): ...@@ -234,69 +380,3 @@ def _gitinfo():
result += "\n {0}: {1}".format(submodule_path, submodule_info) result += "\n {0}: {1}".format(submodule_path, submodule_info)
f.close() f.close()
return result 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")
...@@ -40,28 +40,20 @@ from ansible.inventory import Inventory ...@@ -40,28 +40,20 @@ from ansible.inventory import Inventory
from ansible.parsing import DataLoader from ansible.parsing import DataLoader
from ansible.parsing.splitter import parse_kv from ansible.parsing.splitter import parse_kv
from ansible.playbook.play import Play from ansible.playbook.play import Play
from ansible.utils.display import Display from ansible.utils.cli import CLI
from ansible.utils.cli import base_parser, validate_conflicts, normalize_become_options, ask_passwords
from ansible.utils.display import Display from ansible.utils.display import Display
from ansible.utils.vault import read_vault_file from ansible.utils.vault import read_vault_file
from ansible.vars import VariableManager from ansible.vars import VariableManager
######################################################## ########################################################
class Cli(object): class AdHocCli(CLI):
''' code behind bin/ansible ''' ''' code behind ansible ad-hoc cli'''
def __init__(self, display=None):
if display is None:
self.display = Display()
else:
self.display = display
def parse(self): def parse(self):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
parser = base_parser( self.parser = CLI.base_parser(
usage='%prog <host-pattern> [options]', usage='%prog <host-pattern> [options]',
runas_opts=True, runas_opts=True,
async_opts=True, async_opts=True,
...@@ -71,102 +63,110 @@ class Cli(object): ...@@ -71,102 +63,110 @@ class Cli(object):
) )
# options unique to ansible ad-hoc # 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) 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, help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
default=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: if len(self.args) != 1:
parser.print_help() self.parser.print_help()
sys.exit(1) sys.exit(1)
display.verbosity = options.verbosity self.display.verbosity = self.options.verbosity
validate_conflicts(parser,options) self.validate_conflicts()
return (options, args) return True
# ----------------------------------------------
def run(self, options, args): def run(self):
''' use Runner lib to do SSH things ''' ''' 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": # ignore connection password cause we are local
options.ask_pass = False if self.options.connection == "local":
self.options.ask_pass = False
sshpass = None sshpass = None
becomepass = None becomepass = None
vault_pass = None vault_pass = None
normalize_become_options(options) self.normalize_become_options()
(sshpass, becomepass, vault_pass) = ask_passwords(options) (sshpass, becomepass) = self.ask_passwords()
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass } passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
if options.vault_password_file: if self.options.vault_password_file:
# read vault_pass from a 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) loader = DataLoader(vault_password=vault_pass)
variable_manager = VariableManager() 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) hosts = inventory.list_hosts(pattern)
if len(hosts) == 0: if len(hosts) == 0:
d = Display() self.display.warning("provided hosts list is empty, only localhost is available")
d.warning("provided hosts list is empty, only localhost is available")
if options.listhosts: if self.options.listhosts:
for host in hosts: for host in hosts:
self.display.display(' %s' % host.name) 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): if self.options.module_name in C.MODULE_REQUIRE_ARGS and not self.options.module_args:
raise AnsibleError("No argument passed to %s module" % options.module_name) raise AnsibleError("No argument passed to %s module" % self.options.module_name)
# FIXME: async support needed #TODO: implement async support
#if options.seconds: #if self.options.seconds:
# callbacks.display("background launch...\n\n", color='cyan') # callbacks.display("background launch...\n\n", color='cyan')
# results, poller = runner.run_async(options.seconds) # results, poller = runner.run_async(self.options.seconds)
# results = self.poll_while_needed(poller, options) # results = self.poll_while_needed(poller)
#else: #else:
# results = runner.run() # results = runner.run()
# create a pseudo-play to execute the specified module via a single task # create a pseudo-play to execute the specified module via a single task
play_ds = dict( play_ds = dict(
name = "Ansible Ad-Hoc",
hosts = pattern, hosts = pattern,
gather_facts = 'no', gather_facts = 'no',
tasks = [ tasks = [ dict(action=dict(module=self.options.module_name, args=parse_kv(self.options.module_args))), ]
dict(action=dict(module=options.module_name, args=parse_kv(options.module_args))),
]
) )
play = Play().load(play_ds, variable_manager=variable_manager, loader=loader) play = Play().load(play_ds, variable_manager=variable_manager, loader=loader)
# now create a task queue manager to execute the play # now create a task queue manager to execute the play
try: try:
display = Display() tqm = TaskQueueManager(
tqm = TaskQueueManager(inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=passwords, stdout_callback='minimal') 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) result = tqm.run(play)
finally:
if tqm:
tqm.cleanup() tqm.cleanup()
except AnsibleError:
tqm.cleanup()
raise
return result return result
# ---------------------------------------------- # ----------------------------------------------
def poll_while_needed(self, poller, options): def poll_while_needed(self, poller):
''' summarize results from Runner ''' ''' summarize results from Runner '''
# BACKGROUND POLL LOGIC when -B and -P are specified # BACKGROUND POLL LOGIC when -B and -P are specified
if options.seconds and options.poll_interval > 0: if self.options.seconds and self.options.poll_interval > 0:
poller.wait(options.seconds, options.poll_interval) poller.wait(self.options.seconds, self.options.poll_interval)
return poller.results return poller.results
...@@ -176,14 +176,12 @@ class Cli(object): ...@@ -176,14 +176,12 @@ class Cli(object):
if __name__ == '__main__': if __name__ == '__main__':
display = Display() display = Display()
#display.display(" ".join(sys.argv))
try: try:
cli = Cli(display=display) cli = AdHocCli(sys.argv, display=display)
(options, args) = cli.parse() cli.parse()
sys.exit(cli.run(options, args)) sys.exit(cli.run())
except AnsibleError as e: except AnsibleError as e:
display.error(str(e)) display.display(str(e), stderr=True, color='red')
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
display.error("interrupted") display.error("interrupted")
......
...@@ -44,7 +44,7 @@ from ansible.parsing import DataLoader ...@@ -44,7 +44,7 @@ from ansible.parsing import DataLoader
from ansible.parsing.splitter import parse_kv from ansible.parsing.splitter import parse_kv
from ansible.playbook import Playbook from ansible.playbook import Playbook
from ansible.playbook.task import Task 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.display import Display
from ansible.utils.unicode import to_unicode from ansible.utils.unicode import to_unicode
from ansible.utils.vars import combine_vars from ansible.utils.vars import combine_vars
...@@ -53,11 +53,13 @@ from ansible.vars import VariableManager ...@@ -53,11 +53,13 @@ from ansible.vars import VariableManager
#--------------------------------------------------------------------------------------------------- #---------------------------------------------------------------------------------------------------
def main(display, args): class PlaybookCLI(CLI):
''' run ansible-playbook operations ''' ''' code behind ansible playbook cli'''
def parse(self):
# create parser for CLI options # create parser for CLI options
parser = base_parser( parser = CLI.base_parser(
usage = "%prog playbook.yml", usage = "%prog playbook.yml",
connect_opts=True, connect_opts=True,
meta_opts=True, meta_opts=True,
...@@ -77,35 +79,42 @@ def main(display, args): ...@@ -77,35 +79,42 @@ def main(display, args):
parser.add_option('--list-tags', dest='listtags', action='store_true', parser.add_option('--list-tags', dest='listtags', action='store_true',
help="list all available tags") 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) 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 def run(self):
validate_conflicts(parser,options)
# Note: slightly wrong, this is written so that implicit localhost # Note: slightly wrong, this is written so that implicit localhost
# Manage passwords # Manage passwords
sshpass = None sshpass = None
becomepass = None becomepass = None
vault_pass = None vault_pass = None
passwords = {}
# don't deal with privilege escalation when we don't need to # don't deal with privilege escalation or passwords when we don't need to
if not options.listhosts and not options.listtasks and not options.listtags: if not self.options.listhosts and not self.options.listtasks and not self.options.listtags:
normalize_become_options(options) self.normalize_become_options()
(sshpass, becomepass, vault_pass) = ask_passwords(options) (sshpass, becomepass) = self.ask_passwords()
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass } passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
if options.vault_password_file: if self.options.vault_password_file:
# read vault_pass from a 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) loader = DataLoader(vault_password=vault_pass)
extra_vars = {} 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') extra_vars_opt = to_unicode(extra_vars_opt, errors='strict')
if extra_vars_opt.startswith(u"@"): if extra_vars_opt.startswith(u"@"):
# Argument is a YAML file (JSON is a subset of YAML) # Argument is a YAML file (JSON is a subset of YAML)
...@@ -119,14 +128,14 @@ def main(display, args): ...@@ -119,14 +128,14 @@ def main(display, args):
extra_vars = combine_vars(extra_vars, data) extra_vars = combine_vars(extra_vars, data)
# FIXME: this should be moved inside the playbook executor code # FIXME: this should be moved inside the playbook executor code
only_tags = options.tags.split(",") only_tags = self.options.tags.split(",")
skip_tags = options.skip_tags skip_tags = self.options.skip_tags
if options.skip_tags is not None: if self.options.skip_tags is not None:
skip_tags = options.skip_tags.split(",") skip_tags = self.ptions.skip_tags.split(",")
# initial error check, to make sure all specified playbooks are accessible # initial error check, to make sure all specified playbooks are accessible
# before we start running anything through the playbook executor # before we start running anything through the playbook executor
for playbook in args: for playbook in self.args:
if not os.path.exists(playbook): if not os.path.exists(playbook):
raise AnsibleError("the playbook: %s could not be found" % 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)): if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)):
...@@ -138,7 +147,7 @@ def main(display, args): ...@@ -138,7 +147,7 @@ def main(display, args):
variable_manager.set_extra_vars(extra_vars) variable_manager.set_extra_vars(extra_vars)
# create the inventory, and filter it based on the subset specified (if any) # 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) variable_manager.set_inventory(inventory)
# (which is not returned in list_hosts()) is taken into account for # (which is not returned in list_hosts()) is taken into account for
...@@ -150,48 +159,50 @@ def main(display, args): ...@@ -150,48 +159,50 @@ def main(display, args):
no_hosts = False no_hosts = False
if len(inventory.list_hosts()) == 0: if len(inventory.list_hosts()) == 0:
# Empty inventory # 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 no_hosts = True
inventory.subset(options.subset) inventory.subset(self.options.subset)
if len(inventory.list_hosts()) == 0 and no_hosts is False: if len(inventory.list_hosts()) == 0 and no_hosts is False:
# Invalid limit # 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 # 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() results = pbex.run()
if isinstance(results, list): if isinstance(results, list):
for p in results: for p in results:
display.display('\nplaybook: %s\n' % p['playbook']) self.display.display('\nplaybook: %s\n' % p['playbook'])
for play in p['plays']: for play in p['plays']:
if options.listhosts: if self.options.listhosts:
display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts']))) self.display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
for host in play['hosts']: for host in play['hosts']:
display.display(" %s" % host) self.display.display(" %s" % host)
if options.listtasks: #TODO: do we want to display block info? if self.options.listtasks: #TODO: do we want to display block info?
display.display("\n %s" % (play['name'])) self.display.display("\n %s" % (play['name']))
for task in play['tasks']: for task in play['tasks']:
display.display(" %s" % task) self.display.display(" %s" % task)
if options.listtags: #TODO: fix once we figure out block handling above if self.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("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
for tag in play['tags']: for tag in play['tags']:
display.display(" %s" % tag) self.display.display(" %s" % tag)
return 0 return 0
else: else:
return results return results
########################################################
if __name__ == "__main__": if __name__ == "__main__":
display = Display() display = Display()
#display.display(" ".join(sys.argv), log_only=True)
try: try:
sys.exit(main(display, sys.argv[1:])) cli = PlaybookCLI(sys.argv, display=display)
cli.parse()
sys.exit(cli.run())
except AnsibleError as e: except AnsibleError as e:
display.error(str(e)) display.display(str(e), stderr=True, color='red')
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
display.error("interrupted") display.error("interrupted")
......
...@@ -35,141 +35,100 @@ import traceback ...@@ -35,141 +35,100 @@ import traceback
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.parsing.vault import VaultEditor 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
#------------------------------------------------------------------------------------- class VaultCli(CLI):
# Utility functions for parsing actions/options """ Vault command line class """
#-------------------------------------------------------------------------------------
class Cli(object):
VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view") VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view")
CIPHER = 'AES256'
def __init__(self, args, display=None):
def __init__(self, display=None):
self.vault_pass = None self.vault_pass = None
super(VaultCli, self).__init__(args, display)
if display is None:
self.display = Display()
else:
self.display = display
def parse(self): def parse(self):
# create parser for CLI options # create parser for CLI options
parser = base_parser( self.parser = CLI.base_parser(
usage = "%prog vaultfile.yml", 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: def run(self):
parser.print_help()
raise AnsibleError("missing required action")
# options specific to actions if self.options.vault_password_file:
if action == "create": # read vault_pass from a file
parser.set_usage("usage: %prog create [options] file_name") self.vault_pass = read_vault_file(self.options.vault_password_file)
elif action == "decrypt": elif self.options.ask_vault_pass:
parser.set_usage("usage: %prog decrypt [options] file_name") self.vault_pass, _= self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
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 len(args) == 0 or len(args) > 1: self.execute()
parser.print_help()
raise AnsibleError("Vault requires a single filename as a parameter")
if options.vault_password_file: def execute_create(self):
# 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_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' def execute_decrypt(self):
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 = VaultEditor(cipher, self.vault_pass, f)
this_editor.decrypt_file() this_editor.decrypt_file()
self.display.display("Decryption successful") self.display.display("Decryption successful")
def execute_edit(args, options): def execute_edit(self):
cipher = None for f in self.args:
this_editor = VaultEditor(None, self.vault_pass, f)
for f in args:
this_editor = VaultEditor(cipher, self.vault_pass, f)
this_editor.edit_file() this_editor.edit_file()
def execute_view(args, options): def execute_view(self):
cipher = None for f in self.args:
this_editor = VaultEditor(None, self.vault_pass, f)
for f in args:
this_editor = VaultEditor(cipher, self.vault_pass, f)
this_editor.view_file() 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 = VaultEditor(cipher, self.vault_pass, f)
this_editor.encrypt_file() this_editor.encrypt_file()
self.display.display("Encryption successful") self.display.display("Encryption successful")
def execute_rekey(args, options ): def execute_rekey(self):
__, new_password = ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True) __, new_password = self.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
cipher = None for f in self.args:
for f in args: this_editor = VaultEditor(None, self.vault_pass, f)
this_editor = VaultEditor(cipher, self.vault_pass, f)
this_editor.rekey_file(new_password) this_editor.rekey_file(new_password)
self.display.display("Rekey successful") self.display.display("Rekey successful")
...@@ -179,14 +138,12 @@ class Cli(object): ...@@ -179,14 +138,12 @@ class Cli(object):
if __name__ == "__main__": if __name__ == "__main__":
display = Display() display = Display()
#display.display(" ".join(sys.argv), log_only=True)
try: try:
cli = Cli(display=display) cli = VaultCli(sys.argv, display=display)
(options, args) = cli.parse() cli.parse()
sys.exit(cli.run(options, args)) sys.exit(cli.run())
except AnsibleError as e: except AnsibleError as e:
display.error(str(e)) display.display(str(e), stderr=True, color='red')
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
display.error("interrupted") display.error("interrupted")
......
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