roht.no: on.thor

Projects, articles, notes and other technical musings from a DevOps focused developer

Working Around Recursive until Loops in Ansible

Thor K. Høgås

⏱ 3 minutes read. 📅 Published . ✎ Last updated .

A way to accomplish the use of until with blocks of code. Hard to accomplish normally, as there is no support for loop on blocks, nor support for until. However, you can work around this by recursively including the same task file as part of the rescue block.

# wait_until_success.yml
- name: 'Wait until success'
  block:
    - name: Get server updated ip
      uri:
        url: https://localhost/ip
        return_content: yes
        status_code: 200
      register: ip

    - name: ssh to the server
      wait_for:
        host: "{{ ip }}"
        port: 22
        timeout: 30
        state: started
  rescue:
    - debug:
        msg: "Failed to connect - Retrying..."
    - include_tasks: wait_until_success.yml

If you want to fail after trying a few times you may be interested in a further altered version:

# wait_until_success_max_retries.yml
- name: 'Wait until success'
  block:
    - name: Set the retry count
      set_fact:
        retry_count: "{{ 0 if retry_count is undefined else retry_count|int + 1 }}"

    - name: Get server updated ip
      uri:
        url: https://localhost/ip
        return_content: yes
        status_code: 200
      register: ip

    - name: ssh to the server
      wait_for:
        host: "{{ ip }}"
        port: 22
        timeout: 30
        state: started
  rescue:
    - fail:
        msg: Ended after 5 retries
      when: retry_count|int == 5
    - debug:
        msg: "Failed to connect - Retrying..."
    - include_tasks: wait_until_success.yml

As evident you can accomplish the same behaviour, although it won't be as simple as first envisioned. As Ansible developer “Toshio Kuratomi” pointed out, there are numerous pitfalls (excerpt):

  • Looping over includes already serves the same function as looping over blocks. The syntax is different but the functionality is the same.
  • A naive approach to looping over blocks (by turning them into dynamic includes behind the scenes) would be possible but it would bring with it all the assumptions that plague dynamic includes without being obvious to the user why a block acted in a different way when it was inside of a loop versus outside.
  • Similarly, statically expanding the loop would entail similar shortcomings found when doing static includes (early evaluation of variables, no inventory variables, etc.).
  • So a non-naive way would have to be sought out for looping over blocks. This probably entails a reworking of core architecture as neither looping over dynamic nor looping over static includes offers a model that we can apply here. Any implementation needs to make sure that blocks inside and outside of loops can:
    • Reference the same set of variables with the same results
    • Needs to consider rescue/always semantics …are they also looped?
    • Any_errors_fatal, serial, free vs linear strategies and other interactions.

Personally I think the most interesting part of the reply is the following passage (my emphasis highlighted):

One of the difficulties in implementing features like this is that we are trying to walk a fine line between automation and not becoming a programming language. We want people to be able to declare what their machines look like, using little bits of programming as shortcuts, to make the declarations clearer, and as an escape hatch when declarative forms are not sufficient. We do not want to create an environment where playbooks are mostly imperative code which the next sysadmin has to puzzle through and interpret. Even within the core team we go back and forth on this kind of feature, struggling to balance the needs of functionality, code maintenance, playbook maintenance, overlap with existing features, etc.

Ansible playbooks should be to the highest degree possible declarative. The separation between modifying the state and the desired state is one of SaltStack's strengths. The lack of that in Ansible blurs the line and inches towards imperativity. Your actions should strive to be declarative, with idempotency and abstractions to help accomplish that on a higher level when you cannot on a lower one.