Looping a task until it succeeds

My use case: we use Infoblox for our DNS. What I’m trying to do is automatically create a host record with the next available hostname that isn’t already in DNS. My plan was to loop through a formatted hostname, incrementing a number in that hostname until a hostname is found that isn’t already in Infoblox. Here’s what I have so far:

  • name: Assign next available IP address in your choosen vlan
    nios_host_record:
    name: “{{ ‘hostname%02d’ | format(item) }}”
    ipv4:
  • address: {nios_next_ip: ‘1.1.1.0/24’}
    state: present
    view: Internal
    provider: “{{ provider }}”
    loop: “{{ range(1, 5 + 1)|list }}”
    register: loop_result
    ignore_errors: true

So for this example, in the DNS, there’s already a hostname01, hostname02, hostname03. The output will be:

TASK [Assign next available IP address in your choosen vlan] *******************************************************************************************************************************
failed: [localhost] (item=1) => {“ansible_loop_var”: “item”, “attempts”: 1, “changed”: false, “code”: “Client.Ibap.Data.Conflict”, “item”: 1, “msg”: “The record ‘hostname01’ already exists.”, “operation”: “create_object”, “type”: “AdmConDataError”}

failed: [localhost] (item=2) => {“ansible_loop_var”: “item”, “attempts”: 1, “changed”: false, “code”: “Client.Ibap.Data.Conflict”, “item”: 2, “msg”: “The record ‘hostname02’ already exists.”, “operation”: “create_object”, “type”: “AdmConDataError”}
failed: [localhost] (item=3) => {“ansible_loop_var”: “item”, “attempts”: 1, “changed”: false, “code”: “Client.Ibap.Data.Conflict”, “item”: 3, “msg”: “The record ‘hostname03’ already exists.”, “operation”: “create_object”, “type”: “AdmConDataError”}
changed: [localhost] => (item=4)

changed: [localhost] => (item=5)

I’d like it to stop processing the loop at item=4, since that’s the first success. I’ve messed with a few different until statements at the end but nothing seems to really work. Thanks.

This is not possible atm. All items in the loop will be processed. The
details have already been discussed here
https://groups.google.com/forum/#!topic/ansible-project/uJqSmkKTpZc

Ok, I tried the ‘hack’ in that thread but it didn’t work for me. I was hoping for a clean way to do what I need but l can’t think of anything clean, so I went with functional. I got the result I was looking for by using the same loop against the host command on the shell, dumping that into a file, and then grepping the first hostname that isn’t found. Ugly but it works.

There is no hack available to break a loop. All items of a loop will be
processed atm. Wouldn't be better to test the registered results? For example

    - command: '[ "{{ item }}" -gt "3" ]'
      loop: "{{ range(1, 5 + 1)|list }}"
      register: result
      ignore_errors: true
    - set_fact:
        first: "{{ result.results|
                   selectattr('rc', '==', 0)|
                   map(attribute='item')|
                   first }}"
    - debug:
        var: first

gives

  first: '4'

something like (might need to tweak syntax to get right):

when: not select(result[|deault({'results': }, 'success' )|list

will skip the rest of the iterations once one succeeds

My use case: we use Infoblox for our DNS. What I'm trying to do is automatically create a host record with the next
available hostname that isn't already in DNS. My plan was to loop through a formatted hostname, incrementing a number
in that hostname until a hostname is found that isn't already in Infoblox. Here's what I have so far:

- name: Assign next available IP address in your choosen vlan
nios_host_record:
name: "{{ 'hostname%02d' | format(item) }}"
ipv4:
- address: {nios_next_ip: '1.1.1.0/24'}
state: present
view: Internal
provider: "{{ provider }}"
loop: "{{ range(1, 5 + 1)|list }}"
register: loop_result
ignore_errors: true

Hello Cade,

you could query first the existing DNS records in Infoblox and determine the next hostname from these results.

Regards
         Racke

I messed around with this a little bit. It gets a little complicated to parse the next available hostname from the output but I did get it to work.

  • set_fact:
    first: “{{ result.results|
    selectattr(‘rc’, ‘equalto’, 1)|
    map(attribute=‘item’)|
    first }}”

  • debug:
    msg: “{{ result.results[first|int-1].cmd | regex_search(‘(?<=\s).*’) }}”

This will give me:

ok: [localhost] => {
“msg”: “hostname04”
}

So yes, this is another way I could achieve finding the next available hostname.

So I tried using the nios module to query hostnames but I couldn’t find a way to query part of a hostname. It seemed that it only accepted exact names. Is this something you’ve done before?

So I tried using the nios module to query hostnames but I couldn't find a way to query part of a hostname. It seemed
that it only accepted exact names. Is this something you've done before?

No, but in general it scales better to see what's there and create the new record based on that (2 API calls) instead
of blindly trying out (1 + n API calls).

Regards
        Racke

I might mess with trying to get it to search partial hostnames later. If I could use a wildcard, I could generate a list in a variable and then manipulate that list.

Thank you very much! I got it. For example

    - command: '[ "{{ item }}" -gt "3" ]'
      loop: "{{ range(1, 5 + 1)|list }}"
      register: result
      ignore_errors: true
      when: not condition
      vars:
        condition: "{{ (result|default({'rc': 1})).rc == 0 }}"

gives

    changed: [localhost] => (item=4)
    skipping: [localhost] => (item=5)
    ...ignoring

Awesome, I got it to work with my nios task as well, just had to change rc to failed since I had no rc in my output.

I’m not totally sure I follow why it skips after the first success though. Can you explain what’s happening in this statement: condition: “{{ (result|default({‘rc’: 1})).rc == 0 }}”

Sure. Let's start with the fact that "result" is available at controller
after each iteration. It's necessary to keep in mind that the remote host is
the origin of the "result". Hence, the data must be sent from the remote host
to the controller after each iteration.

If you want to see what data is available in the dictionary "result" after
each iteration run this task

    - shell: 'echo {{ condition }} && [ "{{ item }}" -gt "2" ]'
      loop: "{{ range(1, 3 + 1)|list }}"
      register: result
      ignore_errors: true
      vars:
        condition: '{{ result|default({"rc": 1}) }}'
    - debug:
        var: result

You'll see that "result" of each iteration will be concatenated into the
list "result.results". Knowing this, simply pickup an attribute to test the
success of each iteration. You've already found out how.

Now to the "condition". The "when" statement is evaluated at each iteration
and before the first iteration as well. There is no variable "result"
available before the first iteration and the task would crash. Hence,
"default" value is needed. In this case

    result>default({"rc": 1})

The attribute "rc" is tested for success. Hence the default value {"rc": 1}
because we always want to run the first iteration. Next iterations evaluate
de facto the following expression because "result" was provided by the
previous iteration

    result.rc == 0

HTH,

  -vlado

Thanks for the explanation. The default piece was tripping me up but it all makes total sense now.