18

That's the part:

vars_files:
  - vars/vars.default.yml
  - vars/vars.yml

If a file vars/vars.yml does not exist - here is an error.

ERROR: file could not read: /.../vars/vars.yml

How can I load additional variables from this file only if it exists? (with no errors)

7 Answers 7

26

It's quite simple really. You can squash your different vars_files items into a single tuple and Ansible will automatically go through each one until it finds a file that exists and load it. E.x.:

vars_files:
  - [ "vars/foo.yml", "vars/bar.yml", "vars/default.yml" ]
1
  • 9
    According to Ansible developers, this solution will load all of the files, not just the first one found.
    – tjanez
    Nov 24, 2014 at 12:13
14

According to Ansible developers, the proper way to solve this is to use something like:

vars_files_locs: ['../path/to/file1', '../path/to/file2', ...]

- include_vars: "{{ item }}"
  with_first_found: vars_files_locs

Furthermore, they say:

The above will properly load only the first file found, and is more flexible than trying to do this via the vars_files language keyword.

3
  • "only the first file found" - the idea was to redefine some variables, not all of them
    – Sergey
    Nov 24, 2014 at 17:44
  • @Sergey, reading again your question, I see that what you wanted is a bit different. Thanks for pointing this out. I'll leave the answer as is if someone else finds it useful.
    – tjanez
    Nov 25, 2014 at 9:00
  • 1
    except that include_vars in the task will give a high precedence of variables compared to role defaults or vars
    – Alex F
    Mar 21, 2016 at 8:51
3
- hosts: all
  vars_files: vars/vars.default.yml
  vars:
    optional_vars_file: "{{ lookup('first_found', 'vars/vars.yml', errors='ignore') }}"
  tasks:
  - when: optional_vars_file is file
    include_vars: "{{ optional_vars_file }}"

Note: The path tests (is file, is exists,...) work only with absolute paths or paths relative to the current working directory when running ansible-playbook command. This is the reason we used the lookup. the lookup accepts paths relative to the playbook directory and returns the absolute path when the file exists.

2

I encountered this problem in a setup where I needed to create multiple deployment environments (live, demo, sandbox) to the same physical server (not allowed virtual machines here), and then a script to deploy arbitrary svn repos

This required a directory tree of (optional) variable.yml files, that would merge ontop of each other and not throw an exception if any where missing

Start by enabling variable merging in ansible - note that this does shallow hash merging (1 level deep) and not fully recursive deep merge

ansible.cfg

[defaults]
hash_behaviour=merge ;; merge rather than replace dictionaries http://docs.ansible.com/ansible/intro_configuration.html###hash-behaviour

Ansible directory layout

/group_vars
└── all.yml

/playbooks
├── boostrap.yml
├── demo.yml
├── live.yml
└── sandbox.yml

/roles/deploy/
├── files
├── tasks
│   ├── includes.yml
│   ├── main.yml
└── vars
    ├── main.yml
    ├── project_1.yml
    ├── project_2.yml
    ├── demo
    │   ├── project_1.yml
    │   ├── project_2.yml   
    │   └── main.yml
    ├── live
    │   ├── project_1.yml
    │   ├── project_2.yml   
    │   └── main.yml
    └── sandbox
        ├── project_1.yml
        ├── project_2.yml   
        └── main.yml

roles/deploy/tasks/includes.yml

This is the main logic for a directory tree of optional variable files.

;; imports in this order:
;; - /roles/deploy/vars/main.yml
;; - /roles/deploy/vars/{{ project_name }}.yml
;; - /roles/deploy/vars/{{ project_name }}/main.yml
;; - /roles/deploy/vars/{{ project_name }}/{{ project_env }}.yml
- include_vars:
    dir: 'vars'
    files_matching: "{{ item }}"
    depth: 1
  with_items:
    - "main.yml"
    - "{{ project_name }}.yml"

- include_vars:
    dir: 'vars/{{ env_name }}'
    files_matching: "{{ item }}"
    depth: 1
  with_items:
    - "main.yml"
    - "{{ project_name }}.yml"

group_vars/all.yml

Configure default variables for the project and various users and environments

