Looping a task until it succeeds

836 views
Skip to first unread message

Cade Lambert

unread,
Jun 25, 2020, 9:51:12 AM6/25/20
to Ansible Project
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.

Vladimir Botka

unread,
Jun 25, 2020, 10:21:36 AM6/25/20
to Cade Lambert, ansible...@googlegroups.com
On Thu, 25 Jun 2020 06:51:12 -0700 (PDT)
Cade Lambert <crl...@gmail.com> wrote:

> I'd like it to stop processing the loop at item=4, since that's the first
> success.

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

--
Vladimir Botka

Cade Lambert

unread,
Jun 25, 2020, 11:22:47 AM6/25/20
to Ansible Project
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.

Vladimir Botka

unread,
Jun 25, 2020, 11:56:48 AM6/25/20
to Cade Lambert, ansible...@googlegroups.com
> > > On Thu, 25 Jun 2020 06:51:12 -0700 (PDT)
> > > Cade Lambert <crl...@gmail.com <javascript:>> wrote:
> > > I'd like it to stop processing the loop at item=4, since that's the
> > > first success.

> > 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' ... 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'

--
Vladimir Botka

Brian Coca

unread,
Jun 25, 2020, 1:06:02 PM6/25/20
to Ansible Project
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


--
----------
Brian Coca

Stefan Hornburg (Racke)

unread,
Jun 25, 2020, 1:22:23 PM6/25/20
to ansible...@googlegroups.com
On 6/25/20 3:51 PM, Cade Lambert wrote:
> 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

> 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.
>
> --
> You received this message because you are subscribed to the Google Groups "Ansible Project" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to
> ansible-proje...@googlegroups.com <mailto:ansible-proje...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com
> <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com?utm_medium=email&utm_source=footer>.


--
Ecommerce and Linux consulting + Perl and web application programming.
Debian and Sympa administration. Provisioning with Ansible.

signature.asc

Cade Lambert

unread,
Jun 25, 2020, 2:07:05 PM6/25/20
to Ansible Project
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.

Cade Lambert

unread,
Jun 25, 2020, 2:13:07 PM6/25/20
to Ansible Project
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?

Stefan Hornburg (Racke)

unread,
Jun 25, 2020, 2:42:27 PM6/25/20
to ansible...@googlegroups.com
On 6/25/20 8:13 PM, Cade Lambert wrote:
> 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

>
> On Thursday, June 25, 2020 at 1:22:23 PM UTC-4, Stefan Hornburg (Racke) wrote:
>
> On 6/25/20 3:51 PM, Cade Lambert wrote:
> > 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 <http://1.1.1.0/24>'}
> > ansible...@googlegroups.com <javascript:> <mailto:ansible-proje...@googlegroups.com <javascript:>>.
> <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com?utm_medium=email&utm_source=footer
> <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com?utm_medium=email&utm_source=footer>>.
>
>
>
> --
> Ecommerce and Linux consulting + Perl and web application programming.
> Debian and Sympa administration. Provisioning with Ansible.
>
> --
> You received this message because you are subscribed to the Google Groups "Ansible Project" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to
> ansible-proje...@googlegroups.com <mailto:ansible-proje...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/ansible-project/5c53d589-f668-41ce-babe-6ff3576bdbd9o%40googlegroups.com
> <https://groups.google.com/d/msgid/ansible-project/5c53d589-f668-41ce-babe-6ff3576bdbd9o%40googlegroups.com?utm_medium=email&utm_source=footer>.
signature.asc

Cade Lambert

unread,
Jun 25, 2020, 3:00:37 PM6/25/20
to Ansible Project
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.
>     > ansible...@googlegroups.com <javascript:> <mailto:ansible-project+unsub...@googlegroups.com <javascript:>>.
>     > To view this discussion on the web visit
>     > https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com
>     <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com>
>     >
>     <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com?utm_medium=email&utm_source=footer
>     <https://groups.google.com/d/msgid/ansible-project/3687c5df-e90d-4d50-af6c-38b467951ad3o%40googlegroups.com?utm_medium=email&utm_source=footer>>.
>
>
>
>     --
>     Ecommerce and Linux consulting + Perl and web application programming.
>     Debian and Sympa administration. Provisioning with Ansible.
>
> --
> You received this message because you are subscribed to the Google Groups "Ansible Project" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to

Vladimir Botka

unread,
Jun 25, 2020, 5:13:16 PM6/25/20
to Brian Coca, ansible...@googlegroups.com
On Thu, 25 Jun 2020 13:05:23 -0400
Brian Coca <bc...@redhat.com> wrote:

> 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

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


--
Vladimir Botka

Cade Lambert

unread,
Jun 26, 2020, 9:46:05 AM6/26/20
to Ansible Project
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 }}"

Vladimir Botka

unread,
Jun 26, 2020, 11:37:53 AM6/26/20
to Cade Lambert, ansible...@googlegroups.com
On Fri, 26 Jun 2020 06:46:04 -0700 (PDT)
Cade Lambert <crl...@gmail.com> wrote:

> 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

--
Vladimir Botka

Cade Lambert

unread,
Jun 26, 2020, 2:16:37 PM6/26/20
to Ansible Project
Thanks for the explanation.  The default piece was tripping me up but it all makes total sense now.
Reply all
Reply to author
Forward
0 new messages