Repository: ansible/ansible

Issue:
apt module somehow fails when using delegated variables in /etc/ansible/hosts
##### Issue Type:

Bug Report
##### Ansible Version:

```
ansible:
  Installed: 2.0.0.2-1ppa~trusty
  Candidate: 2.0.0.2-1ppa~trusty
  Version table:
 *** 2.0.0.2-1ppa~trusty 0
        500 http://ppa.launchpad.net/ansible/ansible/ubuntu/ trusty/main amd64 Packages
        100 /var/lib/dpkg/status
```
##### Ansible Configuration:

Not much of a difference, for this bug report I am using default Ansible modules: apt
##### Environment:

```
Linux playground 3.16.0-30-generic #40~14.04.1-Ubuntu SMP Thu Jan 15 17:43:14 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
```
##### Summary:

Should install the requested package through apt as desired.
##### Steps To Reproduce:
- /etc/ansible/hosts:

```
#
# This is the default ansible 'hosts' file.
#

#cluster overview

[controllers]
ctl01 ansible_host=10.100.69.171 hypervisor_name=mas01
ctl02 ansible_host=10.100.69.172 hypervisor_name=mas02
ctl03 ansible_host=10.100.69.173 hypervisor_name=mas03

[computenodes]
cmp01 ansible_host=10.100.69.181 hypervisor_name=hyp01

[storagenodes]
str01 ansible_host=10.100.69.191

#cluster details

[cluster:children]
controllers
computenodes
storagenodes

[cluster:vars]
cluster_name=testcluster100
install_master_ip=10.100.69.171
hypervisor_type=KVM
```
- Playbook:

```

---
- hosts: test
  gather_facts: false

  tasks:

    - name: apt install
      apt: name=htop state=present force=yes update_cache=yes
      delegate_to: "{{item}}"
      with_items: "{{groups['storagenodes']}}"
```
##### Expected Results:

Installation succeeded
##### Actual Results:

```
Using /etc/ansible/ansible.cfg as config file
SSH password: 
Loaded callback default of type stdout, v2.0
1 plays in test.yml

PLAY ***************************************************************************

TASK [apt install] *************************************************************
task path: /home/jonas/operations/gmc_kvm_project/testing/ansible/test_ovs.yml:15
An exception occurred during task execution. The full traceback is:
Traceback (most recent call last):
  File "/usr/lib/pymodules/python2.7/ansible/executor/process/worker.py", line 114, in run
    self._shared_loader_obj,
  File "/usr/lib/pymodules/python2.7/ansible/executor/task_executor.py", line 91, in run
    item_results = self._run_loop(items)
  File "/usr/lib/pymodules/python2.7/ansible/executor/task_executor.py", line 241, in _run_loop
    res = self._execute(variables=task_vars)
  File "/usr/lib/pymodules/python2.7/ansible/executor/task_executor.py", line 313, in _execute
    self._play_context = self._play_context.set_task_and_variable_override(task=self._task, variables=variables, templar=templar)
  File "/usr/lib/pymodules/python2.7/ansible/playbook/play_context.py", line 330, in set_task_and_variable_override
    delegated_vars = variables.get('ansible_delegated_vars', dict()).get(delegated_host_name, dict())
TypeError: unhashable type: 'list'

fatal: [127.0.0.1]: FAILED! => {"failed": true, "stdout": ""}

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=0    changed=0    unreachable=0    failed=1   
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -269,28 +269,39 @@ def _squash_items(self, items, variables):
             if all(isinstance(o, string_types) for o in items):
                 final_items = []
                 name = self._task.args.pop('name', None) or self._task.args.pop('pkg', None)
-                # The user is doing an upgrade or some other operation
-                # that doesn't take name or pkg.
-                if name:
+
+                # This gets the information to check whether the name field
+                # contains a template that we can squash for
+                template_no_item = template_with_item = None
+                if templar._contains_vars(name):
+                    variables['item'] = '\0$'
+                    template_no_item = templar.template(name, variables, cache=False)
+                    variables['item'] = '\0@'
+                    template_with_item = templar.template(name, variables, cache=False)
+                    del variables['item']
+
+                # Check if the user is doing some operation that doesn't take
+                # name/pkg or the name/pkg field doesn't have any variables
+                # and thus the items can't be squashed
+                if name and (template_no_item != template_with_item):
                     for item in items:
                         variables['item'] = item
                         if self._task.evaluate_conditional(templar, variables):
-                            if templar._contains_vars(name):
-                                new_item = templar.template(name, cache=False)
-                                final_items.append(new_item)
-                            else:
-                                final_items.append(item)
+                            new_item = templar.template(name, cache=False)
+                            final_items.append(new_item)
                     self._task.args['name'] = final_items
+                    # Wrap this in a list so that the calling function loop
+                    # executes exactly once
                     return [final_items]
+                else:
+                    # Restore the name parameter
+                    self._task.args['name'] = name
             #elif:
                 # Right now we only optimize single entries.  In the future we
                 # could optimize more types:
                 # * lists can be squashed together
                 # * dicts could squash entries that match in all cases except the
                 #   name or pkg field.
-                # Note: we really should be checking that the name or pkg field
-                # contains a template that expands with our with_items values.
-                # If it doesn't then we may break things
         return items
 
     def _execute(self, variables=None):


Test patch:
diff --git a/test/units/executor/test_task_executor.py b/test/units/executor/test_task_executor.py
--- a/test/units/executor/test_task_executor.py
+++ b/test/units/executor/test_task_executor.py
@@ -198,21 +198,47 @@ def _evaluate_conditional(templar, variables):
             shared_loader_obj = mock_shared_loader,
         )
 
+        #
+        # No replacement
+        #
+        mock_task.action = 'yum'
+        new_items = te._squash_items(items=items, variables=job_vars)
+        self.assertEqual(new_items, ['a', 'b', 'c'])
+
         mock_task.action = 'foo'
+        mock_task.args={'name': '{{item}}'}
+        new_items = te._squash_items(items=items, variables=job_vars)
+        self.assertEqual(new_items, ['a', 'b', 'c'])
+
+        mock_task.action = 'yum'
+        mock_task.args={'name': 'static'}
+        new_items = te._squash_items(items=items, variables=job_vars)
+        self.assertEqual(new_items, ['a', 'b', 'c'])
+
+        mock_task.action = 'yum'
+        mock_task.args={'name': '{{pkg_mgr}}'}
         new_items = te._squash_items(items=items, variables=job_vars)
         self.assertEqual(new_items, ['a', 'b', 'c'])
 
+        #
+        # Replaces
+        #
         mock_task.action = 'yum'
+        mock_task.args={'name': '{{item}}'}
         new_items = te._squash_items(items=items, variables=job_vars)
         self.assertEqual(new_items, [['a','c']])
 
         mock_task.action = '{{pkg_mgr}}'
+        mock_task.args={'name': '{{item}}'}
         new_items = te._squash_items(items=items, variables=job_vars)
         self.assertEqual(new_items, [['a', 'c']])
 
+        #
         # Smoketests -- these won't optimize but make sure that they don't
         # traceback either
+        #
         mock_task.action = '{{unknown}}'
+        mock_task.args={'name': '{{item}}'}
         new_items = te._squash_items(items=items, variables=job_vars)
         self.assertEqual(new_items, ['a', 'b', 'c'])
 
@@ -220,6 +246,7 @@ def _evaluate_conditional(templar, variables):
                 dict(name='b', state='present'),
                 dict(name='c', state='present')]
         mock_task.action = 'yum'
+        mock_task.args={'name': '{{item}}'}
         new_items = te._squash_items(items=items, variables=job_vars)
         self.assertEqual(new_items, items)