project_users:
    bootstrap:
        env:   bootstrap
        user:  ansible
        group: ansible
        mode:  755
        root:  /cs/ansible/
        home:  /cs/ansible/home/ansible/
        directories:
            - /cs/ansible/
            - /cs/ansible/home/

    live:
        env:   live
        user:  ansible-live
        group: ansible
        mode:  755
        root:  /cs/ansible/live/
        home:  /cs/ansible/home/ansible-live/

    demo:
        env:   demo
        user:  ansible-demo
        group: ansible
        mode:  755
        root:  /cs/ansible/demo/
        home:  /cs/ansible/home/ansible-demo/

    sandbox:
        env:   sandbox
        user:  ansible-sandbox
        group: ansible
        mode:  755
        root:  /cs/ansible/sandbox/
        home:  /cs/ansible/home/ansible-sandbox/    

project_env:  bootstrap
project_user: "{{ ansible_users[project_env] }}" ;; this will be retroactively updated if project_env is redefined later

roles/deploy/vars/main.yml

project defaults

ansible_project:
  node_env:   development
  node_port:  4200
  nginx_port: 4400

roles/deploy/vars/project_1.yml

defaults for project_1

ansible_project:
  node_port:  4201
  nginx_port: 4401

roles/deploy/vars/live/main.yml

defaults for live environment, overrides project defaults

ansible_project:
  node_env: production

roles/deploy/vars/live/project_1.yml

final overrides for project_1 in the live environment

ansible_project:
  nginx_port: 80

playbooks/demo.yml

Configure separate playbooks for each environment

- hosts: shared_server
  remote_user: ansible-demo
  vars:
    project_env: demo
  pre_tasks:
    - debug: "msg='{{ facter_gid }}@{{ facter_fqdn }} ({{ server_pseudonym }})'"
    - debug: var=project_ssh_user
  roles:
    - { role: deploy, project_name: project_1 }

WARNING: Because all environments live on a single host, all playbooks must be run individually, otherwise Ansible will brokenly attempt to run all scripts as the first ssh login user and only use the variables for the first user. If you need to run all scripts sequentially, then use xargs to run them each as separate commands.

find ./playbooks/*.yml | xargs -L1 time ansible-playbook
1

New answer based on the latest Ansible versions—basically, you should use with_first_found, along with skip: true to skip the task if no file is found.

- name: Include vars file if one exists meeting our condition.
  include_vars: "{{ item }}"
  with_first_found:
    - files:
        - vars/{{ variable_here }}.yml
      skip: true

This makes it so you don't have to have a fallback vars file in that list.

See related: https://stackoverflow.com/a/39544405/100134

0

Or in a more yaml way:

- hosts: webservers
  vars:
    paths_to_vars_files:
      - vars/{{ ansible_hostname }}.yml
      - vars/default.yml
  tasks:
    - include_vars: "{{ item }}"
      with_first_found: "{{ paths_to_vars_files }}"

That is, instead of writing an array on one line with square brackets, like:

['path/to/file1', 'path/to/file2', ...]

Use the yaml way of writing array values on multiple lines, like:

- path/to/file1
- path/to/file2

As mentioned this looks for a vars file named {{ ansible_hostname }}.yml, and if it doesn't exist uses default.yml

3
  • This answer uses the same code as this one except it uses a different data. Namely {{ ansible_hostname }}.yml file name instead of ../path/to/file1. What's the point? One can add an unlimited number of input file names.
    – techraf
    Dec 23, 2016 at 14:06
  • @techraf: Ok, I added some clarification/amplification on why a new answer was submitted. It's because serverfault comments don't support multi-line code snippets, and I was just making a point that yaml arrays are frequently (preferably?) written on multiple lines. I'd also be fine if the earlier answer was edited and the multi-line array format shown, as I see that more often. Then my answer can be deleted.
    – Donn Lee
    Dec 26, 2016 at 6:26
  • Both notations are specified in Ansible docs in a chapter YAML Basics. The fact that you see one more often than the other does not yet make this a new answer.
    – techraf
    Dec 26, 2016 at 6:34
0

Piecing various pieces together... include_vars with a when clause that is true when the file exists. i.e.

vars:
  file_to_include: /path/to/file
tasks:
  - include_vars: "{{ file_to_include }}"
    when: file_to_include is exists

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .