Getting changed/failed hosts list from a previous task | Ansible

Viewed 1307

All,

Example: If i've got 20 hosts for a playbook and running them with Serial:10, below shell command runs on 10 hosts at a time. Once done handler task is called, wherein the task which creates dict (_dict) doesn't give a dictionary output thus the second task - Failed host - failed with mentioned error.

      - name: Run some shell command
        shell: "echo 2 > /abcd/abcd.txt"
        when: random condition is satisfied
        register: update2
        ignore_errors: yes
        notify: abc_handler

   - handler:
      - name: abcd_handler
        set_fact:
           _dict: "{{ dict(ansible_play_hosts|zip(
                    ansible_play_hosts|map('extract', hostvars, 'update2'))) }}"
        run_once: true
  

      - name: Find failed hosts 
        set_fact:
           _failed:  "{{ _dict|dict2items|json_query('[?value.failed].key') }}"
        run_once: true         

Handler First task output:

        "changed: false"
            "ansible_facts": {
                "_dict": "{u'host1': {'stderr_lines': [], u'changed': True,...u'host2':.....u'host10'}"

2nd handler task gives the mentioned error when the dict2items is run for above values.

Thank you.

2 Answers

Q: "List of hosts where a certain task executed, changed something, or got failed."

A: For example, the command makes no changes at test_11 changes the file at test_12 and fails at test_13

- hosts: test_11,test_12,test_13
  tasks:
    - shell:
        cmd: "echo 2 > /tmp/test/abcd.txt"
        creates: /tmp/test/abcd.txt
      register: update1
      ignore_errors: true
TASK [shell] ***********************************************************
changed: [test_12]
fatal: [test_13]: FAILED! => changed=true 
  cmd: echo 2 > /tmp/test/abcd.txt
  delta: '0:00:00.045992'
  end: '2021-04-25 23:22:31.623804'
  msg: non-zero return code
  rc: 2
  start: '2021-04-25 23:22:31.577812'
  stderr: '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
  stderr_lines: <omitted>
  stdout: ''
  stdout_lines: <omitted>
...ignoring
ok: [test_11]

Let's create a dictionary with the data first, e.g.

    - set_fact:
        _dict: "{{ dict(ansible_play_hosts|zip(
                        ansible_play_hosts|map('extract', hostvars, 'update1'))) }}"
      run_once: true

gives

  _dict:
    test_11:
      changed: false
      cmd: echo 2 > /tmp/test/abcd.txt
      failed: false
      rc: 0
      stdout: skipped, since /tmp/test/abcd.txt exists
      stdout_lines:
      - skipped, since /tmp/test/abcd.txt exists
    test_12:
      changed: true
      cmd: echo 2 > /tmp/test/abcd.txt
      delta: '0:00:00.032474'
      end: '2021-04-25 23:14:36.361510'
      failed: false
      rc: 0
      start: '2021-04-25 23:14:36.329036'
      stderr: ''
      stderr_lines: []
      stdout: ''
      stdout_lines: []
    test_13:
      changed: true
      cmd: echo 2 > /tmp/test/abcd.txt
      delta: '0:00:00.054980'
      end: '2021-04-25 23:14:35.565811'
      failed: true
      msg: non-zero return code
      rc: 2
      start: '2021-04-25 23:14:35.510831'
      stderr: '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
      stderr_lines:
      - '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
      stdout: ''
      stdout_lines: []

Note that test_11 is reported ok not skipped despite the fact that the registered variable shows "stdout: skipped, since /tmp/test/abcd.txt exists".

The analysis is now trivial, e.g.

    - set_fact:
        _failed:  "{{ _dict|dict2items|json_query('[?value.failed].key') }}"
      run_once: true

gives the list of the failed hosts

  _failed:
  - test_13

and the next task

    - set_fact:
        _changed: "{{ (_dict|dict2items|json_query('[?value.changed].key'))|
                       difference(_failed) }}"
        _ok: "{{ _dict|dict2items|json_query('[?value.changed == `false`].key') }}"
      run_once: true

gives

  _changed:
  - test_12

  _ok:
  - test_11

Note that

  1. The failed hosts need to be subtracted from the changed hosts because failed hosts are also reported changed.

  2. There will be no registered variable if a task is skipped, of course.


Serial

Split the playbook into 2 plays if serial is used. e.g.

shell> cat playbook.yml
- hosts: all
  serial: 10
  tasks:
    - shell:
        cmd: "echo 2 > /tmp/test/abcd.txt"
        creates: /tmp/test/abcd.txt
      register: update1
      ignore_errors: true

- hosts: all
  tasks:
    - set_fact:
        _dict: "{{ dict(ansible_play_hosts|zip(
                        ansible_play_hosts|map('extract', hostvars, 'update1'))) }}"
      run_once: true

It seems you would like to get the hosts on which the command task (shown in the question) failed or changed, and then target them for some other tasks.

There are two things required for this:

  • If the command task fails, playbook execution will stop and hence none of the following tasks will run. So we need to add ignore_errors flag to the task

  • add_host module to create a new group of hosts when the task failed or changed

So finally tasks like below should do the trick:

- hosts: some_group
  serial: 1

    - name: update file count
      shell: "echo 2 > /home/ec2-user/abcd.txt"
      when:
        - count.stdout == "1"
      register: update1
      ignore_errors: true

    - name: conditionally add the hosts from current play hosts to a new group
      add_host:
        groups:
          - new_group
        host: "{{ ansible_hostname }}"
      when: >
        cmd_stat is failed or
        cmd_stat is changed

# Then have a play targeting the new group
- hosts: new_group
  tasks:
  # Tasks to be performed

Though the use of serial might make the whole playbook run longer if there are lot of hosts.

Related