From cdefeb6d84499d86bf6fef8352b06d626c1bf4ae Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 27 Apr 2015 07:31:41 -0400 Subject: [PATCH] refactored most binaries added AnsibleOptionsError removed pulicate parser error class --- v2/ansible/constants.py | 19 +- v2/ansible/errors/__init__.py | 16 +- v2/ansible/galaxy/role.py | 117 +++++++-- v2/ansible/utils/cli.py | 458 ++++++++++++++++++++-------------- v2/bin/ansible | 116 +++++---- v2/bin/ansible-galaxy | 307 ++++++++++------------- v2/bin/ansible-playbook | 255 ++++++++++--------- v2/bin/ansible-vault | 153 ++++-------- 8 files changed, 754 insertions(+), 687 deletions(-) diff --git a/v2/ansible/constants.py b/v2/ansible/constants.py index 12eb8db413b..6f35751b506 100644 --- a/v2/ansible/constants.py +++ b/v2/ansible/constants.py @@ -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) + if floating: + value = float(value) + if 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') @@ -212,6 +214,7 @@ GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY 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 diff --git a/v2/ansible/errors/__init__.py b/v2/ansible/errors/__init__.py index 453e63de6e3..63fb8ef023a 100644 --- a/v2/ansible/errors/__init__.py +++ b/v2/ansible/errors/__init__.py @@ -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 diff --git a/v2/ansible/galaxy/role.py b/v2/ansible/galaxy/role.py index 89d8399b2da..0d13233e6a4 100644 --- a/v2/ansible/galaxy/role.py +++ b/v2/ansible/galaxy/role.py @@ -36,6 +36,8 @@ class GalaxyRole(object): SUPPORTED_SCMS = set(['git', 'hg']) META_MAIN = os.path.join('meta', 'main.yml') 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): @@ -45,13 +47,13 @@ class GalaxyRole(object): self.name = role_name self.meta_data = 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 self.version = role_version self.url = role_url - if self.url is None and '://' in self.name: - self.url = self.name + if self.url is None: + self._spec_parse() if C.GALAXY_SCMS: self.scms = self.SUPPORTED_SCMS.intersection(set(C.GALAXY_SCMS)) @@ -62,7 +64,7 @@ class GalaxyRole(object): 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 if scm not in self.scms: @@ -111,12 +113,21 @@ class GalaxyRole(object): return temp_file.name + def get_metadata(self): + """ + Returns role metadata + """ + if self.meta_data is None: + self._read_metadata - def read_metadata(self): + return self.meta_data + + + def _read_metadata(self): """ 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): try: f = open(meta_path, 'r') @@ -127,15 +138,24 @@ class GalaxyRole(object): finally: 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', 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): try: f = open(info_path, 'r') @@ -146,9 +166,7 @@ class GalaxyRole(object): finally: 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 (named .galaxy_install_info) which contains some information @@ -159,7 +177,7 @@ class GalaxyRole(object): version=self.version, 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: f = open(info_path, 'w+') self.install_info = yaml.safe_dump(info, f) @@ -178,7 +196,7 @@ class GalaxyRole(object): """ if self.read_metadata(): try: - rmtree(self.role_path) + rmtree(self.path) return True except: pass @@ -213,7 +231,7 @@ class GalaxyRole(object): self.display.error("failed to download the file.") 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 # to the specified (or default) roles directory @@ -246,10 +264,10 @@ class GalaxyRole(object): # 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 # 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: - if os.path.exists(self.role_path): - if not os.path.isdir(self.role_path): + if os.path.exists(self.path): + if not os.path.isdir(self.path): self.display.error("the specified roles path exists and is not a directory.") return False elif not getattr(self.options, "force", False): @@ -258,13 +276,13 @@ class GalaxyRole(object): else: # using --force, remove the old path 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.") return False 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: # we only extract files, and remove any relative path # bits that might be in the file for security purposes @@ -276,15 +294,62 @@ class GalaxyRole(object): if part != '..' and '~' not in part and '$' not in part: final_parts.append(part) 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 - self.version = role_version - self.write_galaxy_install_info() + self._write_galaxy_install_info() 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 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 + + 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, '') diff --git a/v2/ansible/utils/cli.py b/v2/ansible/utils/cli.py index 6500234c741..0cceab01968 100644 --- a/v2/ansible/utils/cli.py +++ b/v2/ansible/utils/cli.py @@ -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,141 +41,286 @@ 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, - async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False): - ''' create an options parser for any ansible script ''' +#TODO: move many cli only functions in this file into the CLI class +class CLI(object): + ''' code behind bin/ansible* programs ''' - parser = SortedOptParser(usage, version=version("%prog")) + VALID_ACTIONS = ['No Actions'] - 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=[]) + def __init__(self, args, display=None): + """ + Base init method for all command line programs + """ - 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") + self.args = args + self.options = None + self.parser = None + self.action = None - 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 display is None: + self.display = Display() + else: + self.display = display - 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) + 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 - # 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 not self.action: + self.parser.print_help() + raise AnsibleError("Missing required action") + + def execute(self): + """ + Actually runs a child defined method using the execute_ 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 - 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) + 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) - 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)') + 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 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 - -def version(prog): - 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 - -def version_info(gitinfo=False): - 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: + if self.options.become: 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]} + 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=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): + 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): + 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]} def _git_repo_info(repo_path): ''' returns a string containing git branch, commit id and commit date ''' @@ -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") - diff --git a/v2/bin/ansible b/v2/bin/ansible index b4f651ffdaa..77446338da0 100755 --- a/v2/bin/ansible +++ b/v2/bin/ansible @@ -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 [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: - # read vault_pass from a file - vault_pass = read_vault_file(options.vault_password_file) + 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=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) - tqm.cleanup() - except AnsibleError: - tqm.cleanup() - raise + finally: + if tqm: + tqm.cleanup() 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,14 +176,12 @@ 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)) + cli = AdHocCli(sys.argv, display=display) + cli.parse() + sys.exit(cli.run()) except AnsibleError as e: - display.error(str(e)) + display.display(str(e), stderr=True, color='red') sys.exit(1) except KeyboardInterrupt: display.error("interrupted") diff --git a/v2/bin/ansible-galaxy b/v2/bin/ansible-galaxy index 1c8215b944f..cca1dd9d835 100755 --- a/v2/bin/ansible-galaxy +++ b/v2/bin/ansible-galaxy @@ -42,113 +42,109 @@ from optparse import OptionParser import ansible.constants as C import ansible.utils import ansible.galaxy -from ansible.errors import AnsibleError +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.utils.display import Display +from ansible.utils.cli import CLI -class Cli(object): +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): - - if display is None: - self.display = Display() - else: - self.display = display - self.action = None - - def set_action(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] - self.action = arg + 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(VALID_ACTIONS) + + usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS) epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) OptionParser.format_epilog = lambda self, formatter: self.epilog parser = OptionParser(usage=usage, epilog=epilog) - if not self.action: - parser.print_help() - sys.exit(1) + self.parser = parser + self.set_action() # options specific to actions if self.action == "info": - parser.set_usage("usage: %prog info [options] role_name[,version]") + self.parser.set_usage("usage: %prog info [options] role_name[,version]") elif self.action == "init": - parser.set_usage("usage: %prog init [options] role_name") - parser.add_option( + 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.') - parser.add_option( + 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": - parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]") - parser.add_option( + 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.') - parser.add_option( + self.parser.add_option( '-n', '--no-deps', dest='no_deps', action='store_true', default=False, help='Don\'t download roles listed as dependencies') - parser.add_option( + 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": - parser.set_usage("usage: %prog remove role1 role2 ...") + self.parser.set_usage("usage: %prog remove role1 role2 ...") elif self.action == "list": - parser.set_usage("usage: %prog list [role_name]") + self.parser.set_usage("usage: %prog list [role_name]") # options that apply to more than one action if self.action != "init": - parser.add_option( + 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"): - parser.add_option( + self.parser.add_option( '-s', '--server', dest='api_server', default="galaxy.ansible.com", help='The API server destination') if self.action in ("init","install"): - parser.add_option( + self.parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='Force overwriting an existing role') - # done, return the parser - options, args = parser.parse_args() + # get options, args and galaxy object + self.options, self.args =self.parser.parse_args() + self.galaxy = Galaxy(self.options, self.display) - if len(args) == 0 or len(args) > 1: - parser.print_help() - sys.exit(1) + if len(self.args) != 1: + raise AnsibleOptionsError("Missing arguments") - display.verbosity = options.verbosity + return True - return (options, args) + def run(self): - def run(options, args): + #self.display.verbosity = self.options.verbosity + api_server = self.get_opt("api_server", "galaxy.ansible.com") - # execute the desired action - fn = getattr(self, "execute_%s" % self.action) - fn(args, options) + # if not offline, get connect to galaxy api + if self.action == 'init' and not self.options.offline: + 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) - def get_opt(options, k, defval=""): + self.execute() + + def get_opt(self, k, defval=""): """ Returns an option from an Optparse values instance. """ try: - data = getattr(options, k) + data = getattr(self.options, k) except: return defval if k == "roles_path": @@ -156,56 +152,40 @@ class Cli(object): data = data.split(os.pathsep)[0] return data - def exit_without_ignore(options, rc=1): + def exit_without_ignore(self, rc=1): """ Exits with the specified return code unless the option --ignore-errors was specified """ - if not get_opt(options, "ignore_errors", False): - print '- you can use --ignore-errors to skip failed roles.' - sys.exit(rc) + if not self.get_opt("ignore_errors", False): + self.display.error('- you can use --ignore-errors to skip failed tasks/roles.') + return rc - def execute_init(args, options, parser): + 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 = get_opt(options, 'init_path', './') - api_server = get_opt(options, "api_server", "galaxy.ansible.com") - force = get_opt(options, 'force', False) - offline = get_opt(options, 'offline', False) + init_path = self.get_opt('init_path', './') + force = self.get_opt('force', False) + offline = self.get_opt('offline', False) - if not offline: - api_config = api_get_config(api_server) - if not api_config: - print "- the API server (%s) is not responding, please try again later." % api_server - sys.exit(1) - - try: - role_name = args.pop(0).strip() - if role_name == "": - raise Exception("") - role_path = os.path.join(init_path, role_name) - if os.path.exists(role_path): - if os.path.isfile(role_path): - print "- the path %s already exists, but is a file - aborting" % role_path - sys.exit(1) - elif not force: - print "- the directory %s already exists." % role_path - print " 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." - sys.exit(1) - except Exception, e: - parser.print_help() - print "- no role name specified for init" - sys.exit(1) - - ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars') + 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): @@ -215,7 +195,7 @@ class Cli(object): f.write(default_readme_template) f.close - for dir in ROLE_DIRS: + for dir in self.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 @@ -229,11 +209,11 @@ class Cli(object): # tags/platforms included (but commented out) and the # dependencies section platforms = [] - if not offline: - platforms = api_get_list(api_server, "platforms") or [] + if not offline and self.api: + platforms = self.api.get_list("platforms") or [] categories = [] - if not offline: - categories = api_get_list(api_server, "categories") or [] + 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 @@ -264,24 +244,20 @@ class Cli(object): f.close() print "- %s was created successfully" % role_name - def execute_info(args, options, parser): + 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(args) == 0: + if len(self.args) == 0: # the user needs to specify a role - parser.print_help() - print "- you must specify a user/role name" - sys.exit(1) + raise AnsibleOptionsError("- you must specify a user/role name") - api_server = get_opt(options, "api_server", "galaxy.ansible.com") - api_config = api_get_config(api_server) - roles_path = get_opt(options, "roles_path") + roles_path = self.get_opt("roles_path") - for role in args: + for role in self.args: role_info = {} @@ -292,11 +268,11 @@ class Cli(object): del install_info['version'] role_info.update(install_info) - remote_data = api_lookup_role_by_name(api_server, role, False) + remote_data = self.api.lookup_role_by_name(role, False) if remote_data: role_info.update(remote_data) - metadata = get_role_metadata(role, options) + metadata = get_metadata(role, options) if metadata: role_info.update(metadata) @@ -322,7 +298,7 @@ class Cli(object): else: print "- the role %s was not found" % role - def execute_install(args, options, parser): + 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 @@ -330,24 +306,19 @@ class Cli(object): or it can be a local .tar.gz file. """ - role_file = get_opt(options, "role_file", None) + role_file = self.get_opt("role_file", None) - if len(args) == 0 and role_file is 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 - parser.print_help() - print "- you must specify a user/role name or a roles file" - sys.exit() - elif len(args) == 1 and not role_file is None: + 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 - parser.print_help() - print "- please specify a user/role name, or a roles file, but not both" - sys.exit(1) + raise AnsibleOptionsError("- please specify a user/role name, or a roles file, but not both") - api_server = get_opt(options, "api_server", "galaxy.ansible.com") - no_deps = get_opt(options, "no_deps", False) - roles_path = get_opt(options, "roles_path") + no_deps = self.get_opt("no_deps", False) + roles_path = self.get_opt("roles_path") roles_done = [] if role_file: @@ -356,12 +327,12 @@ class Cli(object): roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f)) else: # roles listed in a file, one per line - roles_left = map(ansible.utils.role_spec_parse, f.readlines()) + roles_left = map(gr.get_spec, f.readlines()) 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). - roles_left = map(ansible.utils.role_spec_parse, args) + roles_left = map(gr.get_spec, self.args) while len(roles_left) > 0: # query the galaxy API for the role data @@ -387,19 +358,13 @@ class Cli(object): # just download a URL - version will probably be in the URL tmp_file = fetch_role(role_src, None, None, options) else: - # installing from galaxy - api_config = api_get_config(api_server) - if not api_config: - print "- the API server (%s) is not responding, please try again later." % api_server - sys.exit(1) - - role_data = api_lookup_role_by_name(api_server, role_src) + role_data = self.api.lookup_role_by_name(role_src) if not role_data: - print "- sorry, %s was not found on %s." % (role_src, api_server) + print "- sorry, %s was not found on %s." % (role_src, self.options.api_server) exit_without_ignore(options) continue - role_versions = api_fetch_role_related(api_server, 'versions', role_data['id']) + role_versions = self.api.fetch_role_related('versions', role_data['id']) if "version" not in role or role['version'] == '': # convert the version names to LooseVersion objects # and sort them to get the latest version. If there @@ -430,7 +395,7 @@ class Cli(object): # install dependencies, if we want them if not no_deps and installed: if not role_data: - role_data = get_role_metadata(role.get("name"), options) + 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']) @@ -450,30 +415,28 @@ class Cli(object): if not tmp_file or not installed: print "- %s was NOT installed successfully." % role.get("name") exit_without_ignore(options) - sys.exit(0) + return 0 - def execute_remove(args, options, parser): + 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(args) == 0: - parser.print_help() - print '- you must specify at least one role to remove.' - sys.exit() + if len(self.args) == 0: + raise AnsibleOptionsError('- you must specify at least one role to remove.') - for role in args: + for role in self.args: if get_role_metadata(role, options): if remove_role(role, options): - print '- successfully removed %s' % role + self.display.display('- successfully removed %s' % role) else: - print "- failed to remove role: %s" % role + self.display.display("- failed to remove role: %s" % role) else: - print '- %s is not installed, skipping.' % role - sys.exit(0) + self.display.display('- %s is not installed, skipping.' % role) + return 0 - def execute_list(args, options, parser): + 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 @@ -481,37 +444,33 @@ class Cli(object): be shown. """ - if len(args) > 1: - print "- please specify only one role to list, or specify no roles to see a full list" - sys.exit(1) + 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(args) == 1: + if len(self.args) == 1: # show only the request role, if it exists - role_name = args[0] - metadata = get_role_metadata(role_name, options) + role_name = self.args[0] + gr = GalaxyRole(self.galaxy, role_name) + metadata = gr.get_metadata() if metadata: - install_info = get_galaxy_install_info(role_name, options) + install_info = gr.get_galaxy_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 - print "- %s, %s" % (role_name, version) + self.display.display("- %s, %s" % (role_name, version)) else: - print "- the role %s was not found" % role_name + self.display.display("- the role %s was not found" % role_name) else: # show all valid roles in the roles_path directory - roles_path = get_opt(options, 'roles_path') + roles_path = self.get_opt('roles_path') roles_path = os.path.expanduser(roles_path) if not os.path.exists(roles_path): - parser.print_help() - print "- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path - sys.exit(1) + 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): - print "- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path - parser.print_help() - sys.exit(1) + 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 get_role_metadata(path_file, options): @@ -521,40 +480,26 @@ class Cli(object): version = install_info.get("version", None) if not version: version = "(unknown version)" - print "- %s, %s" % (path_file, version) - sys.exit(0) + self.display.display("- %s, %s" % (path_file, version)) + return 0 #------------------------------------------------------------------------------------- # The main entry point #------------------------------------------------------------------------------------- - -#def main(): -# # parse the CLI options -# action = get_action(sys.argv) -# parser = build_option_parser(action) -# (options, args) = parser.parse_args() -# -# # execute the desired action -# if 1: #try: -# fn = globals()["execute_%s" % action] -# fn(args, options, parser) -# #except KeyError, e: -# # print "- error: %s is not a valid action. Valid actions are: %s" % (action, ", ".join(VALID_ACTIONS)) -# # sys.exit(1) - - if __name__ == '__main__': display = Display() - try: - cli = Cli(display=display) - cli.set_action(sys.argv) - (options, args) = cli.parse() - sys.exit(cli.run(options, args)) - except AnsibleError as e: - display.error(str(e)) + cli = GalaxyCLI(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.error("interrupted") - sys.exit(1) + sys.exit(3) diff --git a/v2/bin/ansible-playbook b/v2/bin/ansible-playbook index d9247fef1c7..700538cb56c 100755 --- a/v2/bin/ansible-playbook +++ b/v2/bin/ansible-playbook @@ -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,145 +53,156 @@ from ansible.vars import VariableManager #--------------------------------------------------------------------------------------------------- -def main(display, args): - ''' run ansible-playbook operations ''' +class PlaybookCLI(CLI): + ''' code behind ansible playbook cli''' - # create parser for CLI options - parser = base_parser( - usage = "%prog playbook.yml", - connect_opts=True, - meta_opts=True, - runas_opts=True, - subset_opts=True, - check_opts=True, - diff_opts=True, - ) + def parse(self): - # 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") + # 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, + ) - options, args = parser.parse_args(args) + # 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") - if len(args) == 0: - parser.print_help(file=sys.stderr) - return 1 + self.options, self.args = parser.parse_args() - display.verbosity = options.verbosity - validate_conflicts(parser,options) + if len(self.args) == 0: + parser.print_help(file=sys.stderr) + raise AnsibleError("You must specify a playbook file to run") - # Note: slightly wrong, this is written so that implicit localhost - # Manage passwords - sshpass = None - becomepass = None - vault_pass = None + self.parser = parser - # 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) - passwords = { 'conn_pass': sshpass, 'become_pass': becomepass } + self.display.verbosity = self.options.verbosity + self.validate_conflicts() - if options.vault_password_file: - # read vault_pass from a file - vault_pass = read_vault_file(options.vault_password_file) + def run(self): - loader = DataLoader(vault_password=vault_pass) + # Note: slightly wrong, this is written so that implicit localhost + # Manage passwords + sshpass = None + becomepass = None + vault_pass = None + passwords = {} - extra_vars = {} - for extra_vars_opt in 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) + # 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: - # Arguments as Key-value - data = parse_kv(extra_vars_opt) - extra_vars = combine_vars(extra_vars, data) + return results - # 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(",") - - # initial error check, to make sure all specified playbooks are accessible - # before we start running anything through the playbook executor - for playbook in 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=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 - display.warning("provided hosts list is empty, only localhost is available") - no_hosts = True - inventory.subset(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") - - # 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) - - results = pbex.run() - - if isinstance(results, list): - for p in results: - - 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']))) - 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'])) - 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']))) - for tag in play['tags']: - 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:])) + cli = PlaybookCLI(sys.argv, display=display) + cli.parse() + sys.exit(cli.run()) except AnsibleError as e: - display.error(str(e)) + display.display(str(e), stderr=True, color='red') sys.exit(1) except KeyboardInterrupt: display.error("interrupted") diff --git a/v2/bin/ansible-vault b/v2/bin/ansible-vault index 638d80ba9ed..78686b6839a 100755 --- a/v2/bin/ansible-vault +++ b/v2/bin/ansible-vault @@ -35,141 +35,100 @@ import traceback from ansible.errors import AnsibleError 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() - def run(self, options, args): - - action = self.get_action(args) - - if not action: - parser.print_help() - raise AnsibleError("missing required action") - - # 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") + # 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": - parser.set_usage("usage: %prog rekey [options] file_name") + self.parser.set_usage("usage: %prog rekey [options] file_name") - if len(args) == 0 or len(args) > 1: - parser.print_help() + 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") - if options.vault_password_file: + def run(self): + + if self.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) + 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) - # execute the desired action - fn = getattr(self, "execute_%s" % action) - fn(args, options) + self.execute() - 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(self): - def execute_create(args, options): - - cipher = 'AES256' - if hasattr(options, 'cipher'): - cipher = options.cipher - - this_editor = VaultEditor(cipher, self.vault_pass, args[0]) + cipher = getattr(self.options, 'cipher', self.CIPHER) + this_editor = VaultEditor(cipher, self.vault_pass, self.args[0]) this_editor.create_file() - def execute_decrypt(args, options): + def execute_decrypt(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.decrypt_file() self.display.display("Decryption successful") - def execute_edit(args, options): + def execute_edit(self): - 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.edit_file() - def execute_view(args, options): + def execute_view(self): - 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.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,14 +138,12 @@ 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)) + cli = VaultCli(sys.argv, display=display) + cli.parse() + sys.exit(cli.run()) except AnsibleError as e: - display.error(str(e)) + display.display(str(e), stderr=True, color='red') sys.exit(1) except KeyboardInterrupt: display.error("interrupted")