Explicitely use role defaults

Hello,

I’m working on a playbook where I’d like to fetch some files from my hosts to the control node.

I want the destination (on the control node) to be controlled by a variable local_release_dir in the localhost context (aka, set in the inventory for localhost, or using role defaults).
However, since the tasks run in the context of the host, if I just use {{ local_release_dir }}, I’ll have (roughly) {{ hostvars[current_host]['local_release_dir'] | default(local_release_dir) # aka the role defaults }}, which does not work, because hostvars[current_host]['local_release_dir'] != hostvars['localhost']['local_release_dir'] is a situation I want to handle correctly.

The workaround I have at the moment is to add the default explicity, but it’s not great, because there is now 2 sources of truth who can diverge.

  - name: Download_file | Fetch file to localhost
    fetch:
      flat: true
      src: "{{ local_release_dir }}/{{ item.value.dest }}"
      # TODO: delete d() filter
      dest: "{{ (hostvars['localhost']['local_release_dir'] | d('/tmp/releases/')) + '/' }}" # the default ilter here should be replaced with a way to explicitly reference the role defaults. 
    loop: "{{ some jinja expression listing the needed files, per host }} 

Possibles workarounds:

  • use include_vars with name to make the defaults variables available under a named variable ; I’m not a fan since I need to add a task.

Full context for anyone interested : Refactor download (file) by VannTen · Pull Request #12299 · kubernetes-sigs/kubespray · GitHub

Would appreciate any ideas, including doing the same thing (getting files on localhost) in a different way / with a different modules.

Given the role

shell> tree roles/
roles/
└── role_A
    ├── defaults
    │   └── main.yml
    └── tasks
        └── main.yml
shell> cat roles/role_A/defaults/main.yml 
local_release_dir: role_default_dir
shell> cat roles/role_A/tasks/main.yml 
- debug:
    var: local_release_dir

and the inventory

shell> cat hosts
localhost local_release_dir=localhost_dir

[test]
foo
bar
baz

The play

shell> cat pb.yml 
- hosts: test,localhost

  pre_tasks:

    - set_fact:
        local_release_dir: "{{ local_release_dir }}"
      delegate_to: localhost
      run_once: true

    - debug:
        var: local_release_dir

  roles:
    - role_A

limited to the localhost gives, as expected

shell> ansible-playbook pb.yml -l localhost

PLAY [test,localhost] **********************************************************************************************

TASK [set_fact] ****************************************************************************************************
ok: [localhost]

TASK [debug] *******************************************************************************************************
ok: [localhost] => 
    local_release_dir: localhost_dir

TASK [role_A : debug] **********************************************************************************************
ok: [localhost] => 
    local_release_dir: localhost_dir

PLAY RECAP *********************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The pre-task set_fact should override local_release_dir from the role defaults for all hosts because of the delegation to the localhost and the run_once "apply any results and facts to all active hosts in the same batch".

But, running the play for all hosts

shell> ansible-playbook pb.yml

PLAY [test,localhost] **********************************************************************************************

TASK [set_fact] ****************************************************************************************************
ok: [foo -> localhost]

TASK [debug] *******************************************************************************************************
ok: [foo] => 
    local_release_dir: role_default_dir
ok: [bar] => 
    local_release_dir: role_default_dir
ok: [baz] => 
    local_release_dir: role_default_dir
ok: [localhost] => 
    local_release_dir: role_default_dir

TASK [role_A : debug] **********************************************************************************************
ok: [foo] => 
    local_release_dir: role_default_dir
ok: [bar] => 
    local_release_dir: role_default_dir
ok: [baz] => 
    local_release_dir: role_default_dir
ok: [localhost] => 
    local_release_dir: role_default_dir

PLAY RECAP *********************************************************************************************************
bar                        : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
baz                        : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
foo                        : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

does not override local_release_dir even for the localhost! This is a bug because the variables from the inventory files should override the role deafults.

It’s not a bug because when you delegate you get the variable from the original host, not the delegated_to.

But that does not answer the question, which is how (if that’s possible) access the role default of a variable (so, not the value from inventory). I’ve seen some reference to a defaults dict on the bug tracker, but nothing conclusive.

You’re right. Wouldn’t it help to reference the localhost then?

- hosts: test

  pre_tasks:

    - set_fact:
        local_release_dir: "{{ hostvars['localhost']['local_release_dir'] |
                               d(local_release_dir) }}"
      delegate_to: localhost
      run_once: true

    - debug:
        var: local_release_dir

  roles:
    - role_A

If the variable local_release_dir is not defined in the localhost context, you will get the role’s default.

1 Like

No, because with delegate_to, you’re not in the delegated host context for variables, you’re in the original host context, as explained in the OP.

Regardless of which target host’s context you’re in, hostvars['localhost']['local_release_dir'] is unambiguously from localhost’s context. The d(local_release_dir) will depend on the specific target host, but since local_release_dir is always defined for localhost (by your inventory), the |default part of the expression should never come into play.

Also, set_fact with run_once: true sets the local_release_dir for all hosts. It is in fact ambiguous which target host’s context such a step will run in, but it doesn’t matter. While that target host will generally be the first target host which is still active in the play, but I wouldn’t rely on that. And the expression from @vbotka’s post doesn’t — ignoring the |default() part what, again, doesn’t come into play.

1 Like

No, I precisely can’t assume that the variable is defined in the inventory for localhost, that’s what I’m trying to say. If I could, of course this wouldn’t be needed.

And in that case the hostvars[‘localhost’][‘local_release_dir’] is not defined, hence the default filter.

If you want to maintain the original role default regardless of overrides, be explicit about it so it makes sense to future you.

In your role/download/defaults/main.yml, do this:

---
# roles/download/defaults/main.yml
# Note: variables renamed so as to avoid the
#    "Variables names from within roles should use download_ as a prefix."
# message from `ansible_lint`.

download_local_release_dir_role_default: &dlrdrd /value/from/roles/download/defaults/main.yml
download_local_release_dir:              *dlrdrd

Then when hostvars[‘localhost’][‘download_local_release_dir’] is not defined, use download_local_release_dir_role_default, like so:

{{ hostvars['localhost']['download_local_release_dir']
   | default('download_local_release_dir_role_default') }}

The “&dlrdrd” defines a YAML node anchor, while “*dlrdrd” is a reference to that anchor. They only exist in the YAML parsing phase. I’m pretty sure Ansible itself will be oblivious to it, and that Ansible will see them as two variables which happen to have identical values. I only used the anchor and reference to be explicit about them being initialized with the same value. (And if you change the value, you don’t have to change it in two places.)

You could just as easily say

download_local_release_dir_role_default: /value/from/roles/download/defaults/main.yml
download_local_release_dir:              /value/from/roles/download/defaults/main.yml  # noqa yaml[colons]

or this:

download_local_release_dir_role_default: /value/from/roles/download/defaults/main.yml
download_local_release_dir: "{{ download_local_release_dir_role_default }}"

I find both of those alternatives aesthetically offensive, but they’ll both work. (Note that in the 2nd alternative, if someone changes the first variable elsewhere, they’ll also change the second – intentionally or not – because of lazy evaluation. That won’t be the case if you use the anchor/reference technique.)

3 Likes

This is a pretty interesting idea for working around the lack of native access to defaults, hadn’t though about that.

I think I’ll put the “true defaults” variable in /vars instead of /defaults, in that case, since it should not be modified.

Thanks for the inspiration !

1 Like