Variables from not-activated roles get set

use include_role for those cases, its not a bug, you are mixing the keywords in a way that does not work as you want it to.

Doesn’t include_role run at the time the task is run, i.e. after the common role has already run?

That also only works if the dhcpd4 role is one of the last ones and nothing else depends on it.

I would use for any role you want to run conditionally, aka using when: . The import order and execution order are different things. Both import_role and include_role exist to give you fine grained control and different behavior options. The roles keyword is akin to a list of import_role entries, with less options all between pre-tasks and tasks, in return you have simpler and less verbose syntax. If you need the advanced functionality, you cannot stick to using roles.

1 Like

Yes. This makes it not work:

tg@tgb1:/tmp/mwe $ ansible-playbook -e run_extra_role=true testplay.yml  
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
 
PLAY [localhost] ************************************************************************************************
 
TASK [common : variable in common role] *************************************************************************
ok: [localhost] => {
    "msg": "variable foo=False"
}
 
TASK [include_role : extra] *************************************************************************************
included: extra for localhost
 
TASK [extra : hi from role extra] *******************************************************************************
ok: [localhost] => {
    "msg": "hi"
}
 
PLAY RECAP ******************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Changed MWE for this:

tg@tgb1:/tmp/mwe $ find . -type f | sort | while IFS= read -r name; do echo === $name; cat $name; done
=== ./roles/common/tasks/main.yml
---
- name: variable in common role
  debug:
    msg: variable foo={{ foo | default(false) }}
=== ./roles/extra/defaults/main.yml
foo: true
=== ./roles/extra/tasks/main.yml
---
- name: hi from role extra
  debug: msg=hi
=== ./testplay.yml
- hosts: localhost
  become: false
  gather_facts: false
  roles:
  - role: common
  #- role: extra
  #  when: run_extra_role | default(false) | bool
  tasks:
  - include_role: name=extra
    when: run_extra_role | default(false) | bool
  #- name: variable test
  #  debug:
  #    msg: variable foo={{ foo | default(false) }}

Not quite. The roles keyword changes ordering: the tasks of the role are ran before the tasks of the playbook including it, not at the time it is included, and, more importantly, the variables are available to roles that run before it.

This is why include_role cannot be used, see changed MWE.

No, they are run before tasks but AFTER pre-tasks and AFTER implicit gathering, anything you import (either via import_role or roles) will export their variables to the play BEFORE any task runs. That is why if you don’t want variables exported, you can a) use import_role with public: no to avoid making the play level variables OR b) use include_role + when to avoid any role resource being loaded. When you include a role the variables become available AFTER the include happens, using public to control if they are exported or not to the play.

You have all the parts you need, you just need to use them correctly.

Again: I do want variables exported.

I want that the common role, which runs BEFORE the extra role, gets a variable set if and exactly if the extra role will run afterwards. See the changed MWE from the post above.

play

- hosts: localhost
  gather_facts: false
  tasks:
    - import_role:
        name: first

    - include_role:
        name: second
      when: doit

first defines doit = false in defaults, and has debug task, second has debug task

#>ansible-playbook play.yml               
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ***************************************************************************************************************************************

TASK [first : debug] ***********************************************************************************************************************************
ok: [localhost] => {
    "doit": false
}

TASK [include_role : second] ***************************************************************************************************************************
skipping: [localhost]

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

and now to run second, i set doit as extra var:

#>ansible-playbook play.yml -e '{doit: True}'
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ***************************************************************************************************************************************

TASK [first : debug] ***********************************************************************************************************************************
ok: [localhost] => {
    "doit": true
}

TASK [include_role : second] ***************************************************************************************************************************
included: second for localhost

TASK [second : debug] **********************************************************************************************************************************
ok: [localhost] => {
    "msg": "in second"
}

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

That’s not what I need.

second is run, for this host group, depending on a host variable from the inventory. (For other hosts, it’s directly included, or even as a dependency of a directly-included role.)

If second is run, first needs to know about it.

And this completely works, except if second is conditionally included.

In my universe this breaks casuality :]].
No idea how to solve this.

It already works, except for conditionally-included roles.

it doesn’t, you are conflating a role being imported and its variables being available to prior roles (import time) vs the role being selected for import/run (runtime). You can have both roles keyed on the same variable or expression that makes that decision, but not by it being activated in the 2nd role when you want that role to only be included conditionally.

Oh well, this is quite a design deficiency. But the mapping of which hosts have which roles is… weird, anyway.

I’ll just bite the sour apple and have the backport repo enabled on more hosts than need it and not generalise this to more repos/distros. (Debian backports, when enabled, are not automatically used, but do automatically upgrade a package if newer than stable, so this is safe for this specific case.)

I’ve also walked through the defaults/main.yml files of all roles we do include conditionally and put warning comments into them. Other than the KEA DHCPD installation, they were safe.

In your initial post, you stated your problem pretty clearly:

Everything after that was going off the rails demonstrating unexpected (by you) behavior of variables. But it is possible to do what you initially wanted, which is this magic Jinja expression.

A problem with your initial attempt that hasn’t been brought up before is the use one boolean to indicate multiple things. Each host (host_vars), group (group_vars) or role default can only assert whether it requires repo_backports, and each should do so with its own boolean. As you’ve demonstrated above, if they all use the same boolean variable, then the last one wins.

You indicated “something like repo_backports_$distroname: true”. Let’s assume your $distroname can be either one of debian or ubuntu just to keep this concrete, but really it’ll be whatever {{ ansible_distribution|lower }} resolves to.

Here’s a best practices thing that actually improves the variable naming problem. All role variables should start with the role name and an underscore. More generally, since these are a whole suite of values coming from various sources, you could adopt a naming scheme like
<source>_repo_backports_<distribution>, so you’d have

# from roles/dhcpd/defaults.yml
dhcpd_repo_backports_debian: false   # I'm just guessing
dhcpd_repo_backports_ubuntu: false

# from roles/dhcpd4/defaults.yml
dhcpd_repo_backports_debian: false
dhcpd_repo_backports_ubuntu: true   # I'm still just guessing

# from group_vars/babble.yaml
babble_repo_backports_ubuntu: true

# from playbook vars:
playvars_repo_backports_debian: '{{ [true, false] | random }}' # not even guessing!

Then in your common role’s vars: section you could have expressions like this:

vars:
  v_names:  "{{ q('ansible.builtin.varnames', '.+_repo_backports_.+_' ~ (ansible_distribution|lower)) | list }}"
  v_values: "{{ q('ansible.builtin.vars', *v_names) | list }}"
  common_repo_backports: "{{ (v_values | sum) > 0 }}" # true if any are true.

Note that last variable doesn’t match the regex
'.+_repo_backports_.+_' ~ (ansible_distribution|lower)
so it isn’t swept up with the others in that v_names query (q(…) above).

Note also that this doesn’t solve the problems with your initial attempt to use the roles: keyword to conditionally import roles based on facts from the future. It merely addresses your initial ask for a Jinja expression to evaluate whether something has set one of these booleans, while not clobbering other similar settings from other sources.

Here’s one more factoid that I hadn’t appreciated until I started playing with this today: You can load role defaults before any roles are invoked by use of the vars_files: playbook keyword.

  vars_files:
    - roles/dhcpd4/defaults/main.yml
    - roles/dhcpd/defaults/main.yml

Your common role may then be able to use some logic on those variable to determine what config to generate. You’re still left with figuring out which roles to include (rather than import, probably) eventually.

Cheers!

1 Like

As @bcoca pointed out above, role defaults are always loaded before any roles are invoked — if you list your roles under the roles: playbook keyword:

  roles:
    - common
    - dhcpd
    - dhcpd4

But after all those role defaults are loaded, all the roles are invoked.

However, if you load the role defaults for, say roles dhcpd and dhcpd4 with the vars_files: playbook keyword, and omit them from your roles: list, they won’t be invoked. You could conditionally include them in your tasks: section with include_role:.

I just wanted to clarify that distinction.

1 Like