diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd0cce48b..bd64c0c496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Ansible Changes By Release * ANSIBLE_KEEP_REMOTE_FILES=1 can be used in debugging (envrionment variable) * add pattern= as a paramter to the service module * various fixes to mysql & postresql modules +* adds 'delegate_to' for a task, which can be used to signal outage windows and load balancers on behalf of hosts 0.6 "Cabo" -- August 6, 2012 diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index d861c0ebd8..9dc1214634 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -23,13 +23,15 @@ class Task(object): __slots__ = [ 'name', 'action', 'only_if', 'async_seconds', 'async_poll_interval', 'notify', 'module_name', 'module_args', 'module_vars', - 'play', 'notified_by', 'tags', 'register', 'with_items', 'first_available_file', 'ignore_errors' + 'play', 'notified_by', 'tags', 'register', 'with_items', + 'delegate_to', 'first_available_file', 'ignore_errors' ] # to prevent typos and such VALID_KEYS = [ - 'name', 'action', 'only_if', 'async', 'poll', 'notify', 'with_items', 'first_available_file', - 'include', 'tags', 'register', 'ignore_errors' + 'name', 'action', 'only_if', 'async', 'poll', 'notify', 'with_items', + 'first_available_file', 'include', 'tags', 'register', 'ignore_errors', + 'delegate_to' ] def __init__(self, play, ds, module_vars=None): @@ -63,6 +65,8 @@ class Task(object): self.notify = ds.get('notify', []) self.first_available_file = ds.get('first_available_file', None) self.with_items = ds.get('with_items', None) + self.delegate_to = ds.get('delegate_to', None) + self.ignore_errors = ds.get('ignore_errors', False) # notify can be a string or a list, store as a list @@ -99,6 +103,9 @@ class Task(object): self.with_items = [ ] self.module_vars['items'] = self.with_items + # allow runner to see delegate_to option + self.module_vars['delegate_to'] = self.delegate_to + # make ignore_errors accessable to Runner code self.module_vars['ignore_errors'] = self.ignore_errors diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index 366875c5e2..5eb3f5e46e 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -70,15 +70,25 @@ class ReturnData(object): __slots__ = [ 'result', 'comm_ok', 'host' ] - def __init__(self, host=None, result=None, comm_ok=True): - self.host = host + def __init__(self, conn=None, host=None, result=None, comm_ok=True): + + # which host is this ReturnData about? + if conn is not None: + delegate_for = getattr(conn, '_delegate_for', None) + if delegate_for: + self.host = delegate_for + else: + self.host = conn.host + else: + self.host = host + self.result = result self.comm_ok = comm_ok if type(self.result) in [ str, unicode ]: self.result = utils.parse_json(self.result) - if host is None: + if self.host is None: raise Exception("host not set") if type(self.result) != dict: raise Exception("dictionary result expected") @@ -236,13 +246,13 @@ class Runner(object): cmd = " ".join([str(x) for x in [remote_module_path, async_jid, async_limit, async_module]]) res = self._low_level_exec_command(conn, cmd, tmp, sudoable=True) - return ReturnData(host=conn.host, result=res) + return ReturnData(conn=conn, result=res) # ***************************************************** def _execute_raw(self, conn, tmp, inject=None): ''' execute a non-module command for bootstrapping, or if there's no python on a device ''' - return ReturnData(host=conn.host, result=dict( + return ReturnData(conn=conn, result=dict( stdout=self._low_level_exec_command(conn, self.module_args, tmp, sudoable = True) )) @@ -292,7 +302,7 @@ class Runner(object): dest = options.get('dest', None) if (source is None and not 'first_available_file' in inject) or dest is None: result=dict(failed=True, msg="src and dest are required") - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) # if we have first_available_file in our vars # look up the files and use the first one we find as src @@ -306,7 +316,7 @@ class Runner(object): break if not found: results=dict(failed=True, msg="could not find src in first_available_file list") - return ReturnData(host=conn.host, results=results) + return ReturnData(conn=conn, results=results) source = utils.template(source, inject) source = utils.path_dwim(self.basedir, source) @@ -314,7 +324,7 @@ class Runner(object): local_md5 = utils.md5(source) if local_md5 is None: result=dict(failed=True, msg="could not find src=%s" % source) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) remote_md5 = self._remote_md5(conn, tmp, dest) @@ -334,7 +344,7 @@ class Runner(object): else: # no need to transfer the file, already correct md5 result = dict(changed=False, md5sum=remote_md5, transferred=False) - return ReturnData(host=conn.host, result=result).daisychain('file') + return ReturnData(conn=conn, result=result).daisychain('file') # ***************************************************** @@ -347,7 +357,7 @@ class Runner(object): dest = options.get('dest', None) if source is None or dest is None: results = dict(failed=True, msg="src and dest are required") - return ReturnData(host=conn.host, result=results) + return ReturnData(conn=conn, result=results) # apply templating to source argument source = utils.template(source, inject) @@ -365,13 +375,13 @@ class Runner(object): # but keep going to fetch other log files if remote_md5 == '0': result = dict(msg="unable to calculate the md5 sum of the remote file", file=source, changed=False) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) if remote_md5 == '1': result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) if remote_md5 == '2': result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) # calculate md5 sum for the local file local_md5 = utils.md5(dest) @@ -386,12 +396,12 @@ class Runner(object): new_md5 = utils.md5(dest) if new_md5 != remote_md5: result = dict(failed=True, md5sum=new_md5, msg="md5 mismatch", file=source) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) result = dict(changed=True, md5sum=new_md5) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) else: result = dict(changed=False, md5sum=local_md5, file=source) - return ReturnData(host=conn.host, result=result) + return ReturnData(conn=conn, result=result) # ***************************************************** @@ -407,7 +417,7 @@ class Runner(object): dest = options.get('dest', None) if (source is None and 'first_available_file' not in inject) or dest is None: result = dict(failed=True, msg="src and dest are required") - return ReturnData(host=conn.host, comm_ok=False, result=result) + return ReturnData(conn=conn, comm_ok=False, result=result) # if we have first_available_file in our vars # look up the files and use the first one we find as src @@ -421,7 +431,7 @@ class Runner(object): break if not found: result = dict(failed=True, msg="could not find src in first_available_file list") - return ReturnData(host=conn.host, comm_ok=False, result=result) + return ReturnData(conn=conn, comm_ok=False, result=result) source = utils.template(source, inject) @@ -430,7 +440,8 @@ class Runner(object): resultant = utils.template_from_file(self.basedir, source, inject) except Exception, e: result = dict(failed=True, msg=str(e)) - return ReturnData(host=conn.host, comm_ok=False, result=result) + return ReturnData(conn=conn, comm_ok=False, result=result) + xfered = self._transfer_str(conn, tmp, 'source', resultant) # run the copy module, queue the file module @@ -483,8 +494,10 @@ class Runner(object): inject.update(self.module_vars) inject['hostvars'] = self.setup_cache - items = self.module_vars.get('items', []) + # allow with_items to work in playbooks... + # apt and yum are converted into a single call, others run in a loop + items = self.module_vars.get('items', []) if isinstance(items, basestring) and items.startswith("$"): items = items.replace("$","") if items in inject: @@ -499,6 +512,8 @@ class Runner(object): inject['item'] = ",".join(items) items = [] + # logic to decide how to run things depends on whether with_items is used + if len(items) == 0: return self._executor_internal_inner(host, inject, port) else: @@ -570,8 +585,14 @@ class Runner(object): return ReturnData(host=host, result=result) conn = None + actual_host = host try: - conn = self.connector.connect(host, port) + delegate_to = inject.get('delegate_to', None) + if delegate_to is not None: + actual_host = delegate_to + conn = self.connector.connect(actual_host, port) + if delegate_to is not None: + conn._delegate_for = host except errors.AnsibleConnectionFailed, e: result = dict(failed=True, msg="FAILED: %s" % str(e)) return ReturnData(host=host, comm_ok=False, result=result) @@ -627,17 +648,17 @@ class Runner(object): # no callbacks return result if 'skipped' in data: - self.callbacks.on_skipped(result.host) + self.callbacks.on_skipped(host) elif not result.is_successful(): ignore_errors = self.module_vars.get('ignore_errors', False) - self.callbacks.on_failed(result.host, data, ignore_errors) + self.callbacks.on_failed(host, data, ignore_errors) if ignore_errors: if 'failed' in result.result: result.result['failed'] = False if 'rc' in result.result: result.result['rc'] = 0 else: - self.callbacks.on_ok(result.host, data) + self.callbacks.on_ok(host, data) return result # *****************************************************