I have the following Ansible playbook to create a cronjob on red hat Linux servers, this works well, but the problem is that it runs every time irrespective of whether the same cronjob is already present or not. I know it runs because it generates new hour and minute values every time. Is there any way to make it not run when this job is already present in crontab?
- hosts: all
tasks:
- name: Generate randon minute
set_fact:
random_minute: "{{ 59 | random }}"
run_once: yes
- debug: var=random_minute
- name: Generate random hour
set_fact:
random_hour: "{{ 23 | random }}"
run_once: yes
- debug: var=random_hour
- name: "Cronjob Entry"
cron:
name: "### oscompliance cronjob"
minute: "{{ random_minute }}"
hour: "{{ random_hour }}"
day: "*"
month: "*"
weekday: "*"
job: /users/user1/scripts/os_check >> /tmp/os_checkoutput 2>&1
Related
I have a playbook that controls a clustered application. The issue is this playbook can be called/executed a few different ways (manual on the cmd line[multiple SREs working], scheduled task, or programmatically via a 3rd party system).
The problem is if the playbook tries to execute simultaneously, it could cause some issues to the application (nature of the application).
Question:
Is there a way to prevent the same playbook from running concurrently on the same Ansible server?
Environment:
ansible [core 2.11.6]
config file = /app/ansible/ansible_linux_playbooks/playbooks/scoutam_client_configs_playbook/ansible.cfg
configured module search path = ['/etc/ansible/library/modules']
ansible python module location = /usr/local/lib/python3.9/site-packages/ansible
ansible collection location = /app/ansible/ansible_linux_playbooks/playbooks/scoutam_client_configs_playbook/collections
executable location = /usr/local/bin/ansible
python version = 3.9.7 (default, Nov 1 2021, 11:34:21) [GCC 8.4.1 20200928 (Red Hat 8.4.1-1)]
jinja version = 3.0.2
libyaml = True
you could test if file exist at the start of playbook and stop the play if the file exist with meta, if not you create the file to block another launch:
- name: lock_test
hosts: all
vars:
lock_file_path: /tmp/ansible-playbook.lock
pre_tasks:
- name: Check if some file exists
delegate_to: localhost
stat:
path: "{{ lock_file_path }}"
register: lock_file
- block:
- name: "end play "
debug:
msg: "playbook already launched, ending play"
- meta: end_play
when: lock_file.stat.exists
- name: create lock_file {{ lock_file_path }}
delegate_to: localhost
file:
path: "{{ lock_file_path }}"
state: touch
# ****************** tasks start
tasks:
- name: debug
debug:
msg: "something to do"
# ****************** tasks end
post_tasks:
- name: delete the lock file {{ lock_file_path }}
delegate_to: localhost
file:
path: "{{ lock_file_path }}"
state: absent
but you have to have only one playbook in your play even the first playbook stops, the second is launched except if you do the same test in the next playbook.
it exist a little lapse time before test and creation of file... so the probality to launch twice the same playbook in same second is very low.
The solution will be always better than you have actually
Another solution is to lock an existing file, and test if file is locked or not, but be careful with this option.. see lock, flock in unix command
You can create a lockfile on the controller with the PID of the ansible-playbook process.
- delegate_to: localhost
vars:
lockfile: /tmp/thisisalockfile
my_pid: "{{ lookup('pipe', 'cut -d\" \" -f4 /proc/$PPID/stat') }}"
lock_pid: "{{ lookup('file', lockfile) }}"
block:
- name: Lock file
copy:
dest: "{{ lockfile }}"
content: "{{ my_pid }}"
when: my_lockfile is not exists
or ('/proc/' ~ lock_pid) is not exists
or 'ansible-playbook' not in lookup('file', '/proc/' ~ lock_pid ~ '/cmdline')
- name: Make sure we won the lock
assert:
that: lock_pid == my_pid
fail_msg: "{{ lockfile }} is locked by process {{ lock_pid }}"
Finding the current PID is the trickiest part; $PPID in the lookup is still the PID of a child, so we're grabbing the grandparent out of /proc/
I wanted to post this here but do not consider it a final/perfect answer.
it does work for general purposes.
I put this 'playbook_lock.yml' at the root of my playbook and call it in before any roles.
playbook_lock.yml:
# ./playbook_lock.yml
#
## NOTES:
## - Uses '/tmp/' on Ansible server as lock file directory
## - Format of lock file: E.g. 129416_20211103094638_playbook_common_01.lock
## -- Detailed explanation further down
## - Race-condition:
## -- Assumption playbooks will not run within 10sec of each other
## -- Assumption lockfiles were not deleted within 10sec
## -- If running the playbook manually with manual input of Ansible Vault
## --- Enter creds within 10 sec or the playbook will consider this run legacy
## - Built logic to only use ansbile.builin modules to not add additional requirements
##
#
---
## Build a transaction ID from year/month/day/hour/min/sec
- name: debug_transactionID
debug:
msg: "{{ transactionID }}"
vars:
filter: "{{ ansible_date_time }}"
transactionID: "{{ filter.year + filter.month + filter.day + filter.hour + filter.minute + filter.second }}"
run_once: true
delegate_to: localhost
register: reg_transactionID
## Find current playbook PID
## Race-condition => assumption playbooks will not run within 10sec of each other
## If playbook is already running >10secs, this return will be empty
- name: debug_current_playbook_pid
ansible.builtin.shell:
## serach PS for any command matching the name of the playbook | remove the 'grep' result | return only the 1st one (if etime < 10sec)
cmd: "ps -e -o 'pid,etimes,cmd' | grep {{ ansible_play_name }} | grep -v grep | awk 'NR==1{if($2<10) print $1}'"
changed_when: false
run_once: true
delegate_to: localhost
register: reg_current_playbook_pid
## Check for existing lock files
- name: find_existing_lock_files
ansible.builtin.find:
paths: /tmp
patterns: "*_{{ ansible_play_name }}.lock"
age: 1s
run_once: true
delegate_to: localhost
register: reg_existing_lock_files
## Check and verify existing lock files
- name: block_discovered_existing_lock_files
block:
## build fact of all lock files discovered
- name: fact_existing_lock_files
ansible.builtin.set_fact:
fact_existing_lock_files: "{{ fact_existing_lock_files | default([]) + [item.path] }}"
loop: "{{ reg_existing_lock_files.files }}"
run_once: true
delegate_to: localhost
when:
- reg_existing_lock_files.matched > 0
## Build fact of all discovered lock files
- name: fact_playbook_lock_file_dict
ansible.builtin.set_fact:
fact_playbook_lock_file_dict: "{{ fact_playbook_lock_file_dict | default([]) + [data] }}"
vars:
## E.g. lockfile => 129416_20211103094638_playbook_common_01.lock
var_pid: "{{ item.split('/')[2].split('_')[0] }}" ## extract the 1st portion = PID
var_transid: "{{ item.split('/')[2].split('_')[1] }}" ## extract 2nd portion = TransactionID
var_playbook: "{{ item.split('/')[2].split('_')[2:] | join('_') }}" ## Extract the remaining and join back together = playbook file
data:
{pid: "{{ var_pid }}", transid: "{{ var_transid }}", playbook: "{{ var_playbook }}"}
loop: "{{ fact_existing_lock_files }}"
run_once: true
delegate_to: localhost
## Check each discovered lock file
## Verify the PID is still operational
- name: shell_verify_pid_is_active
ansible.builtin.shell:
cmd: "ps -p {{ item.pid }} | awk 'NR==2{print $1}'"
loop: "{{ fact_playbook_lock_file_dict }}"
changed_when: false
delegate_to: localhost
register: reg_verify_pid_is_active
## Build fact of discovered previous playbook PIDs
- name: fact_previous_playbook_pids
ansible.builtin.set_fact:
fact_previous_playbook_pids: "{{ fact_previous_playbook_pids | default([]) + [item.stdout | int] }}"
loop: "{{ reg_verify_pid_is_active.results }}"
run_once: true
delegate_to: localhost
## Build fact is playbook already operational
## Add PIDs together
## If SUM =0 => No PIDs found (no previous playbooks running)
## If SUM != 0 => previous playbook is still operational
- name: fact_previous_playbook_operational
ansible.builtin.set_fact:
fact_previous_playbook_operational: "{{ ((fact_previous_playbook_pids | sum) | int) != 0 }}"
when:
- reg_existing_lock_files.matched > 0
- reg_current_playbook_pid.stdout is defined
## Continue with playbook, as no previous instances running
- name: block_continue_playbook_operations
block:
## Cleanup legacy lock files, as the PIDs are not operational
- name: stat_cleanup_legacy_lock_files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ fact_existing_lock_files }}"
run_once: true
delegate_to: localhost
when: fact_existing_lock_files | length >= 1
## Create lock file for current playbook
- name: stat_create_playbook_lock_file
ansible.builtin.file:
path: "/tmp/{{ var_playbook_lock_file }}"
state: touch
mode: '0644'
vars:
var_playbook_lock_file: "{{ reg_current_playbook_pid.stdout }}_{{ reg_transactionID.msg }}_{{ ansible_play_name }}.lock"
run_once: true
delegate_to: localhost
when:
- reg_current_playbook_pid.stdout is defined
## Fail & exit playbook, as previous playbook is still operational
- name: block_playbook_already_operational
block:
- name: fail
fail:
msg: 'Playbook "{{ ansible_play_name }}" is already operational! This playbook will now exit without any modifications!!!'
run_once: true
delegate_to: localhost
when: (fact_previous_playbook_operational is true) or
(reg_current_playbook_pid.stdout is not defined)
...
Real scenario is,
I have n hosts in a inventory group and playbook has to run a specific *command for the specific inventory hostname(done with ansible when condition statement), but whenever the condition met and I need to register a variable for the above *command result.
so this variable creation should be done dynamically and these created variable should be appended into a list and then at end of the same playbook by passing the list to a loop I have to check the job async_status.
So could some one help me here?
tasks:
-name:
command:
when: invenory_hostname == x
async: 360
poll:0
regsiter: "here dynamic variable"
-name:
command:
when: invenory_hostname == x
async: 360
poll:0
regsiter: "here dynamic variable"
-name:
command:
when: invenory_hostname == x
async: 360
poll:0
regsiter: "here dynamic variable" #his will continue based on the requirments
-name: collect the job ids
async_status:
jid:{item}
with_items:"list which has all the dynamically registered variables"
If you can write this as a loop instead of a series of independent tasks this becomes much easier. E.g:
tasks:
- command: "{{ item }}"
register: results
loop:
- "command1 ..."
- "command2 ..."
- name: show command output
debug:
msg: "{{ item.stdout }}"
loop: "{{ results.results }}"
The documentation on "Registering variables with a loop" discusses what the structure of results would look like after this task executes.
If you really need to write independent tasks instead, you could use the
vars lookup to find the results from all the tasks like this:
tasks:
- name: task 1
command: echo task1
register: task_result_1
- name: task 2
command: echo task2
register: task_result_2
- name: task 3
command: echo task3
register: task_result_3
- name: show results
debug:
msg: "{{ item }}"
loop: "{{ q('vars', *q('varnames', '^task_result_')) }}"
loop_control:
label: "{{ item.cmd }}"
You've updated the question to show that you're using async tasks, so
that changes things a bit. In this example, we use an until loop
that waits for each job to complete before checking the status of the
next job. The gather results task won't exit until all the async
tasks have completed.
Here's the solution using a loop:
- hosts: localhost
gather_facts: false
tasks:
- name: run tasks
command: "{{ item }}"
async: 360
poll: 0
register: task_results
loop:
- sleep 1
- sleep 5
- sleep 10
- name: gather results
async_status:
jid: "{{ item.ansible_job_id }}"
register: status
until: status.finished
loop: "{{ task_results.results }}"
- debug:
var: status
And the same thing using individual tasks:
- hosts: localhost
gather_facts: false
tasks:
- name: task 1
command: sleep 1
async: 360
poll: 0
register: task_result_1
- name: task 2
command: sleep 5
async: 360
poll: 0
register: task_result_2
- name: task 3
command: sleep 10
async: 360
poll: 0
register: task_result_3
- name: gather results
async_status:
jid: "{{ item.ansible_job_id }}"
register: status
until: status.finished
loop: "{{ q('vars', *q('varnames', '^task_result_')) }}"
- debug:
var: status
I am running python script using ansible.
Here is my playbook -
- name: Run python script
command: python Test.py -StartDate 2020-10-01T00:00:00 -EndDate 2020-11-05T00:00:00
register: result
- debug: msg="{{result.stdout}}"
I want this playbook to use EndDate as todays date when I run script. How can I use latest date and time in same format I have written every time I run script without having to change manually every day?
Q: "How can I use the latest date and time ...?"
A: Use Ansible facts variable ansible_date_time. For example, given the script below
shell> cat test.sh
#!/bin/sh
echo $1 $2
The playbook
shell> cat playbook.yml
- hosts: localhost
tasks:
- name: Run script
command: "{{ playbook_dir }}/test.sh
2020-10-01T00:00:00
{{ ansible_date_time.date }}T00:00:00"
register: result
- debug:
var: result.stdout
gives (abridged)
shell> ansible-playbook playbook.yml
result.stdout: 2020-10-01T00:00:00 2020-11-07T00:00:00
Notes
"gather_facts: true" (default) is needed to collect Ansible facts.
The values of date and time will be collected on a remote host when a playbook starts.
If needed there are other attributes in the dictionary
ansible_date_time:
date: '2020-11-07'
day: '07'
epoch: '1604779525'
hour: '21'
iso8601: '2020-11-07T20:05:25Z'
iso8601_basic: 20201107T210525040700
iso8601_basic_short: 20201107T210525
iso8601_micro: '2020-11-07T20:05:25.040817Z'
minute: '05'
month: '11'
second: '25'
time: '21:05:25'
tz: CET
tz_offset: '+0100'
weekday: Saturday
weekday_number: '6'
weeknumber: '44'
year: '2020'
The variable ansible_date_time will not be updated automatically when a playbook runs. For example
- debug:
var: ansible_date_time.iso8601_micro
- debug:
var: ansible_date_time.iso8601_micro
- debug:
var: ansible_date_time.iso8601_micro
give (abridged)
ansible_date_time.iso8601_micro: '2020-11-07T20:16:09.481237Z'
ansible_date_time.iso8601_micro: '2020-11-07T20:16:09.481237Z'
ansible_date_time.iso8601_micro: '2020-11-07T20:16:09.481237Z'
Run module setup to update Ansible facts including the variable ansible_date_time. For example
- debug:
var: ansible_date_time.iso8601_micro
- setup:
- debug:
var: ansible_date_time.iso8601_micro
- setup:
- debug:
var: ansible_date_time.iso8601_micro
give (abridged)
ansible_date_time.iso8601_micro: '2020-11-07T20:16:09.481237Z'
ansible_date_time.iso8601_micro: '2020-11-07T20:16:10.759533Z'
ansible_date_time.iso8601_micro: '2020-11-07T20:16:11.475873Z'
Frequently running setup to update the date and time only is overkill. In this case, consider running command and register result instead.
assuming the T00:00:00 is always fixed, you could declare a variable using the lookup plugin, see an example below the exec_date variable and the modified command task:
---
- hosts: localhost
gather_facts: false
vars:
exec_date: "{{ lookup('pipe', 'date +%Y-%m-%d') }}T00:00:00"
tasks:
- name: print
debug: var=exec_date
- name: Run python script
command: "python Test.py -StartDate 2020-10-01T00:00:00 -EndDate {{ exec_date }}"
register: result
- debug: msg="{{result.stdout}}"
If you want to pass the current time too instead of a fixed T00:00:00, you could use the below:
vars:
exec_date: "{{ lookup('pipe', 'date +%Y-%m-%dT%H:%M:%S') }}"
cheers
I am dynamically creating the hostgroup by site and it did well with below recipe.
- name: "generate batches from batch_processor"
batch_processor:
inventory: "{{ inventory_url }}"
ignore_applist: "{{ ignore_applist | default('None') }}"
ignore_podlist: "{{ ignore_podlist | default('None') }}"
register: batch_output
- name: "Creating batches by site"
include: batch_formatter.yml hostkey="{{ 'batch_' + item.key }}" hostlist="{{ item.value | list | join(',') }}"
with_dict: "{{ batch_output.result['assignments'] }}"
In my playbook, i have like this
- hosts: batch_*
serial: 10
gather_facts: False
tasks:
- include: roles/deployment/tasks/dotask.yml
I initially had strategy: free but in some reason it didn't pickup parallel. Currently i am using all batch hosts 10 at time to deploy.
I am thinking of below items
batch_site1 - 10 in parallel
batch_site2 - 10 in parallel
batch_site3 - 10 in parallel
But in the playbook, i don't want to specify the hostgroup execution by site as they are dynamic. Sometime, we will have siteX and sometime it wont be there. Please suggest the best approach.
- hosts: batch_*
gather_facts: False
strategy: free
tasks:
- include: roles/deployment/tasks/dotask.yml
when: "'batch_site1' not in group_names"
async: 600
poll: 0
register: job1
- name: Wait for asynchronous job to end
async_status:
jid: '{{ job1.ansible_job_id }}'
register: job_result1
until: job_result1.finished
retries: 30
- include: roles/deployment/tasks/dotask.yml
when: "'batch_site2' not in group_names"
register: job2
async: 600
poll: 0
- name: Wait for asynchronous job to end
async_status:
jid: '{{ job2.ansible_job_id }}'
register: job_result2
until: job_result2.finished
retries: 30
- include: roles/deployment/tasks/dotask.yml
when: "'batch_site3' not in group_names"
register: job3
async: 600
poll: 0
- name: Wait for asynchronous job to end
async_status:
jid: '{{ job3.ansible_job_id }}'
register: job_result3
until: job_result3.finished
retries: 30
This config slightly working on my purpose, however i couldn't pull the async_status to show better results in screen.
{"msg": "The task includes an option with an undefined variable. The error was: 'job1' is undefined\n\nThe error appears to have been in '/../../../../play.yml': line 12, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Wait for asynchronous job to end\n ^ here\n"}
not sure why ?
In order to generate random cron executions for each host, I'd like to randomize the hour when it is going to be executed, to spread the load and avoid all hosts to run hard tasks at the same time.
So, if it would be possible to generate a different hour and minute for each host, it would spread it automatically.
Example:
cron:
name: "Conquer the world"
minute: "{{ ansible.hostname | str2num(0, 59) }}"
hour: "{{ ansible.hostname | str2num(4, 6) }}"
job: "conquer_the_world.sh"
Wanted function is what I named str2num, that should generate the same number for the same host in a deterministic way, and may be different for each host.
Is already there any solution for this or should I create a custom filter for this?
ANSWER
I finally found the answer by myself, thanks to the blog post: https://ansibledaily.com/idempotent-random-number/:
cron:
name: "Conquer the world"
minute: "{{ (59 |random(seed=ansible_hostname)) }}"
hour: "{{ (2 |random(seed=ansible_hostname)) + 4 }}"
job: "conquer_the_world.sh"
In general: {{ ( (MAX - MIN) | random(seed=ansible_hostname)) + MIN }}
{{ ( inventory_hostname | hash('sha256') | int(0, 16) ) % 59 }}
Use Random Number Filter. There is a cron example
"{{ 60|random }} * * * * root /script/from/cron"
# => '21 * * * * root /script/from/cron'
An option would be to create a "schedule" and then use it. The attribute seed makes the task idempotent. For example the play below
- hosts: test_jails
tasks:
- set_fact:
mcs: "{{ mcs|default([])|
combine({item :{'my_cron_hour':(24|random(seed=item)),
'my_cron_minute':(60|random(seed=item))}}) }}"
loop: "{{ play_hosts }}"
run_once: true
- debug:
var: my_cron_schedule
run_once: true
gives
mcs:
test_11:
my_cron_hour: 2
my_cron_minute: 4
test_12:
my_cron_hour: 5
my_cron_minute: 11
test_13:
my_cron_hour: 13
my_cron_minute: 27