Register output ansible playbook

Viewed 543

I have a playbook to run command on a list of servers and put the results in a file.
But, sometimes, I can't connect to those servers due to an incorrect login/password or because it is unreachable.

When it is unreachable, I have this on the console:

fatal: [fqdn]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname fqdn: Name or service not known", "unreachable": true}

When I have an incorrect login or password, I have this:

fatal: [fqdn]: UNREACHABLE! => {"changed": false, "msg": "Invalid/incorrect password: Permission denied, please try again.", "unreachable": true}

I would like to capture this message in the output console to write in a template Jinja.
In order to have kind of a report.

I have done this in the playbook

- name: Listing des packages
  hosts: all
  gather_facts: false

  tasks:
    - name: Collect only selected facts
      setup:
        gather_subset: min

    - name: Total number of updates
      shell: yum check-update | wc -l
      register: nbupdates

    - name: Output to html file
      template:
        src: ./jinja/src/tpl_dashboard_update.j2
        dest: ./jinja/dst/dashboard_update.html
        force: yes
      delegate_to: localhost
      run_once: true

and the Jinja template

{% for host in ansible_play_hosts_all %}
        <tr>
            <td>{{ loop.index }}</td>
            <td>{{ host | upper }}</td>
            {% if hostvars[host]['ansible_distribution'] is not defined %}
                <td colspan="4">Injoignable</td>
            {% else %}
                <td>{{ hostvars[host]['ansible_distribution'] }} {{ hostvars[host]['ansible_distribution_version'] }}</td>
                <td> {% if hostvars[host].nbupdates.stdout_lines.0 is defined %} {{ hostvars[host].nbupdates.stdout_lines.0 }} {% else %} --- {% endif %} </td>
            {% endif %}
        </tr>
{% endfor %}

It is not working properly, I only have the result when it is a success in the file and not the result, when it fails.

How I can "capture" the error message when it's unreachable or when I have a login/password error?

3 Answers

Q: "How I can "capture" the error message when it's unreachable or when I have a login/password error?"

A: For example, the playbook below creates the dictionary hosts_unreachable

shell> cat pb.yml
- hosts: test_11,test_12,test_99
  gather_facts: false
  tasks:
    - ping:
      register: result
      ignore_unreachable: true
    - set_fact:
        hosts_unreachable: "{{ hostvars|dict2items|json_query(_query) }}"
      vars:
        _query: "[?value.result.unreachable].{host: key,
                                              msg: value.result.msg}"
      run_once: true
    - debug:
        var: hosts_unreachable
      run_once: true

gives

shell> ansible-playbook pb.yml

PLAY [test_11,test_12,test_99] *********************************************

TASK [ping] ****************************************************************
fatal: [test_99]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname test_99: Name or service not known'
  skip_reason: Host test_99 is unreachable
  unreachable: true
fatal: [test_12]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: user@test_12: Permission denied (publickey,keyboard-interactive).'
  skip_reason: Host test_12 is unreachable
  unreachable: true
ok: [test_11]

TASK [set_fact] ************************************************************
ok: [test_11]

TASK [debug] ***************************************************************
ok: [test_11] => 
  hosts_unreachable:
  - host: test_12
    msg: 'Failed to connect to the host via ssh: user@test_12: Permission denied (publickey,keyboard-interactive).'
  - host: test_99
    msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname test_99: Name or service not known'

PLAY RECAP ******************************************************************
test_11: ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_12: ok=0    changed=0    unreachable=1    failed=0    skipped=1    rescued=0    ignored=0   
test_99: ok=0    changed=0    unreachable=1    failed=0    skipped=1    rescued=0    ignored=0

Similarly, get the setup, select variables you want, and create the dictionary hosts_reachable

    - setup:
      register: result
      ignore_unreachable: true
    - set_fact:
        hosts_reachable: "{{ hostvars|dict2items|json_query(_query)|
                                                 rejectattr('distro', 'none') }}"
      vars:
        _query: "[].{host: key,
                     distro: value.ansible_distribution,
                     version: value.ansible_distribution_version}"
      run_once: true

gives

  hosts_reachable:
  - distro: FreeBSD
    host: test_11
    version: '13.0'

Create the HTML file

    - copy:
        content: |-
          <table>
          {% for i in hosts_unreachable %}
          <tr>
            <td>{{ loop.index }}</td>
            <td>{{ i.host|upper }}</td>
            <td>Injoinable</td>
            <td>{{ i.msg }}</td>
          </tr>
          {% endfor %}
          {% for i in hosts_reachable %}
          <tr>
            <td>{{ loop.index }}</td>
            <td>{{ i.host|upper }}</td>
            <td>{{ i.distro }} {{ i.version }}</td>
          </tr>
          {% endfor %}
          </table>
        dest: hosts.html
      delegate_to: localhost
      run_once: true

gives

shell> cat hosts.html 
<table>
<tr>
  <td>1</td>
  <td>TEST_12</td>
  <td>Injoinable</td>
  <td>Failed to connect to the host via ssh: user@test_12: Permission denied (publickey,keyboard-interactive).</td>
</tr>
<tr>
  <td>2</td>
  <td>TEST_99</td>
  <td>Injoinable</td>
  <td>Failed to connect to the host via ssh: ssh: Could not resolve hostname test_99: Name or service not known</td>
</tr>
<tr>
  <td>1</td>
  <td>TEST_11</td>
  <td>FreeBSD 13.0</td>
</tr>
</table>

Optionally, put all available variables into the dictionary hosts_reachable

    - set_fact:
        hosts_reachable: "{{ hostvars|
                             dict2items|
                             rejectattr('value.ansible_distribution', 'undefined')|
                             items2dict }}"

Then, the template below gives the same result

          {% for k,v in hosts_reachable.items() %}
          <tr>
            <td>{{ loop.index }}</td>
            <td>{{ k|upper }}</td>
            <td>{{ v.ansible_distribution }} {{ v.ansible_distribution_version }}</td>
          </tr>
          {% endfor %}

You will have to use ignore_unreachable: yes in order to gather a result form an unreachable host.
You will also have to skip those unreachable hosts in the tasks that follows your setup.
And, in order to overcome the possibility that all hosts are unreachable, you probably want to target localhost, all and skip localhost when it is not needed.

Two other side notes:

  • Prefer the use of a dedicated module when it exists. You shell task to get all the available update would be better fitted in a yum task.
  • This is a quite heavy constructions:
    {% if hostvars[host].nbupdates.stdout_lines.0 is defined %} 
      {{ hostvars[host].nbupdates.stdout_lines.0 }} 
    {% else %} 
      --- 
    {% endif %}
    
    And can be easily shortened with a default filter:
    {{ hostvars[host].nbupdates.stdout_lines.0 | default('---') }}
    
    But, this is not something we should use here since we should rely on the length of the list returned by the yum module

Here would be an example of a playbook with all this together:

- hosts: localhost, all
  gather_facts: no

  tasks:
    - setup:
        gather_subset: min
      register: setup
      ignore_unreachable: yes
      when: inventory_hostname != 'localhost'

    - yum:
        list: updates
      register: yum
      when:
        - setup is reachable
        - inventory_hostname != 'localhost'

    - template:
        src: ./jinja/src/tpl_dashboard_update.j2
        dest: ./jinja/dst/dashboard_update.html
        force: yes
      delegate_to: localhost
      run_once: true

And here is the adapted template tpl_dashboard_update.j2:

<table>
  {% for host in ansible_play_hosts_all if host != 'localhost' %}
    <tr>
      <td>{{ loop.index }}</td>
      <td>{{ host | upper }}</td>
      {% if hostvars[host].setup is unreachable %}
        <td>Injoignable</td>
        <td>{{ hostvars[host].setup.msg }}</td>
      {% else %}
        <td>
          {{ hostvars[host].ansible_distribution }}
          {{ hostvars[host].ansible_distribution_version }}
        </td>
        <td>
          {% if hostvars[host].yum.results is defined %}
            {{ hostvars[host].yum.results | length }}
          {% else %}
            ---
          {% endif %}
        </td>
      {% endif %}
    </tr>
  {% endfor %}
</table>

This all would yield a table like (style added for readability):

