Repository: ansible/ansible

Issue:
Passing dict variable defined in group_vars to role fails when dict merging is enabled
**Ansible version:** 1.9.4

When dict merging is enabled, passing a dict as a role parameter produces an error.

**Example:**
Consider the following Ansible project:

```
├── ansible.cfg
├── group_vars
│   └── all
├── inventory
│   └── local.ini
├── roles
│   └── demo
│       └── tasks
│           └── main.yml
└── test.yml
```

_ansible.cfg_:

```
[defaults]
hash_behaviour = merge
```

_test.yml_

```

---
- hosts: localhost
  roles:
    - role: demo
      role_arg: "{{ foo }}"
```

_group_vars/all_:

```

---
foo:
  bar: baz
```

_inventory/local.ini_:

```
localhost ansible_connection=local
```

_roles/demo/tasks/main.yml_:

```

---
- debug: var=role_arg
```

**Expected behavior:**

```
$ ansible-playbook -i inventory/local.ini test.yml 

PLAY [localhost] **************************************************************

TASK: [demo | debug var=role_arg] ********************************************* 
ok: [localhost] => {
    "var": {
        "role_arg": {
            "bar": "baz"
        }
    }
}

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=0    unreachable=0    failed=0 
```

**Actual behavior:**

```
$ ansible-playbook -i inventory/local.ini test.yml 

PLAY [localhost] **************************************************************

TASK: [demo | debug var=role_arg] ********************************************* 
fatal: [localhost] => failed to combine variables, expected dicts but got a 'dict' and a 'unicode'

FATAL: all hosts have already failed -- aborting

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



Discussion:
This works as expected when dict merging is not enabled.  This also works when the variable is accessed through hostvars:

```
---
- hosts: localhost
  roles:
    - role: demo
      role_arg: "{{ hostvars[inventory_hostname]['foo'] }}"
```

We have the exact same error.
And it works fine with Ansible 2.0.0.2, but fails on 1.9.4 and 1.8.4.

When debugging it seems that it wants to merge a dictionary with a string, after digging into combine_vars(a, b) it appears that the **value of a** is a complete dict (with the contents of packages.foo_api already merged) and the **value of b** is the string "{{ packages.foo_api }}".

See https://github.com/ansible/ansible/blob/stable-1.9/lib/ansible/utils/__init__.py#L1501

Why the additional merge of something that was already successfully merged is beyond me at this point.

Here is our example:

**ansible.cfg**

``` ini
[defaults]
hostfile = hosts
hash_behaviour = merge
```

**group_vars/all**

``` yaml
packages:
  foo_api:
    name: api
    version: 1.2.3
    tomcat: 10
```

**roles/foo-framework/tasks/main.yml**

``` yaml
---
- name: '{{ foo.name }} : Test merging of vars'
  debug:
    var: foo
```

**test62.yml**

``` yaml
---
- hosts: localhost
  gather_facts: no
  roles:
    - role: foo-framework
      foo: '{{ packages.foo_api }}'
```

With the following output:

```
[dag@moria ansible.testing]$ swap-ansible ansible-1.9.4 
Changing Ansible version
    ansible-2.0.0.2  ->  ansible-1.9.4
[dag@moria ansible.testing]$ ansible-playbook -l localhost test62.yml

PLAY [localhost] ************************************************************** 

TASK: [foo-framework | {{ foo.name }} : Test merging of vars] ***************** 
fatal: [localhost] => failed to combine variables, expected dicts but got a 'dict' and a 'unicode'

FATAL: all hosts have already failed -- aborting

PLAY RECAP ******************************************************************** 
           to retry, use: --limit @/home/dag/test62.retry

localhost                  : ok=1    changed=0    unreachable=1    failed=0   

[dag@moria ansible.testing]$ swap-ansible ansible-2.0.0.2
Changing Ansible version
    ansible-1.9.4  ->  ansible-2.0.0.2
[dag@moria ansible.testing]$ ansible-playbook -l localhost test62.yml

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

TASK [foo-framework : api : Test merging of vars] ******************************
ok: [localhost] => {
    "foo": {
        "name": "api", 
        "tomcat": 10, 
        "version": "1.2.3"
    }
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   
```

Using **pdb**:

```
(Pdb) c
> /opt/ansible/ansible-1.9.4/lib/ansible/utils/__init__.py(816)merge_hash()
-> _validate_both_dicts(a, b)
(Pdb) a
a = {'version': '1.2.3', 'name': 'api', 'tomcat': 10}
b = {{ packages.foo_api }}
(Pdb) w
  /usr/local/bin/ansible-playbook(324)<module>()
-> sys.exit(main(sys.argv[1:]))
  /usr/local/bin/ansible-playbook(264)main()
-> pb.run()
  /opt/ansible/ansible-1.9.4/lib/ansible/playbook/__init__.py(348)run()
-> if not self._run_play(play):
  /opt/ansible/ansible-1.9.4/lib/ansible/playbook/__init__.py(789)_run_play()
-> if not self._run_task(play, task, False):
  /opt/ansible/ansible-1.9.4/lib/ansible/playbook/__init__.py(497)_run_task()
-> results = self._run_task_internal(task, include_failed=include_failed)
  /opt/ansible/ansible-1.9.4/lib/ansible/playbook/__init__.py(439)_run_task_internal()
-> results = runner.run()
  /opt/ansible/ansible-1.9.4/lib/ansible/runner/__init__.py(1498)run()
-> results = [ self._executor(h, None) for h in hosts ]
  /opt/ansible/ansible-1.9.4/lib/ansible/runner/__init__.py(586)_executor()
-> exec_rc = self._executor_internal(host, new_stdin)
  /opt/ansible/ansible-1.9.4/lib/ansible/runner/__init__.py(668)_executor_internal()
-> inject = self.get_inject_vars(host)
  /opt/ansible/ansible-1.9.4/lib/ansible/runner/__init__.py(647)get_inject_vars()
-> inject = utils.combine_vars(inject, self.role_params)
  /opt/ansible/ansible-1.9.4/lib/ansible/utils/__init__.py(1494)combine_vars()
-> return merge_hash(a, b)
  /opt/ansible/ansible-1.9.4/lib/ansible/utils/__init__.py(825)merge_hash()
-> result[k] = merge_hash(a[k], v)
> /opt/ansible/ansible-1.9.4/lib/ansible/utils/__init__.py(816)merge_hash()
-> _validate_both_dicts(a, b)
```

The issue here is that templating does not happen until the last possible moment (mostly task execution), in previous versions Ansible would template at several stages while ignoring errors as data was not available (which current behavior guarantees is an error and does not ignore). 


Resolving patch:
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -828,7 +828,7 @@ def merge_hash(a, b):
     for k, v in b.iteritems():
         # if there's already such key in a
         # and that key contains dict
-        if k in result and isinstance(result[k], dict):
+        if k in result and isinstance(result[k], dict) and isinstance(v, dict):
             # merge those dicts recursively
             result[k] = merge_hash(result[k], v)
         else:


Test patch: