Fixing many bugs in v2

* delegate_to rudimentary support (still needs much more work)
* lots of other things
pull/10195/head
James Cammarata 10 years ago
parent 402a6d0533
commit 31dd75de59

@ -43,9 +43,11 @@ class ConnectionInformation:
# various different auth escalation methods (becomes, etc.)
self.connection = C.DEFAULT_TRANSPORT
self.remote_addr = None
self.remote_user = 'root'
self.password = ''
self.port = 22
self.private_key_file = None
self.su = False
self.su_user = ''
self.su_pass = ''
@ -65,6 +67,14 @@ class ConnectionInformation:
if options:
self.set_options(options)
def __repr__(self):
value = "CONNECTION INFO:\n"
fields = self._get_fields()
fields.sort()
for field in fields:
value += "%20s : %s\n" % (field, getattr(self, field))
return value
def set_play(self, play):
'''
Configures this connection information instance with data from
@ -128,26 +138,6 @@ class ConnectionInformation:
when merging in data from task overrides.
'''
#self.connection = ci.connection
#self.remote_user = ci.remote_user
#self.password = ci.password
#self.port = ci.port
#self.su = ci.su
#self.su_user = ci.su_user
#self.su_pass = ci.su_pass
#self.sudo = ci.sudo
#self.sudo_user = ci.sudo_user
#self.sudo_pass = ci.sudo_pass
#self.verbosity = ci.verbosity
# other
#self.no_log = ci.no_log
#self.environment = ci.environment
# requested tags
#self.only_tags = ci.only_tags.copy()
#self.skip_tags = ci.skip_tags.copy()
for field in self._get_fields():
value = getattr(ci, field, None)
if isinstance(value, dict):

@ -172,7 +172,7 @@ class TaskExecutor:
self._connection_info.post_validate(variables=variables, loader=self._loader)
# get the connection and the handler for this execution
self._connection = self._get_connection()
self._connection = self._get_connection(variables)
self._handler = self._get_action_handler(connection=self._connection)
# Evaluate the conditional (if any) for this task, which we do before running
@ -204,6 +204,7 @@ class TaskExecutor:
# with the registered variable value later on when testing conditions
vars_copy = variables.copy()
debug("starting attempt loop")
result = None
for attempt in range(retries):
@ -301,7 +302,7 @@ class TaskExecutor:
else:
return async_result
def _get_connection(self):
def _get_connection(self, variables):
'''
Reads the connection property for the host, and returns the
correct connection object from the list of connection plugins
@ -310,13 +311,17 @@ class TaskExecutor:
# FIXME: delegate_to calculation should be done here
# FIXME: calculation of connection params/auth stuff should be done here
self._connection_info.remote_addr = self._host.ipv4_address
if self._task.delegate_to is not None:
self._compute_delegate(variables)
# FIXME: add all port/connection type munging here (accelerated mode,
# fixing up options for ssh, etc.)? and 'smart' conversion
conn_type = self._connection_info.connection
if conn_type == 'smart':
conn_type = 'ssh'
connection = connection_loader.get(conn_type, self._host, self._connection_info)
connection = connection_loader.get(conn_type, self._connection_info)
if not connection:
raise AnsibleError("the connection plugin '%s' was not found" % conn_type)
@ -350,3 +355,37 @@ class TaskExecutor:
raise AnsibleError("the handler '%s' was not found" % handler_name)
return handler
def _compute_delegate(self, variables):
# get the vars for the delegate by its name
try:
this_info = variables['hostvars'][self._task.delegate_to]
except:
# make sure the inject is empty for non-inventory hosts
this_info = {}
# get the real ssh_address for the delegate and allow ansible_ssh_host to be templated
#self._connection_info.remote_user = self._compute_delegate_user(self.delegate_to, delegate['inject'])
self._connection_info.remote_addr = this_info.get('ansible_ssh_host', self._task.delegate_to)
self._connection_info.port = this_info.get('ansible_ssh_port', self._connection_info.port)
self._connection_info.password = this_info.get('ansible_ssh_pass', self._connection_info.password)
self._connection_info.private_key_file = this_info.get('ansible_ssh_private_key_file', self._connection_info.private_key_file)
self._connection_info.connection = this_info.get('ansible_connection', self._connection_info.connection)
self._connection_info.sudo_pass = this_info.get('ansible_sudo_pass', self._connection_info.sudo_pass)
if self._connection_info.remote_addr in ('127.0.0.1', 'localhost'):
self._connection_info.connection = 'local'
# Last chance to get private_key_file from global variables.
# this is useful if delegated host is not defined in the inventory
#if delegate['private_key_file'] is None:
# delegate['private_key_file'] = remote_inject.get('ansible_ssh_private_key_file', None)
#if delegate['private_key_file'] is not None:
# delegate['private_key_file'] = os.path.expanduser(delegate['private_key_file'])
for i in this_info:
if i.startswith("ansible_") and i.endswith("_interpreter"):
variables[i] = this_info[i]

@ -1 +1 @@
Subproject commit 095f8681dbdfd2e9247446822e953287c9bca66c
Subproject commit 34784b7a617aa35d3b994c9f0795567afc6fb0b0

@ -219,7 +219,7 @@ class ModuleArgsParser:
thing = None
action = None
delegate_to = None
delegate_to = self._task_ds.get('delegate_to', None)
args = dict()
@ -236,15 +236,12 @@ class ModuleArgsParser:
# action
if 'action' in self._task_ds:
# an old school 'action' statement
thing = self._task_ds['action']
delegate_to = None
action, args = self._normalize_parameters(thing, additional_args=additional_args)
# local_action
if 'local_action' in self._task_ds:
# local_action is similar but also implies a delegate_to
if action is not None:
raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task_ds)

@ -66,6 +66,7 @@ class Conditional:
evaluation.
'''
original = conditional
if conditional is None or conditional == '':
return True

