Ansible - How to loop over multiple hosts and stop when find correct answer?

Viewed 1394

I would like to loop over a group of hosts (they are in inventory) with uri module, only until the status code is 200, and save this hostname to a variable.

What's written below is completely wrong, It's just to express my need.
(Note : I have 4 hosts in all_uri_group, I know the 2nd and 3rd ones have the status code 200, I want to stop the loop in 2nd host, and register the valid_host with the value of second hostname.

- name: Looking for valid host
  hosts: all_uri_group
  uri:
    url: https://{{ fa_url }}/api/{{ api_version }}/volume
    method: POST
    validate_certs: no
    return_content: yes
    status_code: 200
    register: reply
    until: status_code == 200
    retries: 0
    delay: 0

But I have no idea how to correctly write it in Ansible.

2 Answers

Since skipping the execution of a host based on the return value of a previous host is quite tricky but skipping in a loop based on a previous return is quite easy, you could create a loop with all the hosts in the play with the special variable ansible_play_batch, then delegate the task according to the loop item.

Here is an example of my setting with 3 nodes.
Note that I am purposefully making the the first node fail to see how it reacts, based on your requirements.

Given the playbook:

- hosts: all
  gather_facts: no

  tasks:
    - uri:
        url: "http://example.{{ 'invalid' if item == 'node1' else 'com' }}"
        status_code: 200
      delegate_to: "{{ item }}"
      register: reply
      loop: "{{ ansible_play_hosts }}"
      when: "reply.status | default(0) != 200"
      run_once: true
      ignore_errors: yes
      
    - set_fact:
        selected_host: "{{ item.item }}"
      loop: "{{ reply.results }}"
      when: item is not skipped and item.status == 200
      run_once: true
      loop_control:
        label: "{{ item.item }}"

    - debug:
        var: selected_host
      run_once: true

This gives me the recap:

PLAY [all] ********************************************************************************************************

TASK [uri] ********************************************************************************************************
failed: [node1 -> node1] (item=node1) => changed=false 
  ansible_loop_var: item
  elapsed: 0
  item: node1
  msg: 'Status code was -1 and not [200]: Request failed: <urlopen error [Errno -2] Name does not resolve>'
  redirected: false
  status: -1
  url: http://example.invalid
ok: [node1 -> node2] => (item=node2)
skipping: [node1] => (item=node3) 
...ignoring

TASK [set_fact] ***************************************************************************************************
skipping: [node1] => (item=node1) 
ok: [node1] => (item=node2)
skipping: [node1] => (item=node3) 

TASK [debug] ******************************************************************************************************
ok: [node1] => 
  selected_host: node2

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

I don't see a straightforward way to fullfill your exact requirement (i.e. go through the list of host, stop when the first one answers correctly and register its name)

This is the closest I could come up with. Note that I am not using a host play in the loop but a specific loop on the uri task using your group. The idea is to call the url for each host then to print the first hostname in the list which gave a satisfying answer.

In my example below, I took for granted that fa_url and api_version are variables that can be set for each hosts in your group and that they are reachable from localhost.

- name: Find first responding host
  hosts: localhost
  gather_facts: false

  tasks:
    - name: Try uri on each host, ignore errors
      uri:
        url: https://{{ hostvar[item].fa_url }}/api/{{ hostvar[item].api_version }}/volume
        method: POST
        validate_certs: no
        return_content: yes
        status_code: 200
      register: reply
      ignore_errors: true
      loop: "{{ groups.all_uri_group }}"

    
    - name: Print the name of first responding host
      vars:
        first_responding: "{{ (reply.results | select('success') | first).item }}"
      msg: "First responding host is {{ first_responding }}"
Related