Repository: ansible/ansible

Issue:
`AttributeError: 'unicode' object has no attribute 'items'` on  `ansible/inventory/__init__.py", line 472, in _get_variables`
##### Issue Type: Bug report
##### Ansible Version: `ansible 1.7.1` (I can't reproduce the bug with 1.6.10).
##### Environment: Mac OS X 10.9.4
##### Summary: `AttributeError: 'unicode' object has no attribute 'items'` on  `ansible/inventory/__init__.py", line 472, in _get_variables`
##### Steps To Reproduce:

Running any of my playbooks, it seems to be related to the vars of my hosts. For instance: 

```
ansible-playbook -i staging site.yml -l app --tags jt --diff --verbose
```
##### Expected Results:

Well, the playbook would be run :) As I said above, it works just fine in ansible 1.6.10.
##### Actual Results:

I get this traceback:

``` pytb
{21-08-2014 12:33}rigoletto:~/src/jt/system/ansible@master✗✗✗ teo% ansible-playbook -i staging site.yml -l app --tags jt --diff --verbose --check
Traceback (most recent call last):
  File "/usr/local/Cellar/ansible/1.7.1/libexec/bin/ansible-playbook", line 5, in <module>
    pkg_resources.run_script('ansible==1.7.1', 'ansible-playbook')
  File "/Library/Python/2.7/site-packages/distribute-0.6.49-py2.7.egg/pkg_resources.py", line 507, in run_script
    self.require(requires)[0].run_script(script_name, ns)
  File "/Library/Python/2.7/site-packages/distribute-0.6.49-py2.7.egg/pkg_resources.py", line 1272, in run_script
    execfile(script_filename, namespace, namespace)
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/EGG-INFO/scripts/ansible-playbook", line 309, in <module>
    sys.exit(main(sys.argv[1:]))
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/EGG-INFO/scripts/ansible-playbook", line 150, in main
    inventory = ansible.inventory.Inventory(options.inventory, vault_password=vault_pass)
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/ansible/inventory/__init__.py", line 149, in __init__
    host.vars = utils.combine_vars(host.vars, self.get_variables(host.name, vault_password=self._vault_password))
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/ansible/inventory/__init__.py", line 442, in get_variables
    self._vars_per_host[hostname] = self._get_variables(hostname, vault_password=vault_password)
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/ansible/inventory/__init__.py", line 472, in _get_variables
    vars = utils.combine_vars(vars, self.parser.get_host_variables(host))
  File "/usr/local/Cellar/ansible/1.7.1/lib/python2.7/site-packages/ansible-1.7.1-py2.7.egg/ansible/utils/__init__.py", line 1278, in combine_vars
    return dict(a.items() + b.items())
AttributeError: 'unicode' object has no attribute 'items'
```



Discussion:
we can definitely look into this traceback, but would it be possible for you to share a minimal playbook that can reproduce this problem?

Ideally this should be a short (half page or so) file, mostly using modules that don't change things, and your variable file.

Also, if you can check to see if this happens for you on the development branch - seeing this may be hard to reproduce, this would be greatly appreciated.  Thanks!

@teoruiz this is a bug in the inventory parsing, if you could show us an inventory, with variables, where this bug shows? A playbook will not be necessary. 

This sounds like a bad variable, I suspect it comes from a dynamic inventory script.

@mpdehaan it does fail with the latest version from git, `ansible-playbook 1.8 (devel 8a1fbed5d6) last updated 2014/08/21 16:11:04 (GMT +200)`.

@sergevanginderachter it is indeed a (dynamic) inventory problem. I can make it fail with this JSON (IP addresses redacted), generated dynamically:

``` json
{
  "app1": {
    "ansible_ssh_host": "10.0.0.0",
    "name": "app1",
    "role": "app",
    "private_ip": "10.0.0.0"
  },
  "app2": {
    "ansible_ssh_host": "10.0.0.0",
    "name": "app2",
    "role": "app",
    "private_ip": "10.0.0.0"
  },
  "localhost": "127.0.0.1 env=staging"
}
```

However it does work with this JSON (notice the missing localhost entry at the end).

``` json
{
  "app1": {
    "ansible_ssh_host": "10.0.0.0",
    "name": "app1",
    "role": "app",
    "private_ip": "10.0.0.0"
  },
  "app2": {
    "ansible_ssh_host": "10.0.0.0",
    "name": "app2",
    "role": "app",
    "private_ip": "10.0.0.0"
  }
}
```

@teoruiz `127.0.0.1 env=staging` is a syntax possible in ini files, but not in the dynamic script json, in older ansible versions I think the "127.0.0.1 env=staging" part would be ignored, but not really sure;
also, that part must be a dict, not a string, as in  

`"localhost": "127.0.0.1 env=staging"` ->  `"localhost": {"ansible_ssh_host": 127.0.0.1 env=staging}`

this being a string and not a dict is the specific error you get in the trace.

Now, the problem is a syntax problem in your json, but we should avoid the trace, and return a proper error. I'll look at that, as I recall getting traces for that before.

@sergevanginderachter totally with you on our wrong syntax, I already fixed our dynamic inventory script. 

I just wanted to report that ugly trace :angel:


Resolving patch:
diff --git a/lib/ansible/inventory/script.py b/lib/ansible/inventory/script.py
--- a/lib/ansible/inventory/script.py
+++ b/lib/ansible/inventory/script.py
@@ -84,10 +84,14 @@ def _parse(self, err):
 
             if not isinstance(data, dict):
                 data = {'hosts': data}
+            # is not those subkeys, then simplified syntax, host with vars
             elif not any(k in data for k in ('hosts','vars')):
                 data = {'hosts': [group_name], 'vars': data}
 
             if 'hosts' in data:
+                if not isinstance(data['hosts'], list):
+                    raise errors.AnsibleError("You defined a group \"%s\" with bad "
+                        "data for the host list:\n %s" % (group_name, data))
 
                 for hostname in data['hosts']:
                     if not hostname in all_hosts:
@@ -96,6 +100,10 @@ def _parse(self, err):
                     group.add_host(host)
 
             if 'vars' in data:
+                if not isinstance(data['vars'], dict):
+                    raise errors.AnsibleError("You defined a group \"%s\" with bad "
+                        "data for variables:\n %s" % (group_name, data))
+
                 for k, v in data['vars'].iteritems():
                     if group.name == all.name:
                         all.set_variable(k, v)
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
@@ -696,10 +696,17 @@ def parse_kv(args):
                 options[k.strip()] = unquote(v.strip())
     return options
 
+def _validate_both_dicts(a, b):
+
+    if not (isinstance(a, dict) and isinstance(b, dict)):
+        raise errors.AnsibleError("Failed to combine two values which are not "
+            "both hashes, got these differing values now:\n\n%s\n and\n%s" % (a, b))
+
 def merge_hash(a, b):
     ''' recursively merges hash b into a
     keys from b take precedence over keys from a '''
 
+    _validate_both_dicts(a, b)
     result = {}
 
     for dicts in a, b:
@@ -1308,6 +1315,8 @@ def listify_lookup_plugin_terms(terms, basedir, inject):
 
 def combine_vars(a, b):
 
+    _validate_both_dicts(a, b)
+
     if C.DEFAULT_HASH_BEHAVIOUR == "merge":
         return merge_hash(a, b)
     else:


Test patch: