Fact delegation when calling a role. I'm going crazy.

Hi all,

I signed up specifically for this topic because I’m trying to wrap my head around this and there seems to be absolutely no solution for this other than duplicated code.

In this example i’m trying to find the correct IP to use in my main playbook, so that i may reference it using hostvars[‘logging01’].backnet_ip in a jinja templatefile to create a config file on zzz-test01 to push my logs to my logging server.

I wrote a role that sets a lot of facts special to our company and it has worked fine so far, as long as the host is part of the play. Now that I do not want to target logging01 with my play (Might also be that a playbook has “hosts: all” and I simply run it using --limit) I am running into problems with fact delegation.

What I’m trying to do is this:

- name: Test playbook for fact delegation
  hosts: zzz-test01
  gather_facts: false
  vars:
    ansible_user: root
  tasks:
      - name: Setup facts on host not part of this play
        vars:
          delegation_host: "logging01"
        delegate_to: "{{ delegation_host }}"
        delegate_facts: true
        run_once: true
        block:
            - name: Run fact gathering on "{{ delegation_host }}"
              setup:

            - name: Run mde_common on "{{ delegation_host }}"
              tags: always
              import_role:
                name: mde_common   

Inside that role I have two problems, which are essentially the same:

- name: "Set variables according to environment: Pre-Live"
  set_fact:
    env: "prelive"
    mde_host_prefix: "zzz-"
    pretty_env: "Pre-Live"
    regular_user: "test"
  when: inventory_hostname is match("^zzz-.*")

- name: "Check for 10.0.0.0/24 IP and overwrite variables for office"
  set_fact:
    backnet_ip: "{{ ansible_facts['all_ipv4_addresses']
                    | select('search', '^10\\.0\\.0\\.')
                    | list
                    | sort
                    | first }}"
  when: >
    ansible_facts['all_ipv4_addresses']
    is defined and
    (
      ansible_facts['all_ipv4_addresses']
      | select('search', '^10\\.0\\.0\\.')
      | list | length > 0
    )

  1. inventory_hostname always targets the delegator, not the delegatee.
  2. As for using ansible_facts to set a fact, this does not work.
    If this role is called using delegate_to: i’d need to do this:
- name: "Check for 10.0.0.0/24 IP and overwrite variables for office"
  set_fact:
    backnet_ip: "{{ hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
                    | select('search', '^10\\.0\\.0\\.')
                    | list
                    | sort
                    | first }}"
  when: >
    hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
    is defined and
    (
      hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
      | select('search', '^10\\.0\\.0\\.')
      | list | length > 0
    )

Note, I’m doing:

hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']

because otherwise the name of the fact changes

hostvars[delegation_host]['ansible_all_ipv4_addresses']

Now I cannot do something like this:

- name: Determine effective ansible facts source
  set_fact:
    effective_ansible_facts: >-
      {{
        (delegation_host is defined)
        | ternary(
            hostvars[delegation_host]['ansible_facts'],
            ansible_facts
        )
      }}

because that, again, puts that fact either in the hostvars of delegation_host, which I would need to reference by hostvars, or easily referencable if not called with delegation.

The only solution I can think of for a scenario like this is duplicating every single task and adding “when: delegation_host is not defined” […]

- name: "Check for 10.0.0.0/24 IP and overwrite variables for office"
  set_fact:
    backnet_ip: "{{ hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
                    | select('search', '^10\\.0\\.0\\.')
                    | list
                    | sort
                    | first }}"
  when: >
    hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
    is defined and
    (
      hostvars[delegation_host]['ansible_facts']['all_ipv4_addresses']
      | select('search', '^10\\.0\\.0\\.')
      | list | length > 0
    )

- name: "Check for 10.0.0.0/24 IP and overwrite variables for office"
  set_fact:
    backnet_ip: "{{ ansible_facts['all_ipv4_addresses']
                    | select('search', '^10\\.0\\.0\\.')
                    | list
                    | sort
                    | first }}"
  when: >
    ansible_facts['all_ipv4_addresses']
    is defined and delegation_host is not defined and
    (
      ansible_facts['all_ipv4_addresses']
      | select('search', '^10\\.0\\.0\\.')
      | list | length > 0
    )

[…] which I’d like to avoid for obvious reasons. The scenario I am thinking of cannot be that special to our use-case. Is there really no solution for this?

Sorry for rambling, but I want you to see that I have thought about this for hours now.
Thanks in advance for anyone taking the time to read this madness.
~nosebeggar

When all you have is set_fact, every variable looks like one that should be created with set_fact. :slight_smile:

One natural way to solve this would be to set a normal variable for the host (e.g. in host_vars/logging01.yml):

backnet_ip: "{{ ansible_facts['all_ipv4_addresses'] | select('search', '^10\\.0\\.80\\.') | list | sort | first | default(none) }}"

Then all you have to do is make sure that facts have been gathered for the host and Ansible will take care of juggling contexts when resolving the value:

- hosts: zzz-test01
  tasks:
    - debug:
        msg: "{{ hostvars.logging01.backnet_ip }}"

    - gather_facts:
        gather_subset: network
      delegate_to: logging01
      delegate_facts: true
      run_once: true
      when: hostvars.logging01.backnet_ip is none

    - debug:
        msg: "{{ hostvars.logging01.backnet_ip }}"
TASK [Gathering Facts] *********************************************************
ok: [zzz-test01]

TASK [debug] *******************************************************************
ok: [zzz-test01] => 
    msg: null

TASK [gather_facts] ************************************************************
ok: [zzz-test01 -> logging01]

TASK [debug] *******************************************************************
ok: [zzz-test01] => 
    msg: 10.0.80.234

But if you’re really committed to your current approach, remember that you can define variables at any task or block scope you need.

- vars:
    fact_source: "{{ hostvars[delegation_host].ansible_facts if delegation_host is defined else ansible_facts }}"
  block:
    - name: "Check for 10.0.0.0/24 IP and overwrite variables for office"
      set_fact:
        backnet_ip: "{{ fact_source['all_ipv4_addresses']
                        | select('search', '^10\\.0\\.80\\.')
                        | list
                        | sort
                        | first }}"
      when: >
        fact_source['all_ipv4_addresses']
        is defined and
        (
          fact_source['all_ipv4_addresses']
          | select('search', '^10\\.0\\.80\\.')
          | list | length > 0
        )

With delegation:

TASK [Run fact gathering on "logging01"] ***************************************
ok: [zzz-test01 -> logging01]

TASK [mde_common : Check for 10.0.0.0/24 IP and overwrite variables for office] ***
ok: [zzz-test01 -> logging01]

TASK [debug] *******************************************************************
ok: [zzz-test01] => 
    hostvars['logging01']['backnet_ip']: 10.0.80.234

Without delegation:

TASK [Gathering Facts] *********************************************************ok: [logging01]

TASK [mde_common : Check for 10.0.0.0/24 IP and overwrite variables for office] ***
ok: [logging01]

TASK [debug] *******************************************************************
ok: [logging01] => 
    hostvars['logging01']['backnet_ip']: 10.0.80.234

Later on when my head was clearer I came to the same conclusion. I ended up doing the second one. I think I need to re-read the docs about variables, because I think I have a fundamental misunderstanding of ansible facts/variables.

Thanks!