Repository: ansible/ansible
Pull request: validate vars_files entries are string_type or list
Signed-off-by: Adam Miller <admiller@redhat.com>

##### SUMMARY

Validate `vars_files` entries to ensure they are of the correct type and display an useful error message otherwise.

Fixes #17594


##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->

 - Bugfix Pull Request

##### COMPONENT NAME
<!--- Name of the module, plugin, module or task -->
lib/ansible/vars/manager.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.7.0.dev0 (validate/vars_files 007fdde3af) last updated 2018/05/31 16:42:10 (GMT -500)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/admiller/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/admiller/src/dev/ansible/lib/ansible
  executable location = /home/admiller/src/dev/ansible/bin/ansible
  python version = 2.7.15 (default, May 15 2018, 15:37:31) [GCC 7.3.1 20180303 (Red Hat 7.3.1-5)]
```




Problem statement:
Parsing error results in ERROR! Unexpected Exception: 0
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

parsing/quoting.py
##### ANSIBLE VERSION

```
ansible 2.1.1.0
  config file = /home/lmadsen/src/github/leifmadsen/ansible-cira/ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

```
[defaults]
roles_path = ./roles
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ~/.ansible/cachedir
fact_caching_timeout = 86400
host_key_checking = False
inventory = ./hosts/

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o ForwardAgent=yes
```
##### OS / ENVIRONMENT

Fedora 24
##### SUMMARY

When failing to provide a `tasks:` value about a named play, `ERROR! Unexpected Exception: 0` is returned.
##### STEPS TO REPRODUCE

Fail to provide a `tasks:` header above a named play.

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

```
# vim: ft=ansible                                                               
---                                                                             
- hosts: jenkins_master                                                         
  tags:                                                                         
    - jenkins_master                                                            
    - jenkins_ssh                                                               

  vars_files:                                                                   
    - "{{ inventory_dir  }}/../vars/main.yml"                                   
    - ~/.ansible/vars/cira_vars.yml                                             

    - name: Create Jenkins user .ssh directory                                  
      file:                                                                     
        path: "{{ jenkins_master_ssh_directory }}"                              
        state: directory                                                        
        owner: jenkins                                                          
        group: jenkins                                                          
        mode: "0700"
```

_Note_ the missing `tasks:` above `- name: Create...`

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

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

Ansible to nicely exit pointing to line in file that couldn't be parsed appropriately.
##### ACTUAL RESULTS

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

Ansible crashes and outputs information that isn't overly helpful to narrowing down where the parsing error happened in the file.

<!--- Paste verbatim command output between quotes below -->

```
ansible-playbook -vvv --syntax-check jenkins_ssh.yml 
Using /home/lmadsen/src/github/leifmadsen/ansible-cira/ansible.cfg as config file
1 plays in jenkins_ssh.yml
ERROR! Unexpected Exception: 0
the full traceback was:

Traceback (most recent call last):
  File "/usr/bin/ansible-playbook", line 86, in <module>
    sys.exit(cli.run())
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/cli/playbook.py", line 154, in run
    results = pbex.run()
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/executor/playbook_executor.py", line 117, in run
    all_vars = self._variable_manager.get_vars(loader=self._loader, play=play)
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/vars/__init__.py", line 305, in get_vars
    data = preprocess_vars(loader.load_from_file(vars_file))
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/parsing/dataloader.py", line 105, in load_from_file
    file_name = self.path_dwim(file_name)
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/parsing/dataloader.py", line 212, in path_dwim
    given = unquote(given)
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/parsing/quoting.py", line 28, in unquote
    if is_quoted(data):
  File "/home/lmadsen/.local/lib/python2.7/site-packages/ansible/parsing/quoting.py", line 24, in is_quoted
    return len(data) > 1 and data[0] == data[-1] and data[0] in ('"', "'") and data[-2] != '\\'
KeyError: 0
```

Hints from issue discussion:
+1 
Ansible version: ansible 2.3.0.0



> 2017-05-16 16:49:35,593 p=2435 u=root |  META: ran handlers
> 2017-05-16 16:49:35,594 p=2435 u=root |  META: ran handlers
> 2017-05-16 16:49:35,601 p=2435 u=root |  ERROR! Unexpected Exception: 0
> 2017-05-16 16:49:35,603 p=2435 u=root |  the full traceback was:
> 
> Traceback (most recent call last):
>   File "/usr/local/bin/ansible-playbook", line 109, in <module>
>     exit_code = cli.run()
>   File "/usr/local/lib/python2.7/dist-packages/ansible/cli/playbook.py", line 154, in run
>     results = pbex.run()
>   File "/usr/local/lib/python2.7/dist-packages/ansible/executor/playbook_executor.py", line 125, in run
>     all_vars = self._variable_manager.get_vars(loader=self._loader, play=play)
>   File "/usr/local/lib/python2.7/dist-packages/ansible/vars/__init__.py", line 312, in get_vars
>     data = preprocess_vars(loader.load_from_file(vars_file))
>   File "/usr/local/lib/python2.7/dist-packages/ansible/parsing/dataloader.py", line 110, in load_from_file
>     file_name = self.path_dwim(file_name)
>   File "/usr/local/lib/python2.7/dist-packages/ansible/parsing/dataloader.py", line 218, in path_dwim
>     given = unquote(given)
>   File "/usr/local/lib/python2.7/dist-packages/ansible/parsing/quoting.py", line 28, in unquote
>     if is_quoted(data):
>   File "/usr/local/lib/python2.7/dist-packages/ansible/parsing/quoting.py", line 24, in is_quoted
>     return len(data) > 1 and data[0] == data[-1] and data[0] in ('"', "'") and data[-2] != '\\'
> KeyError: 0
+1 for ansible 2.3.0:

```
ERROR! Unexpected Exception: 0
the full traceback was:

Traceback (most recent call last):
  File ".venv/bin/ansible-playbook", line 109, in <module>
    exit_code = cli.run()
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/cli/playbook.py", line 154, in run
    results = pbex.run()
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/executor/playbook_executor.py", line 125, in run
    all_vars = self._variable_manager.get_vars(loader=self._loader, play=play)
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/vars/__init__.py", line 312, in get_vars
    data = preprocess_vars(loader.load_from_file(vars_file))
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/parsing/dataloader.py", line 110, in load_from_file
    file_name = self.path_dwim(file_name)
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/parsing/dataloader.py", line 218, in path_dwim 
    given = unquote(given)
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/parsing/quoting.py", line 28, in unquote
    if is_quoted(data):
  File "/home/user/ansible-playbook/.venv/local/lib/python2.7/site-packages/ansible/parsing/quoting.py", line 24, in is_quoted 
    return len(data) > 1 and data[0] == data[-1] and data[0] in ('"', "'") and data[-2] != '\\'
KeyError: 0
```

This occurs with the following code:

```
- hosts: localhost
  become: false
  gather_facts: false
  vars_files:
    - { var_file: 'group_vars/vagrant.yml', when: "groups['vagrant'] is not defined" }
```
@leifmadsen I'm unable to reproduce this with your example in Ansible 2.5.3, can you please verify this is resolved for you?
@kayrus what you're attempting to do is not supported, but we should definitely handle that with a graceful error message

Commit message: validate vars_files entries are string_type or list

Fixes #17594

Signed-off-by: Adam Miller <admiller@redhat.com>

Patch to review:
diff --git a/lib/ansible/vars/manager.py b/lib/ansible/vars/manager.py
index 6aa3cd6397cd5b..4271f6da9ef77e 100644
--- a/lib/ansible/vars/manager.py
+++ b/lib/ansible/vars/manager.py
@@ -36,7 +36,7 @@
 from ansible.inventory.host import Host
 from ansible.inventory.helpers import sort_groups, get_group_vars
 from ansible.module_utils._text import to_native
-from ansible.module_utils.six import iteritems, text_type
+from ansible.module_utils.six import iteritems, text_type, string_types
 from ansible.plugins.loader import lookup_loader, vars_loader
 from ansible.plugins.cache import FactCache
 from ansible.template import Templar
@@ -340,6 +340,12 @@ def plugins_by_groups():
                     try:
                         for vars_file in vars_file_list:
                             vars_file = templar.template(vars_file)
+                            if not (isinstance(vars_file, string_types) or isinstance(vars_file, list)):
+                                raise AnsibleError(
+                                    "Invalid vars_files entry found: %r\n"
+                                    "vars_files entries should be either a string type or "
+                                    "a list of string types after template expansion" % vars_file
+                                )
                             try:
                                 data = preprocess_vars(self._loader.load_from_file(vars_file, unsafe=True))
                                 if data is not None:


Code review comments:
diff_hunk: @@ -340,6 +340,12 @@ def plugins_by_groups():
                     try:
                         for vars_file in vars_file_list:
                             vars_file = templar.template(vars_file)
+                            if not (isinstance(vars_file, string_types) or isinstance(vars_file, list)):
path: lib/ansible/vars/manager.py
text: @user1:
Sequence should cover both