Repository: ansible/ansible

Issue:
In extra vars  in json format List of string are casted as List for Lists
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
 2.1.0.0
```
##### CONFIGURATION

<!---
[default]
host_key_checking
hostfile
roles_path

[ssh]
control_path
ssh_args
-->
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
##### SUMMARY

<!--- Explain the problem briefly -->
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->

```
In the beginning of the playbook 
- hosts: '{{ targets }}'

in the command line I have 
ansible-playbook -i dyna-inventory.py  -e '{"foo": "bar", "targets": ["ec2-XXXXXXX.eu-west-1.compute.amazonaws.com"]}' myplaybook.yml 

To mention that this is working since 1.9.X and the only modification is a ansible upgrade

```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

nothing but my playbook running 
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

It's like there is a modification on the way  variables are casted when doing 
host = '{{ targets }}'
while my json definition specifies a string list  in targets 

```
ERROR! the field 'hosts' should be a list of (<type 'basestring'>,), but the item '[u'ec2-xxxxxxx-eu-west-1.compute.amazonaws.com']' is a <type 'list'>
```



Discussion:
I've verified that this works without Jinja2 in the mix,  eg:

```
- hosts:
  - host1
  - host2
```

I see where the underlying problem is between early validation, templating, and post-validation, and I see a way to fix it, but it'll require a fundamental change to our attribute processor/validator that we'd probably want to beat the crap out of in testing. The problem is that we're already typing the pre-templated hosts variable as a list of string (containing a single value `{{ targets }}`), then we're expanding targets in place, leading to the list-of-lists behavior (which causes the later validation error).

We'd probably want to add an opt-in "flatten" behavior to our FieldAttribute stuff that would detect this and do the right thing on attributes we _know_ aren't hierarchical like `hosts` (rather than doing it blindly to everything). 

As a workaround for this, you can pass a colon-separated list of hosts and it should work, eg:

```
ansible-playbook -i dyna-inventory.py  -e '{"foo": "bar", "targets": "ec2-XXXXXXX.eu-west-1.compute.amazonaws.com:someotherhost"}' myplaybook.yml 
```


Resolving patch:
diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py
--- a/lib/ansible/playbook/play.py
+++ b/lib/ansible/playbook/play.py
@@ -137,28 +137,6 @@ def preprocess_data(self, ds):
 
         return super(Play, self).preprocess_data(ds)
 
-    def _load_hosts(self, attr, ds):
-        '''
-        Loads the hosts from the given datastructure, which might be a list
-        or a simple string. We also switch integers in this list back to strings,
-        as the YAML parser will turn things that look like numbers into numbers.
-        '''
-
-        if isinstance(ds, (string_types, int)):
-            ds = [ ds ]
-
-        if not isinstance(ds, list):
-            raise AnsibleParserError("'hosts' must be specified as a list or a single pattern", obj=ds)
-
-        # YAML parsing of things that look like numbers may have
-        # resulted in integers showing up in the list, so convert
-        # them back to strings to prevent problems
-        for idx,item in enumerate(ds):
-            if isinstance(item, int):
-                ds[idx] = "%s" % item
-
-        return ds
-
     def _load_tasks(self, attr, ds):
         '''
         Loads a list of blocks from a list which may be mixed tasks/blocks.


Test patch:
diff --git a/test/integration/Makefile b/test/integration/Makefile
--- a/test/integration/Makefile
+++ b/test/integration/Makefile
@@ -27,7 +27,7 @@ UNAME := $(shell uname | tr '[:upper:]' '[:lower:]')
 
 all: setup other non_destructive destructive
 
-other: test_test_infra parsing test_var_precedence unicode test_templating_settings environment test_connection test_async_conditional includes blocks pull check_mode test_hash test_handlers test_group_by test_vault test_tags test_lookup_paths no_log test_gathering_facts test_binary_modules
+other: test_test_infra parsing test_var_precedence unicode test_templating_settings environment test_connection test_async_conditional includes blocks pull check_mode test_hash test_handlers test_group_by test_vault test_tags test_lookup_paths no_log test_gathering_facts test_binary_modules test_hosts_field
 
 test_test_infra:
 	# ensure fail/assert work locally and can stop execution with non-zero exit code
@@ -354,3 +354,32 @@ test_async:
 	# Verify that the warning exists by examining debug output.
 	ANSIBLE_DEBUG=1 LC_ALL=bogus ansible-playbook test_async.yml -i localhost, -e ansible_connection=ssh -v $(TEST_FLAGS) \
 	| grep 'bash: warning: setlocale: LC_ALL: cannot change locale (bogus)' > /dev/null
+
+test_hosts_field:
+	# Hosts in playbook has a list of strings consisting solely of digits
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t string_digit_host_in_list -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 1
+	# Hosts taken from kv extra_var on the CLI
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t hosts_from_kv_string -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 1
+	# hosts is taken from an all digit json extra_vars string on the CLI
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t hosts_from_cli_json_string -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 1
+	# hosts is taken from a json list in extra_vars on the CLI
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t hosts_from_cli_json_list -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	grep 'Running on localhost' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 2
+	# hosts is taken from a json string in an extra_vars file
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t hosts_from_json_file_string -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 1
+	# hosts is taken from a json list in an extra_vars file
+	ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e'target_kv=42' -e'{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" -t hosts_from_json_file_list -v $(TEST_FLAGS) | tee test_hosts_field.out
+	grep 'Running on 42' test_hosts_field.out 2>&1
+	grep 'Running on localhost' test_hosts_field.out 2>&1
+	test `grep -c 'ok=1' test_hosts_field.out` = 2
+	rm test_hosts_field.out
diff --git a/test/integration/inventory.hosts_field b/test/integration/inventory.hosts_field
new file mode 100644
--- /dev/null
+++ b/test/integration/inventory.hosts_field
@@ -0,0 +1,2 @@
+42 ansible_host=127.0.0.42 ansible_connection=local
+
diff --git a/test/integration/test_hosts_field.json b/test/integration/test_hosts_field.json
new file mode 100644
--- /dev/null
+++ b/test/integration/test_hosts_field.json
@@ -0,0 +1 @@
+{ "target_json_file": "42", "target_json_file_list": ["42", "localhost"] }
diff --git a/test/integration/test_hosts_field.yml b/test/integration/test_hosts_field.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/test_hosts_field.yml
@@ -0,0 +1,62 @@
+---
+#- name: Host in playbook is an integer
+#  hosts: 42
+#  tags: numeric_host
+#  tasks:
+#    - command: echo 'Running on {{ inventory_hostname }}'
+
+#- name: Host in playbook is a string of digits
+#  hosts: "42"
+#  tags: string_digit_host
+#  tasks:
+#    - command: echo 'Running on {{ inventory_hostname }}'
+
+#- name: Host in playbook is a list of integer
+#  hosts:
+#    - 42
+#  tags: numeric_host_in_list
+#  tasks:
+#    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Host in playbook is a list of strings of digits
+  hosts:
+    - "42"
+  gather_facts: False
+  tags: string_digit_host_in_list
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from kv extra_var on the CLI
+  hosts: "{{ target_kv }}"
+  gather_facts: False
+  tags: hosts_from_kv_string
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from a json string on the CLI
+  hosts: "{{ target_json_cli }}"
+  gather_facts: False
+  tags: hosts_from_cli_json_string
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from a json list on the CLI
+  hosts: "{{ target_json_cli_list }}"
+  gather_facts: False
+  tags: hosts_from_cli_json_list
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts is taken from a json string in an extra_vars file
+  hosts: "{{ target_json_file }}"
+  gather_facts: False
+  tags: hosts_from_json_file_string
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts is taken from a json list in an extra_vars file
+  hosts: "{{ target_json_file_list }}"
+  gather_facts: False
+  tags: hosts_from_json_file_list
+  tasks:
+    - command: echo 'Running on {{ inventory_hostname }}'