Run Maintenance Commands and Handle Failures

AnsibleBeginner
Practice Now

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.