I'm working on a cluster setup and need to compare if the kernel versions of both the machines are same if not end the play using "meta" but it is not functioning as expected and giving error :
- name: Cluster setup
hosts: cluster_nodes
tasks:
- name: Check kernel version of primary
shell: uname -r
when: inventory_hostname in groups['primary']
register: primary
- name: check kernel version of secondary
shell: uname -r
when: inventory_hostname in groups['secondary']
register: secondary
- meta: end_play
when: primary.stdout != secondary.stdout
ERROR:
ERROR! The conditional check 'primary.stdout != secondary.stdout' failed. The error was: error while evaluating conditional (primary.stdout != secondary.stdout): 'dict object' has no attribute 'stdout'
The error appears to be in '/var/lib/awx/projects/pacemaker_RHEL_7_ST/main_2.yml': line 55, column 7, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- meta: end_play
^ here
Please suggest how to write a when condition to stop the play if the OS versions are not RHEL7 and both are of same kernel version.
Why firing a shell when the information you need is directly inside the gathered facts?
Moreover, your above logic is wrong as:
all servers in your cluster_nodes group go through all tasks which are skipped when the condition is not met (hence why you do not get a stdout defined on your register on skipped servers)
you are only trying to compare 2 servers (one from each primary and secondary group) where your cluster can grow and contain many. So what you want to check IMO is that all kernel version are aligned for all nodes. If this is not exactly what you want, you can still adapt from the below example.
Here is how I would do that check.
- name: Cluster setup
hosts: cluster_nodes
vars:
# Get all kernel versions from all nodes into a list
# Note that this var will be undefined if facts are not gathered
# prior to using it.
kernels_list: "{{ groups['cluster_nodes']
| map('extract', hostvars, 'ansible_kernel') | list }}"
tasks:
# Make sure the kernel list has a single unique value
# (i.e. all kernel versions are identical)
# We check only once for all servers in the play.
- name: Make sure all kernel versions are aligned
assert:
that:
- kernels_list | unique | count == 1
fail_msg: "Node kernel versions are not aligned ({{ kernels_list | string }})"
run_once: true
- name: go on with install if assert was ok
debug:
msg: Go on.
Related
This should be a very simple task but I'm unable to get past a syntax error.
In the below playbook, the first task gets the capabilities of Python and registers it in python_capability where python_capability.stdout becomes /usr/bin/python3.10 cap_sys_nice=ep.
In the next task, I split this string and set capabilities to everything following after the 0th item. In this case, capabilities becomes [cap_sys_nice=ep].
Then, I want to check the length of the above list and fail if it's > 1.
- hosts: tests
become: yes
pre_tasks:
- block:
- name: Ensure Python has CAP_SYS_NICE privileges
capabilities:
path: /usr/bin/python3.10
capability: cap_sys_nice+ep
state: present
register: python_capability
rescue:
- name: RESCUE | Get Python capabilities
set_fact:
capabilities: "{{ python_capability.stdout.split()[1:] }}"
- name: Get length
set_fact:
num_capabilities: "{{ capabilities | length }}"
- name: Failed when
failed_when: num_capabilities | int > 1
But I get the below syntax related error:
ERROR! no module/action detected in task.
The error appears to be in '/test.yml': line 25, column 11, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: Failed when
^ here
What am I missing?
You need to change the last task to:
- name: Validate number of capabilities
ansible.builtin.fail:
msg: "There are too many capabilities"
when: num_capabilities | int > 1
This will stop the execution if the condition is met.
One can recover failed hosts using rescue. How can I configure Ansible so that the other hosts in the play are aware of the host which will be recovered?
I thought I was smart, and tried using difference between ansible_play_hosts_all and ansible_play_batch, but Ansible doesn't list the failed host, since it's rescued.
---
- hosts:
- host1
- host2
gather_facts: false
tasks:
- block:
- name: fail one host
shell: /bin/false
when: inventory_hostname == 'host1'
# returns an empty list
- name: list failed hosts
debug:
msg: "{{ ansible_play_hosts_all | difference(ansible_play_batch) }}"
rescue:
- shell: /bin/true
"How can I configure Ansible so that the other hosts in the play are aware of the host which will be recovered?"
It seems that according documentation Handling errors with blocks
If any tasks in the block return failed, the rescue section executes tasks to recover from the error. ... Ansible provides a couple of variables for tasks in the rescue portion of a block: ansible_failed_task, ansible_failed_result
as well the source of ansible/playbook/block.py, such functionality isn't implemented yet.
You may need to implement some logic to keep track of the content of return values of ansible_failed_task and on which host it happened during execution. Maybe it is possible to use add_host module – Add a host (and alternatively a group) to the ansible-playbook in-memory inventory with parameter groups: has_rescued_tasks.
Or probably do further investigation beginning with default Callback plugin and Ansible Issue #48418 "Add stats on rescued/ignored tasks" since it added statistics about rescued tasks.
I want to execute tasks only if a file exists on the target.
I can do that, of course, with
- ansible.builtin.stat:
path: <remote_file>
register: remote_file
- name: ...
something:
when: remote_file.stat.exists
but is there something smaller?
You can, for example, check files on the host with
when: '/tmp/file.txt' is file
But that only checks files on host.
Is there also something like that available for files on remotes?
Added, as according the comment section I need to be bit more specific.
I want to run tasks on the target, and on the next run, they shouldn´t be executed again. So I thought to put a file somewhere on the target, and on the next run, when this file exists, the tasks should not be executed anymore.
- name: do big stuff
bigsgtuff:
when: not <file> exists
They should be executed, if the file does not exists.
I am not aware of a way to let the Control Node know during templating or compile time if there exist a specific file on Remote Node(s).
I understand your question that you
... want to execute tasks on the target ...
only if a certain condition is met on the target.
This sounds to be impossible without taking a look on the target before or do a lookup somewhere else, in example in a Configuration Management Database (CMDB).
... is there something smaller?
It will depend on your task, what you try to achieve and how do you like to declare a configuration state.
In example, if you you like to declare the absence of a file, there is no need to check if it exists before. Just make sure it becomes deleted.
---
- hosts: test
become: false
gather_facts: false
tasks:
- name: Remove file
shell:
cmd: "rm /home/{{ ansible_user }}/test.txt"
warn: false
register: result
changed_when: result.rc != 1
failed_when: result.rc != 0 and result.rc != 1
- name: Show result
debug:
msg: "{{ result }}"
As you see, it will be necessary to Defining failure and control how the task behaves. Another example for showing the content.
---
- hosts: test
become: false
gather_facts: false
tasks:
- name: Gather file content
shell:
cmd: "cat /home/{{ ansible_user }}/test.txt"
register: result
changed_when: false
failed_when: result.rc != 0 and result.rc != 1
- name: Show result
debug:
msg: "{{ result.stdout }}"
Please take note, for the given example tasks there are already specific Ansible modules available which do the job better.
According the additional given information in your comments I understand that you like to install or configure "something" and like to leave that fact left on the remote node(s). You like to run the task the next time on the remote node(s) in fact only if it is wasn't successful performed before.
To do so, you may have a look into Ansible facts and Discovering variables: facts and magic variables, especially into Adding custom facts.
What your installation or configuration tasks could do, is leaving a custom .fact file with meaningful keys and values after they were successful running.
During next playbook execution and if gather_facts: true, the information would be gathered from the setup module and you can than let tasks run based on conditions in ansible_local.
Further Q&A
How Ansible gather_facts and sets variables
An introduction to Ansible facts
Loading custom facts
Whereby the host facts can be considered as kind of distributed CMDB, by using facts Cache
fact_caching = yaml
fact_caching_connection = /tmp/ansible/facts_cache
fact_caching_timeout = 129600
you can have the information also available on the Control Node. It might even be possible to organize it in host_vars and group_vars.
Here is the problem I have. I am running the following playbook
- name: Check for RSA-Key existence
stat:
path: /opt/cert/{{item.username}}.key
with_items: "{{roles}}"
register: rsa
- name: debug
debug:
var: item.stat.exists
loop: "{{rsa.results}}"
- name: Generate RSA-Key
community.crypto.openssl_privatekey:
path: /opt/cert/{{item.username}}.key
size: 2048
when: item.stat.exists == False
with_items:
- "{{roles}}"
- "{{rsa.results}}"
This is the error I receive:
The error was: error while evaluating conditional (item.stat.exists == False): 'dict object' has no attribute 'stat'
The debug task is not firing any error
"item.stat.exists": true
What am I doing wrong and how can I fix my playbook to make it work?
TL;DR
Replace all your tasks with a single one:
- name: Generate RSA-Key or check they exist
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.username }}.key
size: 2048
state: present
with_items: "{{ roles }}"
Problem with your last loop in original example
I don't know what you are trying to do exactly when writing the following in your last task:
with_items:
- "{{roles}}"
- "{{rsa.results}}"
What I know is the actual result: you are looping over a single list made of roles elements at the beginning followed by rsa.results elements. Since I am pretty sure no elements in your roles list has a stat.exists entry, the error you get is quite expected.
Once you have looped over an original list (e.g. roles) and registered the result of the tasks (in e.g. rsa), you actually have all the information you need inside that registered var. rsa.results is a list of individual results. In each elements, you will find all the keys returned by the module you ran (e.g. stat) and an item key holding the original element that was used in the loop (i.e. an entry of your original roles list).
I strongly suggest you study this by yourself with most attention by looking at the entire variable to see how it is globally structured:
- name: Show my entire registered var
debug:
var: rsa
Once you have looked at your incoming data, it will become obvious that you should modify your last task as the following (note the item.item referencing the original element from previous loop):
- name: Generate RSA-Key
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.item.username }}.key
size: 2048
when: not item.stat.exists # Comparing to explicit False is bad use this instead
with_items: "{{ rsa.results }}"
Using stat here is an overkill
To go further, if all the above actually answers your direct question, it does not make much sense in Ansible world. You are doing a bunch of work that Ansible is already doing behind the scene for you.
The community.crypto.openssl_privatekey module create keys idempotently, i.e. it will create the key only if it doesn't exist and report changed or do nothing if the key already exists and report ok. So you can basically reduce all of your 3 tasks example to a single task
- name: Generate RSA-Key or check they exist
community.crypto.openssl_privatekey:
path: /opt/cert/{{ item.username }}.key
size: 2048
state: present # This is not necessary (default) but good practice
with_items: "{{ roles }}"
Consider changing your var name
Last, I'd like to mention that roles is actually a reserved name in Ansible. So defining a var with that name should issue a warning in current ansible version, and will probably be deprecated in some time.
Refs:
registering variables
registering variables with a loop
I need to check if VMs exist or not. The check is registered in one play, 'control' and referenced in another, 'production'. What is the correct format for this?
create_vm.yml:
---
- hosts: control
tasks:
- name: Check VM
virt:
command: "list_vms"
register: vms
- hosts: production
tasks:
- name: Create VM
STUFF
when: inventory_hostname not in groups['control']['vms']
I've tried different 'when' formats but they've all failed.
I have various errors depending on the format of when condition used; the following is for the specified condition.
fatal: [prod-operator]: FAILED! => {"failed": true, "msg": "The conditional check 'inventory_hostname not in groups['control']['vms']' failed. The error was: error while evaluating conditional (inventory_hostname not in groups['control']['vms']): Unable to look up a name or access an attribute in template string ({% if inventory_hostname not in groups['control']['vms'] %} True {% else %} False {% endif %}).\nMake sure your variable name does not contain invalid characters like '-': argument of type 'StrictUndefined' is not iterable\n\nThe error appears to have been in '/root/micks-ci-setup/production/virtual_machine/create-vm.yml': line 29, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Create Production VM\n ^ here\n"}
From what I've seen I should be able to reference the registered value form another task using a reference to host but it just won't work for me. I'll move on with a workaround of performing another check in the hosts: production play as follows.
(I'm doing a round-robin deployment of VMs hence the delegate_to complexity)
---
- hosts: control
tasks:
- name: Check VM
virt:
command: "list_vms"
register: vms
- hosts: production
tasks:
- name: Check VMs
virt:
command: "list_vms"
register: vms
delegate_to: '{{ groups["control"][play_hosts.index(inventory_hostname) % groups["control"]|length] }}'
And when condition as follows:
when: inventory_hostname not in vms.list_vms
If anyone can provide the "correct" method or a reason why it's not working for me then please feel free to post another answer.