163

I'm customizing linux users creation inside my role. I need to let users of my role customize home_directory, group_name, name, password.

I was wondering if there's a more flexible way to cope with default values.

I know that the code below is possible:

- name: Create default
  user:
    name: "default_name"
  when: my_variable is not defined

 - name: Create custom
  user:
    name: "{{my_variable}}"
  when: my_variable is defined

But as I mentioned, there's a lot of optional variables and this creates a lot of possibilities.

Is there something like the code above?

user:
  name: "default_name", "{{my_variable}}"

The code should set name="default_name" when my_variable isn't defined.

I could set all variables on defaults/main.yml and create the user like that:

- name: Create user
  user:
    name: "{{my_variable}}"

But those variables are inside a really big hash and there are some hashes inside that hash that can't be a default.

Chen A.
  • 10,140
  • 3
  • 42
  • 61
Bernardo Vale
  • 3,224
  • 4
  • 21
  • 34

9 Answers9

293

You can use Jinja's default:

- name: Create user
  user:
    name: "{{ my_variable | default('default_value') }}"
ydaetskcoR
  • 53,225
  • 8
  • 158
  • 177
tedder42
  • 23,519
  • 13
  • 86
  • 102
  • 13
    And if the default variable depends upon another variable. For example `default('/home/' {{ anothervar }})`, is there a way to concatenate those values? – Bernardo Vale Jan 31 '16 at 22:02
  • 21
    Just remove the ' around the `default_value` and it would use a variable called default_value – dagonza Dec 02 '16 at 14:37
  • 10
    Doesn't work if variable is nested like `{{ dict.key | default(5) }}`. It will fail if `dict` is undefined. – Jordan Stewart Feb 07 '19 at 06:12
  • 1
    FYI, this will now throw an error: `[WARNING]: when statements should not include jinja2 templating delimiters such as {{ }} or {% %}.` – bryan kennedy Feb 24 '19 at 21:21
  • 2
    @JordanStewart I had the same case - with nested variables. I posted an answer with possible solutions below. – Roman Kruglov Jul 10 '19 at 15:44
  • will it pick another variable's value for default? "{{ my_variable | default(another_variable) }}" ? – letthefireflieslive Sep 10 '20 at 08:46
  • 3
    @BernardoVale if the default variable depends from another variable. As on your example `default('/home/' {{ anothervar }})`. I had to do the following to get it to work with for example a path construction: `{{ my_variable | default('/opt/'+ another_var_dir) }}` – svcabre Nov 19 '20 at 14:02
  • This solution won't work if you have something like this: `"{{ custom_delay*60 | default(900) }}"`. You will get still "undefined variable error". – Rohlik Aug 24 '22 at 09:14
  • 1
    @Rohlik `"{{ custom_delay | default(15) * 60 }}"` will do it. – stackprotector Mar 08 '23 at 11:08
16

Not totally related, but you can also check for both undefined AND empty (for e.g my_variable:) variable. (NOTE: only works with ansible version > 1.9, see: link)

- name: Create user
  user:
    name: "{{ ((my_variable == None) | ternary('default_value', my_variable)) \
    if my_variable is defined else 'default_value' }}"
Tommy Nguyen
  • 3,403
  • 1
  • 15
  • 14
11

If anybody is looking for an option which handles nested variables, there are several such options in this github issue.

In short, you need to use "default" filter for every level of nested vars. For a variable "a.nested.var" it would look like:

- hosts: 'localhost'
  tasks:
    - debug:
        msg: "{{ ((a | default({})).nested | default({}) ).var | default('bar') }}"

or you could set default values of empty dicts for each level of vars, maybe using "combine" filter. Or use "json_query" filter. But the option I chose seems simpler to me if you have only one level of nesting.

Roman Kruglov
  • 3,375
  • 2
  • 40
  • 46
7

In case you using lookup to set default read from environment you have also set the second parameter of default to true:

- set_facts:
    ansible_ssh_user: "{{ lookup('env', 'SSH_USER') | default('foo', true) }}"

You can also concatenate multiple default definitions:

- set_facts:
    ansible_ssh_user: "{{ some_var.split('-')[1] | default(lookup('env','USER'), true) | default('foo') }}"
panticz
  • 2,135
  • 25
  • 16
6

If you are assigning default value for boolean fact then ensure that no quotes is used inside default().

- name: create bool default
  set_fact:
    name: "{{ my_bool | default(true) }}"

For other variables used the same method given in verified answer.

- name: Create user
  user:
    name: "{{ my_variable | default('default_value') }}"
Samselvaprabu
  • 16,830
  • 32
  • 144
  • 230
5

If you have a single play that you want to loop over the items, define that list in group_vars/all or somewhere else that makes sense:

all_items:
  - first
  - second
  - third
  - fourth

Then your task can look like this:

  - name: List items or default list
    debug:
      var: item
    with_items: "{{ varlist | default(all_items) }}"

Pass in varlist as a JSON array:

ansible-playbook <playbook_name> --extra-vars='{"varlist": [first,third]}'

Prior to that, you might also want a task that checks that each item in varlist is also in all_items:

  - name: Ensure passed variables are in all_items
    fail:
      msg: "{{ item }} not  in all_items list"
    when: item not in all_items
    with_items: "{{ varlist | default(all_items) }}"
Jack
  • 5,801
  • 1
  • 15
  • 20
4

The question is quite old, but what about:

- hosts: 'localhost'
  tasks:
    - debug:
        msg: "{{ ( a | default({})).get('nested', {}).get('var','bar') }}"

It looks less cumbersome to me...

pshunter
  • 71
  • 6
  • 1
    The problem is you need to repeat this wherever the variable is used. The question is how to set a default once and for all. – reinierpost Dec 22 '21 at 14:11
0

@Roman Kruglov mentioned json_query. It's perfect for nested queries.

An example of json_query sample playbook for existing and non-existing value:

- hosts: localhost
  gather_facts: False
  vars:
    level1:
      level2:
        level3:
          level4: "LEVEL4"
  tasks:
    - name: Print on existing level4
      debug:
         var: level1 | json_query('level2.level3.level4')  # prints 'LEVEL4'
      when: level1 | json_query('level2.level3.level4')

    - name: Skip on inexistent level5
      debug:
         var: level1 | json_query('level2.level3.level4.level5')  # skipped
      when: level1 | json_query('level2.level3.level4.level5')
Juuso Ohtonen
  • 8,826
  • 9
  • 65
  • 98
0

You can also use an if statement:

# Firewall manager: firewalld or ufw
firewall: "{{ 'firewalld' if ansible_os_family == 'RedHat' else 'ufw' }}"