Introduction
Reliable scheduling needs more evidence than a successful play recap. You will deploy a protected report program for an ordinary user, manage a stable named cron entry, observe natural executions, update its phase and remove a separate temporary entry. The final check will observe a new scheduler run while preserving the unrelated existing job.
Deploy an Ordinary-User Report and a Named Schedule
In this step, you will deploy a supplied report program and schedule it for the automation user on both managed hosts.
Cron is installed and its daemon is already running as a prerequisite. The nodes do not run systemd. Each automation crontab also contains an unrelated keep-existing-report entry; preserve it. The cron module manages a stable named entry instead of replacing the user's whole crontab.
Enter the project and inspect the supplied controller program:
cd /home/labex/project
cat files/host-report.sh
The program appends a UTC epoch timestamp, actual hostname and phase argument to a protected managed log. Umask 027 gives new log files mode 0640. Leave the program unchanged and let cron run it naturally; manually invoking it would not demonstrate the schedule.
Define the phase as reusable input:
cat > group_vars/all.yml <<'YAML'
report_phase: initial
YAML
Create the playbook. A cron job has minute, hour, day-of-month, month and weekday fields; an asterisk in each field runs once each minute. The name creates an Ansible marker used for later updates, and user automation selects that user's crontab. Become allows managing this named user's schedule and explicit file ownership; it does not make the scheduled program run as root.
cat > reporting.yml <<'YAML'
- name: Manage a scheduled ordinary-user host report
hosts: all
gather_facts: false
become: true
tasks:
- name: Prepare the ordinary user's reporting directory
ansible.builtin.file:
path: /home/automation/reporting
state: directory
owner: automation
group: automation
mode: "0750"
- name: Deploy the supplied report program
ansible.builtin.copy:
src: /home/labex/project/files/host-report.sh
dest: /home/automation/reporting/host-report.sh
owner: automation
group: automation
mode: "0750"
- name: Keep the named report schedule present
ansible.builtin.cron:
name: ansible-host-report
user: automation
minute: "*"
hour: "*"
day: "*"
month: "*"
weekday: "*"
job: "/home/automation/reporting/host-report.sh {{ report_phase }}"
YAML
Validate and apply:
ansible-playbook reporting.yml --syntax-check
ansible-playbook reporting.yml
Inspect the actual automation crontabs. The command runs with become so it can name the queried account explicitly:
ansible all -b -m ansible.builtin.command -a 'crontab -u automation -l'
Each should contain one ansible-host-report marker and the every-minute command, plus the unchanged keep-existing-report entry. A crontab line alone does not prove execution.
Wait for the first natural scheduler tick. timeout bounds the wait at 90 seconds; bash's until loop checks for a nonempty log every two seconds. The command only reads state while cron creates the log:
ansible all -m ansible.builtin.command -a "timeout 90 bash -c 'until test -s /home/automation/reporting/host-report.log; do sleep 2; done'"
ansible all -m ansible.builtin.command -a 'cat /home/automation/reporting/host-report.log'
Each should have a record like time= host=web1 phase=initial, with the corresponding actual host name on web2. The timestamps vary and should align with natural minute scheduling. If the bounded wait fails, inspect the schedule and running daemon rather than writing a success log yourself.
Update One Named Entry and Observe the New Phase
In this step, you will change the job's input while preserving its stable identity and the unrelated schedule.
Update the saved phase value. The cron task will render the new command while retaining the same name:
cat > group_vars/all.yml <<'YAML'
report_phase: verified
YAML
ansible-playbook reporting.yml
Inspect both crontabs again:
ansible all -b -m ansible.builtin.command -a 'crontab -u automation -l'
There should still be exactly one ansible-host-report entry per host, now ending in verified. Reusing its name updates the existing entry instead of accumulating a second schedule. Keep-existing-report must remain unchanged.
Wait for a new natural run using the updated phase. grep -q checks for a matching log record without printing it; timeout and the until loop keep this wait bounded:
ansible all -m ansible.builtin.command -a "timeout 90 bash -c 'until grep -q phase=verified /home/automation/reporting/host-report.log; do sleep 2; done'"
ansible all -m ansible.builtin.command -a 'cat /home/automation/reporting/host-report.log'
Both should now contain a new phase=verified record from the actual scheduler. The older initial records are useful history and should remain. Updating a crontab command does not retroactively change old log lines.
Remove Only a Named Temporary Job and Keep Reporting Stable
In this step, you will remove a temporary schedule by its stable name and verify that the intended ordinary-user report keeps executing.
Create a harmless named temporary entry using an ad hoc cron task. It schedules /usr/bin/true at midnight and provides an actual entry to remove:
ansible all -b -m ansible.builtin.cron -a 'name=ansible-temporary-report user=automation minute=0 hour=0 job=/usr/bin/true'
ansible all -b -m ansible.builtin.command -a 'crontab -u automation -l'
Write a small cleanup playbook. State absent and the matching name/user remove only that managed entry. Do not empty or replace the complete crontab:
cat > cleanup.yml <<'YAML'
- name: Remove only the obsolete named schedule
hosts: all
gather_facts: false
become: true
tasks:
- name: Remove the obsolete report entry by its stable name
ansible.builtin.cron:
name: ansible-temporary-report
user: automation
state: absent
YAML
ansible-playbook cleanup.yml
ansible all -b -m ansible.builtin.command -a 'crontab -u automation -l'
The temporary marker and command should be gone. Both ansible-host-report with phase verified and keep-existing-report should remain.
Repeat both declarations:
ansible-playbook reporting.yml
ansible-playbook cleanup.yml
Both playbooks should report changed=0, unreachable=0 and failed=0 for both hosts. Cron may continue appending log records independently; those natural runtime records do not make the declarative deployment non-idempotent.
The final check inspects the named ordinary-user jobs, exact deployed program and protected logs, then waits for another natural scheduler tick on both hosts. It can take about a minute. Leave the report schedule and daemon running so this observation can establish ongoing execution rather than a manually prepared success file.
Summary
You managed stable named cron entries for the intended ordinary user, observed actual scheduled execution, updated command input without duplicating the job and removed only a temporary entry. Explicit program/log permissions and preserved unrelated schedules kept the scope clear. Deployment repeated without changes while the report continued running naturally.