table, th, td {
  border: 1px solid black;
  border-collapse: collapse;
  padding: 4px;
}
<table>
  <tr>
    <td>1</td>
    <td>TEST</td>
    <td>Injoignable</td>
    <td>Failed to connect to the host via ssh: ssh: Could not resolve hostname test: Name does not resolve</td>
  </tr>
  <tr>
    <td>2</td>
    <td>NODE1</td>
    <td>
      Fedora 35
    </td>
    <td>
      29
    </td>
  </tr>
  <tr>
    <td>3</td>
    <td>NODE2</td>
    <td>Injoignable</td>
    <td>Invalid/incorrect password: Permission denied, please try again.</td>
  </tr>
</table>

many thank for your replies, I have done a lot of tests today, I have now a playbook works successfully.

I have this :


- name: Listing des packages
  hosts: all
  gather_facts: false

  tasks:
    - name: Collect only selected facts
      setup:
        gather_subset: min
      register: setup_status
      ignore_unreachable: yes
      when: inventory_hostname != 'localhost'

   - name: Total number of updates
      yum:
        list: updates
      register: nbupdates
      ignore_errors: yes
      when:
        - setup_status is reachable
        - inventory_hostname != 'localhost'     
 
   - name: Total number of security updates
      shell: yum list updates --security | grep "needed for security" | awk '{print $1}'
      register: nbsecupdates
      when:
        - setup_status is reachable
        - inventory_hostname != 'localhost'  

    - name: Total number of bugfix updates
      shell: yum list updates --bugfix | grep "needed for security" | awk '{print $1}'
      register: nbbugupdates
      when:
        - setup_status is reachable
        - inventory_hostname != 'localhost' 


- name: Output to html file

      template:
        src: ./jinja/src/tpl_dashboard_update.j2
        dest: ./jinja/dst/dashboard_update.html
        force: yes
      delegate_to: localhost
      run_once: true

In my jinja template, I have this

{% for host in ansible_play_hosts %}
        <tr>
            <td>{{ loop.index }}</td>
            <td>{{ host | upper }}</td>
            {% if hostvars[host].setup_status is unreachable %}
                <td colspan="4">{{ hostvars[host].setup_status.msg }}</td>

            {% else %}

                <td>{{ hostvars[host]['ansible_distribution'] }} {{ hostvars[host]['ansible_distribution_version'] }}</td>
      

                {% if hostvars[host].nbupdates.results is undefined %}     
                    <td> {{ hostvars[host].nbupdates.msg }} </td>
                {% else %}
                    <td> {{ hostvars[host].nbupdates.results | length | default('---') }} </td>
                {% endif %}

                {% if hostvars[host].nbsecupdates is undefined %}     
                    <td> {{ hostvars[host].nbsecupdates.msg }} </td>
                {% else %}
                    <td> {{ hostvars[host].nbsecupdates.stdout_lines.0 | default('---') }} </td>

                {% endif %}
                {% if hostvars[host].nbbugupdates is undefined %}     
                    <td> {{ hostvars[host].nbbugupdates.msg }} </td>
                {% else %}

                    <td> {{ hostvars[host].nbbugupdates.stdout_lines.0 | default('---') }} </td>

                {% endif %}

            {% endif %}
        </tr>

{% endfor %}

I know yum module is better than the shell command but when I want to count only the security and bugfix update with yum command,

it's not working so I'm using shell command.

Now, when I run the playbook :

  1. I have in my report the total updates, the bugfix updates and the security updates when the hosts are reacheables.

  2. I have the error when the hosts are not reacheables.

  3. I have also the error when the hosts are reacheables and they don't have the yum command to list the updates (repoquery is required to use list= with this module.)

I had to add this option in the update module "ignore_errors: yes" otherwise, I cant' have the hosts with the missing yum command.

In the jinja template, I had some difficulties today, I had this error :

    fatal: [srvlt-a1]: FAILED! => {"changed": false, "msg": "AnsibleUndefinedVariable: 'dict object' has no attribute 'results'"}

because I'm not familiar with dictionnnary.

But now, I think it's ok.

Thank you very much for your help and your suggestions.

Related