Strategy for repeated complex logic between hosts

I’ve found my “when” clauses are getting too complicated. They involve multiple filters and data from multiple hosts. In any other language I would declare intermediate variables but I can’t get that to work in Ansible.

I basically want to do:

- name: Gather information on host1
  host: host1
  tasks:
   - dowidget:
     register: widget_info_host1

- name: Gather information on host1
  host: host2
  tasks:
   - dowocket:
     register: wocket_info_host2

- name: Do something based on host1 and host2
  host: host3
  tasks:
   - dowhippet:
     when: (hostvars.host1.widget_info_host1 | COMPLICATED_FILTER)[0] >= (hostvars.host2.wocket_info_host2 | OTHER_COMPLICATED_FILTER)[1]
   - dowombat:
     when: (hostvars.host1.widget_info_host1 | COMPLICATED_FILTER)[0] >= (hostvars.host2.wocket_info_host2 | THIRD_COMPLICATED_FILTER)[1]

- name: Do more based on host1 and host2
  host: host4
  tasks:
   - dowhippet:
     when: (hostvars.host1.widget_info_host1 | COMPLICATED_FILTER)[0] >= host4_variable

So I want to store clauses like (widget_info_host1 | COMPLICATED_FILTER)[0] in a temporary variable likehost1_widget_mtime because the template is 327 characters long and it is error-prone to edit it in multiple places. I can’t put it in host1’s variables as

host1_widget_mtime: (widget_info_host1 | COMPLICATED_FILTER)[0]

because hostvars aren’t treated as templates. I can’t put it in host3 or host4 because it can’t be shared. set_fact isn’t designed for this. I don’t know of any other way to set a variable.

How do I avoid having to repeat complex logic in multiple locations?

Thank you!

something like this?:

- name: Gather information on host1
  host: host1
  tasks:
   - dowidget:
     register: widget_info_host1
   - set_fact:
        condition: '{{(widget_info_host1 | COMPLICATED_FILTER)[0]}}'

- name: Gather information on host1
  host: host2
  tasks:
   - dowocket: 
     register: wocket_info_host2
   - set_fact:
        condition_1: '{{(wocket_info_host2 | OTHER_COMPLICATED_FILTER)[1]}}'
        condition_2: '{{(wocket_info_host2 | THIRD_COMPLICATED_FILTER)[1]}}'


- name: Do something based on host1 and host2
  host: host3
  tasks:
   - dowhippet:
     when: hostvars['host1']['condition'] >= hostvars['host2']['condition_1']
   - dowombat:
     when: hostvars['host1']['condition'] >= hostvars['host2']['condition_2']

- name: Do more based on host1 and host2
  host: host4
  tasks:
   - dowhippet:
     when: hostvars['host1']['condition'] >= host4_variable

That’s plausible but I don’t think I understand why set_facts is appropriate. Shouldn’t it be set_variables? Why isn’t that a thing?

Naming issue aside (I brokered peace between 2 parties after weeks of discussion and we ended up with the inadequate set_facts), the main other way to create variables in Ansible is normally vars: (or derived), this creates ‘lazyily evaluated’ variables, while set_facts creates static variables.

Since hostvars cannot template (because it is in the wrong host context except for hostvars[inventory_hostname] but that is not special cased either) my example uses set_fact to ensure a value you do not need to template, then you can just use hostvars to access those values, you could even use vars to shorten it at that point, but that seemed excessive for my example.

I appreciate that explanation, particularly the color commentary about the naming of set_facts. FWIW (as a new Ansible user) I find the distinction between facts and variables to be somewhat confusing, particularly if you are trying to use them in a principled way.

In this case I’ve convinced myself that these are “facts” about the hosts. Later I will probably convince myself that I should delegate the facts and go through hostvars because set_facts solves the templating problem.

But the truth is I’m choosing facts because they bypass lazy evaluation because hostvars blocks template evaluation. Some part of that is a code smell but I haven’t been here long enough to say what part.

Thank you for your help. I’ll update this thread when I settle on an answer (for posterity).

set_facts is really ‘set host scoped variable’, unless you use cacheable=true … then it creates 2 variables, one of them a fact. So yes, badly named and confusing . But that is not only problem with variables in ansible, why I proposed redesign variable interface · Issue #127 · ansible/proposals · GitHub

I know we’re off track for this thread, but aren’t all variables host-scoped? (inasmuch as you have to go through hostvars to get it)

No, hostvars does not contain all variables available in a scope, many are task/play variables. You can use the vars and varnames lookups to verify this.

Oh, interesting, so inadvertently back to the thread then we’ve got:

  • facts: not in hostvars, eager evaluation, does not delegate unless requested, changeable
  • variables: in hostvars, lazy evaluation, delegates, changeable
  • cached facts: actually a fact + a variable and cached between execution, changeable

So while I’ve been trying to choose between facts and variables based on whether it “changes” or not, in reality they can all change and in practice the choice might be driven by functional requirements instead.

Do I have that right?

no,

Variables are lazy in general, they get ‘resolved’ or made static on consumption in a task. set_fact happens to be a task and resolves the variables/templates and creates a static value, but the nature of the variable is still ‘lazy’ … it just does not have a template to resolve.

  • facts: not in hostvars, eager evaluation, changeable

Facts from set_facts are in hostvars, not to be confused with facts that go in ansible_facts

  • cached facts: actually a fact + a variable and cached between execution, changeable

Facts are only cached between executions if you configured a persistent cache plugin

Use set_fact when a)you need a host scoped variable or b) you need to set a static value to a variable (i.e set_fact: now={{ q('pipe', 'date')}}) , even you you dont want it host scoped (normally delegatae_to/delegate_facts to localhost as it is always ‘present’ host)