From f07e55c5681649b810026ca16746e7984ec1fbfb Mon Sep 17 00:00:00 2001
From: Michael DeHaan <michael.dehaan@gmail.com>
Date: Sat, 18 Aug 2012 08:46:51 -0400
Subject: [PATCH] Adds 'delegate_to' as a task option which can be used to
 signal load balancers and outage windows.

---
 CHANGELOG.md                   |  1 +
 lib/ansible/playbook/task.py   | 13 +++++--
 lib/ansible/runner/__init__.py | 69 ++++++++++++++++++++++------------
 3 files changed, 56 insertions(+), 27 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bd0cce48bd..bd64c0c4965 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 d861c0ebd8f..9dc12146345 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 366875c5e2e..5eb3f5e46ec 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
 
     # *****************************************************