17

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)

27

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" ]
  • 4
    According to Ansible developers, this solution will load all of the files, not just the first one found. – tjanez Nov 24 '14 at 12:13
10

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.

  • "only the first file found" - the idea was to redefine some variables, not all of them – Sergey Nov 24 '14 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 '14 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 '16 at 8:51
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
- 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.

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

  • 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 '16 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 '16 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 '16 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
0

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.