@ -161,7 +161,9 @@ class ActionBase:
tmp_mode = 'a+rx'
cmd = self._shell.mkdtemp(basefile, use_system_tmp, tmp_mode)
debug("executing _low_level_execute_command to create the tmp path")
result = self._low_level_execute_command(cmd, None, sudoable=False)
debug("done with creation of tmp path")
# error handling on this seems a little aggressive?
if result['rc'] != 0:
@ -196,11 +198,13 @@ class ActionBase:
def _remove_tmp_path(self, tmp_path):
'''Remove a temporary path we created. '''
if "-tmp-" in tmp_path:
if tmp_path and "-tmp-" in tmp_path:
cmd = self._shell.remove(tmp_path, recurse=True)
# If we have gotten here we have a working ssh configuration.
# If ssh breaks we could leave tmp directories out on the remote system.
debug("calling _low_level_execute_command to remove the tmp path")
self._low_level_execute_command(cmd, None, sudoable=False)
debug("done removing the tmp path")
def _transfer_data(self, remote_path, data):
'''
@ -213,14 +217,16 @@ class ActionBase:
afd, afile = tempfile.mkstemp()
afo = os.fdopen(afd, 'w')
try:
if not isinstance(data, unicode):
#ensure the data is valid UTF-8
data = data.decode('utf-8')
else:
data = data.encode('utf-8')
# FIXME: is this still necessary?
#if not isinstance(data, unicode):
# #ensure the data is valid UTF-8
# data = data.decode('utf-8')
#else:
# data = data.encode('utf-8')
afo.write(data)
except Exception, e:
raise AnsibleError("failure encoding into utf-8: %s" % str(e))
#raise AnsibleError("failure encoding into utf-8: %s" % str(e))
raise AnsibleError("failure writing module data to temporary file for transfer: %s" % str(e))
afo.flush()
afo.close()
@ -238,7 +244,10 @@ class ActionBase:
'''
cmd = self._shell.chmod(mode, path)
return self._low_level_execute_command(cmd, tmp, sudoable=sudoable)
debug("calling _low_level_execute_command to chmod the remote path")
res = self._low_level_execute_command(cmd, tmp, sudoable=sudoable)
debug("done with chmod call")
return res
def _remote_checksum(self, tmp, path):
'''
@ -250,7 +259,9 @@ class ActionBase:
#python_interp = inject['hostvars'][inject['inventory_hostname']].get('ansible_python_interpreter', 'python')
python_interp = 'python'
cmd = self._shell.checksum(path, python_interp)
debug("calling _low_level_execute_command to get the remote checksum")
data = self._low_level_execute_command(cmd, tmp, sudoable=True)
debug("done getting the remote checksum")
# FIXME: implement this function?
#data2 = utils.last_non_blank_line(data['stdout'])
try:
@ -286,7 +297,9 @@ class ActionBase:
expand_path = '~%s' % self._connection_info.su_user
cmd = self._shell.expand_user(expand_path)
debug("calling _low_level_execute_command to expand the remote user path")
data = self._low_level_execute_command(cmd, tmp, sudoable=False)
debug("done expanding the remote user path")
#initial_fragment = utils.last_non_blank_line(data['stdout'])
initial_fragment = data['stdout'].strip().splitlines()[-1]
@ -354,7 +367,9 @@ class ActionBase:
# FIXME: async stuff here?
#if (module_style != 'new' or async_jid is not None or not self._connection._has_pipelining or not C.ANSIBLE_SSH_PIPELINING or C.DEFAULT_KEEP_REMOTE_FILES):
if remote_module_path:
debug("transfering module to remote")
self._transfer_data(remote_module_path, module_data)
debug("done transfering module to remote")
environment_string = self._compute_environment_string()
@ -389,7 +404,9 @@ class ActionBase:
# specified in the play, not the sudo_user
sudoable = False
debug("calling _low_level_execute_command() for command %s" % cmd)
res = self._low_level_execute_command(cmd, tmp, sudoable=sudoable, in_data=in_data)
debug("_low_level_execute_command returned ok")
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
if (self._connection_info.sudo and self._connection_info.sudo_user != 'root') or (self._connection_info.su and self._connection_info.su_user != 'root'):
@ -446,7 +463,7 @@ class ActionBase:
# FIXME: hard-coded sudo_exe here
cmd, prompt, success_key = self._connection_info.make_sudo_cmd('/usr/bin/sudo', executable, cmd)
debug("executing the command through the connection")
debug("executing the command %s through the connection" % cmd)
rc, stdin, stdout, stderr = self._connection.exec_command(cmd, tmp, executable=executable, in_data=in_data)
debug("command execution done")

@ -251,12 +251,6 @@ class ActionModule(ActionBase):
)
)
# FIXME: checkmode and no_log stuff
#if self.runner.noop_on_check(inject):
# new_module_args['CHECKMODE'] = True
#if self.runner.no_log:
# new_module_args['NO_LOG'] = True
module_return = self._execute_module(module_name='copy', module_args=new_module_args, delete_remote_tmp=delete_remote_tmp)
module_executed = True
@ -279,11 +273,6 @@ class ActionModule(ActionBase):
original_basename=source_rel
)
)
# FIXME: checkmode and no_log stuff
#if self.runner.noop_on_check(inject):
# new_module_args['CHECKMODE'] = True
#if self.runner.no_log:
# new_module_args['NO_LOG'] = True
# Execute the file module.
module_return = self._execute_module(module_name='file', module_args=new_module_args, delete_remote_tmp=delete_remote_tmp)
@ -296,18 +285,17 @@ class ActionModule(ActionBase):
if module_return.get('changed') == True:
changed = True
# the file module returns the file path as 'path', but
# the copy module uses 'dest', so add it if it's not there
if 'path' in module_return and 'dest' not in module_return:
module_return['dest'] = module_return['path']
# Delete tmp path if we were recursive or if we did not execute a module.
if (not C.DEFAULT_KEEP_REMOTE_FILES and not delete_remote_tmp) \
or (not C.DEFAULT_KEEP_REMOTE_FILES and delete_remote_tmp and not module_executed):
if (not C.DEFAULT_KEEP_REMOTE_FILES and not delete_remote_tmp) or (not C.DEFAULT_KEEP_REMOTE_FILES and delete_remote_tmp and not module_executed):
self._remove_tmp_path(tmp)
# the file module returns the file path as 'path', but
# the copy module uses 'dest', so add it if it's not there
if 'path' in module_return and 'dest' not in module_return:
module_return['dest'] = module_return['path']
# TODO: Support detailed status/diff for multiple files
if len(source_files) == 1:
if module_executed and len(source_files) == 1:
result = module_return
else:
result = dict(dest=dest, src=source, changed=changed)

@ -92,7 +92,7 @@ class ActionModule(ActionBase):
dest = self._loader.path_dwim(dest)
else:
# files are saved in dest dir, with a subdir for each host, then the filename
dest = "%s/%s/%s" % (self._loader.path_dwim(dest), self._connection._host, source_local)
dest = "%s/%s/%s" % (self._loader.path_dwim(dest), self._connection_info.remote_addr, source_local)
dest = dest.replace("//","/")

@ -34,8 +34,7 @@ class ConnectionBase:
A base class for connections to contain common code.
'''
def __init__(self, host, connection_info, *args, **kwargs):
self._host = host
def __init__(self, connection_info, *args, **kwargs):
self._connection_info = connection_info
self._has_pipelining = False
self._display = Display(connection_info)

