Introduction
Existing maintenance programs need careful integration with declarative automation. You will distinguish command, shell and script, pass a spaced path safely, classify read-only results, and guard a real script output. A pipeline and delegated controller receipts will make execution location and failure propagation observable. You will also induce and recover a real missing-input failure.
Run a Direct Command with Safe Arguments
In this step, you will inspect managed input with command, preserve a path containing a space, and classify the read-only operation correctly.
Prefer declarative modules such as copy and file for state management. Command, shell and script are useful when integrating an existing executable or maintenance program. command runs a managed executable without a shell, so pipes and redirection are not interpreted. Its argv form keeps each argument separate, including a path containing spaces.
The prepared targets each have /home/automation/maintenance input.txt containing one line of approved data, and a maintenance-ready prerequisite. The controller provides an existing script under scripts/ for later use. Preserve those inputs.
Enter the controller project:
cd /home/labex/project
Create a playbook using argv. The path containing a space is one YAML list entry. register captures the executable's result, including stdout and its numeric return code rc. Zero normally means success; a nonzero command return code normally fails the task.
cat > maintenance.yml <<'YAML'
- name: Inspect maintenance inputs safely
hosts: all
gather_facts: false
tasks:
- name: Read the managed input with a space-safe argument list
ansible.builtin.command:
argv:
- /usr/bin/cat
- /home/automation/maintenance input.txt
register: input_result
changed_when: false
- name: Retain the inspected input in a managed record
ansible.builtin.copy:
dest: /home/automation/command-result.txt
content: "{{ input_result.stdout }}\n"
mode: "0640"
YAML
The cat command is read-only, so changed_when false classifies its result as unchanged. This setting changes reporting, not what a command actually does; it must not be used to hide changes from a mutating program. The copy task consumes the returned stdout and writes a real managed record with an explicit final newline.
Check and run the playbook:
ansible-playbook maintenance.yml --syntax-check
ansible-playbook maintenance.yml
Both read tasks should report ok, while the first record copy changes each target. Inspect the actual records:
ansible all -m ansible.builtin.command -a 'cat /home/automation/command-result.txt'
Each should contain Approved maintenance data. In this core version, command can expand environment-variable arguments through Python even though it does not invoke a shell. Use expand_argument_vars false when literal dollar-prefixed arguments are required; it is unnecessary for this fixed path.
Transfer a Controller Script and Guard Its Real Output
In this step, you will invoke the supplied controller-side maintenance script on both targets and avoid unnecessary repeat execution.
Inspect the provided program before running it:
cat scripts/host-maintenance.sh
It checks the target's maintenance-ready input before creating any output. Missing readiness prints an error and exits 42. Otherwise it writes a protected report containing the actual target hostname and its label argument. Its small shell implementation is an existing integration input; use declarative modules for ordinary file configuration.
The script module transfers a controller source to the target and executes it there. This differs from command, whose executable must already exist on the managed node. The quoted label below remains one argument with a space. The creates path is checked on the managed target: when that report already exists, the script is skipped.
Append the script task:
cat >> maintenance.yml <<'YAML'
- name: Run the supplied controller script once per target report
ansible.builtin.script:
cmd: /home/labex/project/scripts/host-maintenance.sh 'Maintenance checked'
creates: /home/automation/maintenance-run/report.txt
register: maintenance_result
failed_when: maintenance_result.rc is defined and maintenance_result.rc != 0
YAML
The registered result's rc controls failed_when. The is defined guard accounts for a result that does not include an execution return code. Do not use ignore_errors or failed_when false to turn a real script failure into apparent success.
Run the extended playbook:
ansible-playbook maintenance.yml
Both targets should create reports. Read the real outputs:
ansible all -m ansible.builtin.command -a 'cat /home/automation/maintenance-run/report.txt'
Each should contain its own host name and label=Maintenance checked. Repeat the same playbook:
ansible-playbook maintenance.yml
The script should be skipped because its target report exists, and both recaps should report changed=0 with no failed/unreachable targets. Creates is an existence guard, not a content reconciliation engine: it does not repair a corrupt existing report or automatically regenerate it when script inputs change.
Propagate Failures and Delegate Controller Receipts
In this step, you will use shell only for a real pipeline, store distinct controller receipts, and exercise a visible script failure without suppressing it.
A pipe connects one command's output to another command's input and therefore needs shell. Bash pipefail makes a failing command in a pipeline produce a failure instead of allowing a successful final wc command to hide it. The pipeline below only counts input lines; changed_when false is appropriate because it reads without changing target state. failed_when also rejects an unexpected line count.
Create a controller receipt directory. Each delegated copy will use the original inventory name in a separate filename, preventing hosts from overwriting one shared receipt:
mkdir -p receipts
Append the pipeline and receipt tasks. delegate_to localhost runs only the receipt task on the controller; inventory_hostname still identifies the original target. become false keeps controller file creation under the ordinary labex account.
cat >> maintenance.yml <<'YAML'
- name: Count managed input lines with explicit pipeline failure propagation
ansible.builtin.shell: |
set -o pipefail
cat '/home/automation/maintenance input.txt' | wc -l
args:
executable: /bin/bash
register: line_result
changed_when: false
failed_when: line_result.rc != 0 or (line_result.stdout | trim) != '1'
- name: Store a per-target receipt on the controller
ansible.builtin.copy:
dest: "/home/labex/project/receipts/{{ inventory_hostname }}.txt"
content: |
host={{ inventory_hostname }}
input_lines={{ line_result.stdout | trim }}
mode: "0640"
delegate_to: localhost
become: false
YAML
Run the complete maintenance workflow:
ansible-playbook maintenance.yml
Inspect both controller receipts. Each must identify its own original host and input_lines=1:
cat receipts/web1.txt receipts/web2.txt
Now exercise the script's real failure branch on web1. Remove its guarded report and readiness input with the file module; these are intentional temporary changes inside the training node:
ansible web1 -m ansible.builtin.file -a 'path=/home/automation/maintenance-run/report.txt state=absent'
ansible web1 -m ansible.builtin.file -a 'path=/home/automation/maintenance-ready state=absent'
The next run should fail the script task on web1 with rc 42 and the missing-input message. || allows the Terminal workflow to continue after this expected nonzero playbook exit; it does not change Ansible's failure classification:
ansible-playbook maintenance.yml || printf 'Expected missing-readiness failure\n'
Inspect the report path; web1 must not have a fabricated success report after the failed script:
ansible web1 -m ansible.builtin.stat -a 'path=/home/automation/maintenance-run/report.txt'
Restore the readiness input from an exact controller source and rerun the complete workflow:
printf 'Ready\n' > ready.txt
ansible web1 -m ansible.builtin.copy -a 'src=/home/labex/project/ready.txt dest=/home/automation/maintenance-ready mode=0644'
ansible-playbook maintenance.yml
Web1 should regenerate its report successfully. The final repeat should have no changes, unreachable targets or failures:
ansible-playbook maintenance.yml
Read-only inspections still execute, the guarded script skips existing reports, and each delegated receipt remains on the controller. This preserves real error signals while keeping successful repeat execution stable.
Summary
You used argv for direct managed execution, script for controller-to-target program transfer, and shell only for an actual pipeline with explicit failure propagation. Read-only change reporting, a real creates guard and non-suppressed exit codes kept results meaningful. Delegated per-host receipts demonstrated controller execution, and the missing-input exercise ended in a restored, stable workflow.


