diff --git a/examples/ansible.cfg b/examples/ansible.cfg index dc42359fcaf..35af8efd38e 100644 --- a/examples/ansible.cfg +++ b/examples/ansible.cfg @@ -1,138 +1,75 @@ -# config file for ansible -- http://ansible.github.com -# -# nearly all parameters can be overridden in ansible-playbook or with command line flags -# ansible will read ~/.ansible.cfg, ansible.cfg in the current working directory or +# config file for ansible -- http://ansibleworks.com/ +# ================================================== + +# nearly all parameters can be overridden in ansible-playbook +# or with command line flags. ansible will read ~/.ansible.cfg, +# ansible.cfg in the current working directory or # /etc/ansible/ansible.cfg, whichever it finds first [defaults] -# location of inventory file, eliminates need to specify -i - -hostfile = /etc/ansible/hosts - -# location of ansible library, eliminates need to specify --module-path - -library = /usr/share/ansible - -# default module name used in /usr/bin/ansible when -m is not specified - -module_name = command - -# location for ansible log file. If set, will store output from ansible -# and ansible-playbook. If enabling, you may wish to configure -# logrotate. - -#log_path = /var/log/ansible.log - -# home directory where temp files are stored on remote systems. Should -# almost always contain $HOME or be a directory writeable by all users - -remote_tmp = $HOME/.ansible/tmp - -# the default pattern for ansible-playbooks ("hosts:") - -pattern = * - -# the default number of forks (parallelism) to be used. Usually you -# can crank this up. - -forks=5 - -# the timeout used by various connection types. Usually this corresponds -# to an SSH timeout - -timeout=10 - -# when using --poll or "poll:" in an ansible playbook, and not specifying -# an explicit poll interval, use this interval +# some basic default values... -poll_interval=15 +hostfile = /etc/ansible/hosts +library = /usr/share/ansible +remote_tmp = $HOME/.ansible/tmp +pattern = * +forks = 5 +poll_interval = 15 +sudo_user = root +#ask_sudo_pass = True +#ask_pass = True +transport = paramiko +remote_port = 22 -# when specifying --sudo to /usr/bin/ansible or "sudo:" in a playbook, -# and not specifying "--sudo-user" or "sudo_user" respectively, sudo -# to this user account +# change this for alternative sudo implementations +sudo_exe = sudo -sudo_user=root - -# the following forces ansible to always ask for the sudo password (instead of having -# to add -K to the commandline). Or you can use the environment variable (ANSIBLE_ASK_SUDO_PASS) - -#ask_sudo_pass=True - -# the following forces ansible to always ask for the ssh-password (-k) -# can also be set by the environment variable ANSIBLE_ASK_PASS - -#ask_pass=True - -# connection to use when -c is not specified - -transport=paramiko - -# remote SSH port to be used when --port or "port:" or an equivalent inventory -# variable is not specified. - -remote_port=22 - -# if set, always run /usr/bin/ansible commands as this user, and assume this value -# if "user:" is not set in a playbook. If not set, use the current Unix user -# as the default - -#remote_user=root +# what flags to pass to sudo +# sudo_flags=-H -# the default sudo executable. If a sudo alternative with a sudo-compatible interface -# is used, specify its executable name as the default +# SSH timeout +timeout = 10 -sudo_exe=sudo +# default user to use for playbooks if user is not specified +# (/usr/bin/ansible will use current user as default) +remote_user=root -# the default flags passed to sudo -# sudo_flags=-H +# logging is off by default unless this path is defined +# if so defined, consider logrotate +# log_path = /var/log/ansible.log -# all commands executed under sudo are passed as arguments to a shell command -# This shell command defaults to /bin/sh -# Changing this helps the situation where a user is only allowed to run -# e.g. /bin/bash with sudo privileges +# default module name for /usr/bin/ansible +module_name = command +# use this shell for commands executed under sudo +# you may need to change this to bin/bash in rare instances +# if sudo is constrained # executable = /bin/sh -# how to handle hash defined in several places -# hash can be merged, or replaced -# if you use replace, and have multiple hashes named 'x', the last defined -# will override the previously defined one -# if you use merge here, hash will cumulate their keys, but keys will still -# override each other -# replace is the default value, and is how ansible always handled hash variables -# +# if inventory variables overlap, does the higher precedence one win +# or are hash values merged together? The default is 'replace' but +# this can also be set to 'merge'. # hash_behaviour=replace # How to handle variable replacement - as of 1.2, Jinja2 variable syntax is # preferred, but we still support the old $variable replacement too. -# If you change legacy_playbook_variables to no then Ansible will no longer -# try to do replacement on $variable style variables. -# +# Turn off ${old_style} variables here if you like. # legacy_playbook_variables=yes -# if you need to use jinja2 extensions, you can list them here -# use a coma to separate extensions, e.g. : +# list any Jinja2 extensions to enable here: # jinja2_extensions=jinja2.ext.do,jinja2.ext.i18n -# no extensions are loaded by default - -#jinja2_extensions= - -# if set, always use this private key file for authentication, same as if passing -# --private-key to ansible or ansible-playbook +# if set, always use this private key file for authentication, same as +# if passing --private-key to ansible or ansible-playbook #private_key_file=/path/to/file -# format of string $ansible_managed available within Jinja2 templates, replacing -# {file}, {host} and {uid} with template filename, host and owner respectively. -# The resulting string is passed through strftime(3) so it may contain any -# time-formatting specifiers. -# -# Example: ansible_managed = DONT TOUCH {file}: call {uid} at {host} for changes +# format of string {{ ansible_managed }} available within Jinja2 +# templates indicates to users editing templates files will be replaced. +, replacing {file}, {host} and {uid} and strftime codes with proper values. ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host} -# additional plugin paths for non-core plugins - +# set plugin path directories here, seperate with colons action_plugins = /usr/share/ansible_plugins/action_plugins callback_plugins = /usr/share/ansible_plugins/callback_plugins connection_plugins = /usr/share/ansible_plugins/connection_plugins @@ -140,24 +77,23 @@ lookup_plugins = /usr/share/ansible_plugins/lookup_plugins vars_plugins = /usr/share/ansible_plugins/vars_plugins filter_plugins = /usr/share/ansible_plugins/filter_plugins -# set to 1 if you don't want cowsay support. Alternatively, set ANSIBLE_NOCOWS=1 -# in your environment -# nocows = 1 +# don't like cows? that's unfortunate. +# set to 1 if you don't want cowsay support or export ANSIBLE_NOCOWS=1 +# nocows = 1 [paramiko_connection] -# nothing to configure yet +# nothing configurable yet [ssh_connection] -# if uncommented, sets the ansible ssh arguments to the following. Leaving off ControlPersist -# will result in poor performance, so use transport=paramiko on older platforms rather than -# removing it - +# ssh arguments to use +# Leaving off ControlPersist will result in poor performance, so use +# paramiko on older platforms rather than removing it ssh_args=-o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r -# the following makes ansible use scp if the connection type is ssh (default is sftp) - +# if True, make ansible use scp if the connection type is ssh +# (default is sftp) #scp_if_ssh=True diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 95842a71e8a..6632bdc51d2 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -110,6 +110,8 @@ ANSIBLE_NOCOWS = get_config(p, DEFAULTS, 'nocows', 'ANSIBLE_NOCO ANSIBLE_SSH_ARGS = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', None) ZEROMQ_PORT = int(get_config(p, 'fireball', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099)) +DEFAULT_UNDEFINED_VAR_BEHAVIOR = get_config(p, DEFAULTS, 'error_on_undefined_vars', 'ANSIBLE_ERROR_ON_UNDEFINED_VARS', False) + # non-configurable things DEFAULT_SUDO_PASS = None DEFAULT_REMOTE_PASS = None diff --git a/lib/ansible/errors.py b/lib/ansible/errors.py index 29562b7f658..de0e6e38f3c 100644 --- a/lib/ansible/errors.py +++ b/lib/ansible/errors.py @@ -32,3 +32,6 @@ class AnsibleConnectionFailed(AnsibleError): class AnsibleYAMLValidationFailed(AnsibleError): pass + +class AnsibleUndefinedVariable(AnsibleError): + pass diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index 6adf5512f78..1dac884af59 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -30,6 +30,7 @@ import base64 import sys import shlex import pipes +import jinja2 import ansible.constants as C import ansible.inventory @@ -128,7 +129,8 @@ class Runner(object): check=False, # don't make any changes, just try to probe for potential changes diff=False, # whether to show diffs for template files that change environment=None, # environment variables (as dict) to use inside the command - complex_args=None # structured data in addition to module_args, must be a dict + complex_args=None, # structured data in addition to module_args, must be a dict + error_on_undefined_vars=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR # ex. False ): if not complex_args: @@ -164,6 +166,7 @@ class Runner(object): self.environment = environment self.complex_args = complex_args self.module_with_list = False + self.error_on_undefined_vars = error_on_undefined_vars self.callbacks.runner = self @@ -446,11 +449,11 @@ class Runner(object): if type(complex_args) != dict: raise errors.AnsibleError("args must be a dictionary, received %s" % complex_args) result = self._executor_internal_inner( - host, - self.module_name, - self.module_args, - inject, - port, + host, + self.module_name, + self.module_args, + inject, + port, complex_args=complex_args ) results.append(result.result) @@ -576,8 +579,12 @@ class Runner(object): tmp = self._make_tmp_path(conn) # render module_args and complex_args templates - module_args = template.template(self.basedir, module_args, inject) - complex_args = template.template(self.basedir, complex_args, inject) + try: + module_args = template.template(self.basedir, module_args, inject, fail_on_undefined=self.error_on_undefined_vars) + complex_args = template.template(self.basedir, complex_args, inject, fail_on_undefined=self.error_on_undefined_vars) + except jinja2.exceptions.UndefinedError, e: + raise errors.AnsibleUndefinedVariable("Undefined variables: %s" % str(e)) + result = handler.run(conn, tmp, module_name, module_args, inject, complex_args) @@ -644,7 +651,7 @@ class Runner(object): path = pipes.quote(path) # The following test needs to be SH-compliant. BASH-isms will # not work if /bin/sh points to a non-BASH shell. - test = "rc=0; [ -r \"%s\" ] || rc=2; [ -f \"%s\" ] || rc=1; [ -d \"%s\" ] && rc=3" % ((path,) * 3) + test = "rc=0; [ -r \"%s\" ] || rc=2; [ -f \"%s\" ] || rc=1; [ -d \"%s\" ] && rc=3" % ((path,) * 3) md5s = [ "(/usr/bin/md5sum %s 2>/dev/null)" % path, # Linux "(/sbin/md5sum -q %s 2>/dev/null)" % path, # ? diff --git a/lib/ansible/utils/template.py b/lib/ansible/utils/template.py index 9c6e26e6a56..7da36b0ba9b 100644 --- a/lib/ansible/utils/template.py +++ b/lib/ansible/utils/template.py @@ -34,7 +34,7 @@ class Globals(object): FILTERS = None def __init__(self): - pass + pass def _get_filters(): ''' return filter plugin instances ''' @@ -48,7 +48,7 @@ def _get_filters(): for fp in plugins: filters.update(fp.filters()) Globals.FILTERS = filters - + return Globals.FILTERS def _get_extensions(): @@ -90,7 +90,7 @@ def lookup(name, *args, **kwargs): def _legacy_varFindLimitSpace(basedir, vars, space, part, lookup_fatal, depth, expand_lists): ''' limits the search space of space to part - + basically does space.get(part, None), but with templating for part and a few more things ''' @@ -295,7 +295,7 @@ def legacy_varReplace(basedir, raw, vars, lookup_fatal=True, depth=0, expand_lis # TODO: varname is misnamed here -def template(basedir, varname, vars, lookup_fatal=True, depth=0, expand_lists=True, convert_bare=False): +def template(basedir, varname, vars, lookup_fatal=True, depth=0, expand_lists=True, convert_bare=False, fail_on_undefined=False): ''' templates a data structure by traversing it and substituting for other data structures ''' if convert_bare and isinstance(varname, basestring): @@ -305,7 +305,7 @@ def template(basedir, varname, vars, lookup_fatal=True, depth=0, expand_lists=Tr if isinstance(varname, basestring): if '{{' in varname or '{%' in varname: - varname = template_from_string(basedir, varname, vars) + varname = template_from_string(basedir, varname, vars, fail_on_undefined) if not '$' in varname: return varname @@ -341,7 +341,7 @@ class _jinja2_vars(object): is avoiding duplicating the large hashes that inject tends to be. To facilitate using builtin jinja2 things like range, globals are handled here. - extras is a list of locals to also search for variables. + extras is a list of locals to also search for variables. ''' def __init__(self, basedir, vars, globals, *extras): @@ -398,6 +398,8 @@ class J2Template(jinja2.environment.Template): def template_from_file(basedir, path, vars): ''' run a file through the templating engine ''' + fail_on_undefined = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR + from ansible import utils realpath = utils.path_dwim(basedir, path) loader=jinja2.FileSystemLoader([basedir,os.path.dirname(realpath)]) @@ -409,6 +411,8 @@ def template_from_file(basedir, path, vars): environment = jinja2.Environment(loader=loader, trim_blocks=True, extensions=_get_extensions()) environment.filters.update(_get_filters()) environment.globals['lookup'] = my_lookup + if fail_on_undefined: + environment.undefined = StrictUndefined try: data = codecs.open(realpath, encoding="utf8").read() @@ -417,7 +421,7 @@ def template_from_file(basedir, path, vars): except: raise errors.AnsibleError("unable to read %s" % realpath) - + # Get jinja env overrides from template if data.startswith(JINJA2_OVERRIDE): eol = data.find('\n') @@ -455,15 +459,18 @@ def template_from_file(basedir, path, vars): # This line performs deep Jinja2 magic that uses the _jinja2_vars object for vars # Ideally, this could use some API where setting shared=True and the object won't get # passed through dict(o), but I have not found that yet. - res = jinja2.utils.concat(t.root_render_func(t.new_context(_jinja2_vars(basedir, vars, t.globals), shared=True))) + try: + res = jinja2.utils.concat(t.root_render_func(t.new_context(_jinja2_vars(basedir, vars, t.globals), shared=True))) + except jinja2.exceptions.UndefinedError, e: + raise errors.AnsibleUndefinedVariable("Undefined variables: %s" % str(e)) if data.endswith('\n') and not res.endswith('\n'): res = res + '\n' return template(basedir, res, vars) -def template_from_string(basedir, data, vars): +def template_from_string(basedir, data, vars, fail_on_undefined=False): ''' run a string through the (Jinja2) templating engine ''' - + try: if type(data) == str: data = unicode(data, 'utf-8') @@ -487,16 +494,19 @@ def template_from_string(basedir, data, vars): raise errors.AnsibleError("recursive loop detected in template string: %s" % data) else: return data - + def my_lookup(*args, **kwargs): kwargs['vars'] = vars return lookup(*args, basedir=basedir, **kwargs) - + t.globals['lookup'] = my_lookup - + res = jinja2.utils.concat(t.root_render_func(t.new_context(_jinja2_vars(basedir, vars, t.globals), shared=True))) return res except jinja2.exceptions.UndefinedError: + if fail_on_undefined: + raise + else: # this shouldn't happen due to undeclared check above - return data + return data