@ -65,7 +65,7 @@ class Connection(ConnectionBase):
executable = executable.split()[0] if executable else None
self._display.vvv("%s EXEC %s" % (self._host, local_cmd))
self._display.vvv("%s EXEC %s" % (self._connection_info.remote_addr, local_cmd))
# FIXME: cwd= needs to be set to the basedir of the playbook
debug("opening command with Popen()")
p = subprocess.Popen(
@ -115,7 +115,7 @@ class Connection(ConnectionBase):
''' transfer a file from local to local '''
#vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
self._display.vvv("%s PUT %s TO %s" % (self._host, in_path, out_path))
self._display.vvv("%s PUT %s TO %s" % (self._connection_info.remote_addr, in_path, out_path))
if not os.path.exists(in_path):
#raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
raise AnsibleError("file or module does not exist: %s" % in_path)
@ -130,7 +130,7 @@ class Connection(ConnectionBase):
def fetch_file(self, in_path, out_path):
#vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
self._display.vvv("%s FETCH %s TO %s" % (self._host, in_path, out_path))
self._display.vvv("%s FETCH %s TO %s" % (self._connection_info.remote_addr, in_path, out_path))
''' fetch a file from local to local -- for copatibility '''
self.put_file(in_path, out_path)

@ -37,8 +37,8 @@ from ansible.plugins.connections import ConnectionBase
class Connection(ConnectionBase):
''' ssh based connections '''
def __init__(self, host, connection_info, *args, **kwargs):
super(Connection, self).__init__(host, connection_info)
def __init__(self, connection_info, *args, **kwargs):
super(Connection, self).__init__(connection_info)
# SSH connection specific init stuff
self.HASHED_KEY_MAGIC = "|1|"
@ -57,7 +57,7 @@ class Connection(ConnectionBase):
def connect(self):
''' connect to the remote host '''
self._display.vvv("ESTABLISH CONNECTION FOR USER: %s" % self._connection_info.remote_user, host=self._host)
self._display.vvv("ESTABLISH CONNECTION FOR USER: %s" % self._connection_info.remote_user, host=self._connection_info.remote_addr)
self._common_args = []
extra_args = C.ANSIBLE_SSH_ARGS
@ -277,7 +277,7 @@ class Connection(ConnectionBase):
# not sure if it's all working yet so this remains commented out
#if self._ipv6:
# ssh_cmd += ['-6']
ssh_cmd += [self._host.ipv4_address]
ssh_cmd += [self._connection_info.remote_addr]
if not (self._connection_info.sudo or self._connection_info.su):
prompt = None
@ -293,9 +293,9 @@ class Connection(ConnectionBase):
sudo_cmd, prompt, success_key = self._connection_info.make_sudo_cmd('/usr/bin/sudo', executable, cmd)
ssh_cmd.append(sudo_cmd)
self._display.vvv("EXEC %s" % ' '.join(ssh_cmd), host=self._host)
self._display.vvv("EXEC %s" % ' '.join(ssh_cmd), host=self._connection_info.remote_addr)
not_in_host_file = self.not_in_host_file(self._host.get_name())
not_in_host_file = self.not_in_host_file(self._connection_info.remote_addr)
# FIXME: move the locations of these lock files, same as init above
#if C.HOST_KEY_CHECKING and not_in_host_file:
@ -309,44 +309,6 @@ class Connection(ConnectionBase):
self._send_password()
no_prompt_out = ''
no_prompt_err = ''
# FIXME: su/sudo stuff
#if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
# (self.runner.su and su and self.runner.su_pass):
# # several cases are handled for sudo privileges with password
# # * NOPASSWD (tty & no-tty): detect success_key on stdout
# # * without NOPASSWD:
# # * detect prompt on stdout (tty)
# # * detect prompt on stderr (no-tty)
# fcntl.fcntl(p.stdout, fcntl.F_SETFL,
# fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
# fcntl.fcntl(p.stderr, fcntl.F_SETFL,
# fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
# sudo_output = ''
# sudo_errput = ''
#
# while True:
# if success_key in sudo_output or \
# (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
# (self.runner.su_pass and utils.su_prompts.check_su_prompt(sudo_output)):
# break
self._display.vvv("EXEC %s" % ' '.join(ssh_cmd), host=self._host)
not_in_host_file = self.not_in_host_file(self._host.get_name())
# FIXME: file locations
#if C.HOST_KEY_CHECKING and not_in_host_file:
# # lock around the initial SSH connectivity so the user prompt about whether to add
# # the host to known hosts is not intermingled with multiprocess output.
# fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
# fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
# create process
(p, stdin) = self._run(ssh_cmd, in_data)
self._send_password()
no_prompt_out = ''
no_prompt_err = ''
# FIXME: su/sudo stuff
@ -429,13 +391,13 @@ class Connection(ConnectionBase):
def put_file(self, in_path, out_path):
''' transfer a file from local to remote '''
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._host)
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
if not os.path.exists(in_path):
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
cmd = self._password_cmd()
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
host = self._host.ipv4_address
host = self._connection_info.remote_addr
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
# not sure if it's all working yet so this remains commented out
@ -461,16 +423,16 @@ class Connection(ConnectionBase):
def fetch_file(self, in_path, out_path):
''' fetch a file from remote to local '''
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._host)
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
cmd = self._password_cmd()
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
host = self._host.ipv4_address
host = self._connection_info.remote_addr
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
# not sure if it's all working yet so this remains commented out
#if self._ipv6:
# host = '[%s]' % self._host
# host = '[%s]' % self._connection_info.remote_addr
if C.DEFAULT_SCP_IF_SSH:
cmd += ["scp"] + self._common_args

@ -119,6 +119,9 @@
import os
from jinja2.exceptions import UndefinedError
from ansible.errors import AnsibleUndefinedVariable
from ansible.plugins.lookup import LookupBase
from ansible.template import Templar
from ansible.utils.boolean import boolean
@ -172,7 +175,11 @@ class LookupModule(LookupBase):
templar = Templar(loader=self._loader, variables=variables)
roledir = variables.get('roledir')
for fn in total_search:
fn = templar.template(fn)
try:
fn = templar.template(fn)
except (AnsibleUndefinedVariable, UndefinedError), e:
continue
if os.path.isabs(fn) and os.path.exists(fn):
return [fn]
else:

@ -85,8 +85,8 @@ class StrategyBase:
def get_hosts_remaining(self, play):
return [host for host in self._inventory.get_hosts(play.hosts) if host.name not in self._tqm._failed_hosts and host.get_name() not in self._tqm._unreachable_hosts]
def get_failed_hosts(self):
return [host for host in self._inventory.get_hosts() if host.name in self._tqm._failed_hosts]
def get_failed_hosts(self, play):
return [host for host in self._inventory.get_hosts(play.hosts) if host.name in self._tqm._failed_hosts]
def _queue_task(self, host, task, task_vars, connection_info):
''' handles queueing the task up to be sent to a worker '''
@ -129,6 +129,7 @@ class StrategyBase:
task = task_result._task
if result[0] == 'host_task_failed':
if not task.ignore_errors:
debug("marking %s as failed" % host.get_name())
self._tqm._failed_hosts[host.get_name()] = True
self._callback.runner_on_failed(task, task_result)
elif result[0] == 'host_unreachable':
@ -284,7 +285,7 @@ class StrategyBase:
result = True
debug("getting failed hosts")
failed_hosts = self.get_failed_hosts()
failed_hosts = self.get_failed_hosts(iterator._play)
if len(failed_hosts) == 0:
debug("there are no failed hosts")
return result
@ -317,8 +318,9 @@ class StrategyBase:
# pop the task, mark the host blocked, and queue it
self._blocked_hosts[host_name] = True
task = iterator.get_next_task_for_host(host)
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=task)
self._callback.playbook_on_cleanup_task_start(task.get_name())
self._queue_task(iterator._play, host, task, connection_info)
self._queue_task(host, task, task_vars, connection_info)
self._process_pending_results()
@ -352,8 +354,8 @@ class StrategyBase:
self._callback.playbook_on_handler_task_start(handler_name)
for host in self._notified_handlers[handler_name]:
if not handler.has_triggered(host):
temp_data = handler.serialize()
self._queue_task(iterator._play, host, handler, connection_info)
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=handler)
self._queue_task(host, handler, task_vars, connection_info)
handler.flag_for_host(host)
self._process_pending_results()

@ -76,7 +76,7 @@ class Display:
if host is None:
self.display(msg, color='blue')
else:
self.display("<%s> %s" % (host.name, msg), color='blue')
self.display("<%s> %s" % (host, msg), color='blue')
def deprecated(self, msg, version, removed=False):
''' used to print out a deprecation message.'''

@ -43,6 +43,8 @@ def secure_hash_s(data, hash_func=sha1):
digest = hash_func()
try:
if not isinstance(data, basestring):
data = "%s" % data
digest.update(data)
except UnicodeEncodeError:
digest.update(data.encode('utf-8'))

@ -39,14 +39,11 @@ def listify_lookup_plugin_terms(terms, variables, loader):
# with_items: {{ alist }}
stripped = terms.strip()
if not (stripped.startswith('{') or stripped.startswith('[')) and \
not stripped.startswith("/") and \
not stripped.startswith('set([') and \
not LOOKUP_REGEX.search(terms):
templar = Templar(loader=loader, variables=variables)
if not (stripped.startswith('{') or stripped.startswith('[')) and not stripped.startswith("/") and not stripped.startswith('set([') and not LOOKUP_REGEX.search(terms):
# if not already a list, get ready to evaluate with Jinja2
# not sure why the "/" is in above code :)
try:
templar = Templar(loader=loader, variables=variables)
new_terms = templar.template("{{ %s }}" % terms)
if isinstance(new_terms, basestring) and "{{" in new_terms:
pass
@ -54,6 +51,8 @@ def listify_lookup_plugin_terms(terms, variables, loader):
terms = new_terms
except:
pass
else:
terms = templar.template(terms)
if '{' in terms or '[' in terms:
# Jinja2 already evaluated a variable to a list.

@ -0,0 +1,17 @@
- hosts: localhost
connection: local
gather_facts: no
tasks:
- block:
- command: /bin/false
- debug: msg="you shouldn't see me"
rescue:
- debug: msg="this is the rescue"
- command: /bin/false
- debug: msg="you shouldn't see this either"
always:
- debug: msg="this is the always block, it will always be seen"
when: foo|default('') != "some value"
tags:
- foo
- bar
Loading…
Cancel
Save