Ansible: A Shell Execution Shortcut

Ansible: A Shell Execution Shortcut
Photo by Romain Virtuel / Unsplash

Here is a small trick that could make your playbooks, if not more readable, at least shorter. The trick is useful if you use Shell to execute multiple commands on targets.

A short description to set some perspective: I have multiple environments with numerous Oracle Fusion Middleware Installations. Most hosts are uniform and have a single product installation folder. Yet, for various reasons, some hosts have more than one Oracle Home with different names. It means if I need a patch list for each home on each host, my playbook should:

  • Find all .*/Opatch/opatch files on the target
  • Collect the results into a new variable
  • Loop through the results in the variable
  • Run the command inside the loop.

In Ansible, it would look close to this snippet:

# Find all Oracle Homes on the host
- find:
    paths:
      - "/opt/oracle/product/"
    patterns:
      - ".*/OPatch/opatch$"
    recurse: yes
    use_regex: yes
  register: op_list

#Collect patch lists for each home
- shell:
    cmd: |
      {{ item.path }} lspatches 
  loop: "{{ op_list.files }}"
  loop_control:
      label: "{{ item.path }}"
  register: patch_report    
  when: not op_list.failed|bool
  
# Report All Patches for Target
- debug: 
    var: patch_report

Classic Generic Solution

The code above is bulletproof and should find any Oracle Home under a set of patches. But for cases when options are well-known and relatively small, you could do better using the shell's attribute removes. The code below delivers the same result for possible Oracle Home installations.

---
- hosts: all
  vars:
    opatch:
       - "/var/opt/product/fmw_home/OPatch/opatch"
       - "/var/opt/product/ohs_home/OPatch/opatch"
       - "/var/opt/product/adf_home/OPatch/opatch"
  tasks:
# Run Opatch Command for Existing Homes  
    - shell: 
        cmd: |
          {{ item }} lspatches
      args:
        removes: "{{ item }}"
      register: patch_report
      loop: "{{ opatch }}"
      
# Report All Patches for Target
    - debug: 
        var: patch_report      
      

Run only what you have

The trick is that we already know all potential and valid locations. The shell task loops through all values and runs only existing files because we promised that our command would remove them. Of course, we are not going to do such a thing. So, the shell task selects a new file name, finds out it doesn't exist, and skips the step, assuming the task has been completed already.