Back to the main page

Test Ansible roles with OCI compute

Intro

These are examples to test an Ansible role on an OCI compute instance. The idea is to create an OCI compute, test the role, and delete that OCI compute. This can be integrated into CI/CD pipeline.

The assumption is that the test OCI compartment is empty, although it won't hurt if there are some other computes there:
- The play creates a new compute.
- The role will be tested on all computes in the compartment (using oci dynamic inventory)
- The play at the end deletes only the compute created in step one.

The folder structure of roles and tests is like:
$ tree my-work -L 2
my-work
|-- README.md
|-- roles
|   |-- sudo 
|   |-- logging
|   |-- cron
|   |-- your-role-example
|-- test
|   |-- create-compute.yml
|   |-- delete-compute.yml
|   |-- inventory-localhost
|   |-- inventory.oci.yml
|   |-- readme
|   |-- test-role.py
|   |-- test-role.sh
|   |-- test-role.yml
|   |-- vars
|       |-- main.yml

OCI dynamic inventory

The file name is inventory.oci.yml. It reads:
---
# Help
# https://github.com/oracle/oci-ansible-collection/blob/master/plugins/inventory/oci.py
# https://oci-ansible-collection.readthedocs.io/en/latest/collections/oracle/oci/oci_inventory.html
# https://docs.oracle.com/en/learn/olae-dyninv/#introduction

# Oracle dynamic inventory plugin comes with OCI Ansible Collection
plugin: oracle.oci.oci

# ------------------------
# OCI Config information
# ------------------------
# config_file: /my-home/.oci/config
# config_profile: PHX

# ------------------------
# Specify regio, do not use conf file
# ------------------------
# One region
regions: ap-phoenix-1

# Multiple regions, list type
# regions:
# - us-ashburn-1
# - us-phoenix-1

# Enable threads to speedup lookup
enable_parallel_processing: true # default

# -----------------------
# How to display hosts
# -----------------------
hostname_format: "fqdn"
# hostname_format: "display_name"
# hostname_format: "private_ip"

# ---------------------
# Compartment
# ---------------------
compartments:
- compartment_ocid: ocid-your-test-compartment

# fetch_compute_hosts: true # default
fetch_hosts_from_subcompartments: false
...
To get all computes in compartment using this dynamic inventory, run: ansible-inventory -i inventory.oci.yml --graph

Playbooks

Variables

The folder vars/main.yml has vars to create compute, it reads:
---
# example region
profile: "MUMBAI"
ad: "DSdu:AP-MUMBAI-1-AD-1"

# labops.zd
compartment: "ocid1-your-compartment"

shape: "VM.Standard.E4.Flex"

# Oracle-Linux-7.9-2023.09.26-0
image_ol79: "ocid1.image.oc1.ap-mumbai-1.xxxxxxx"

# common.sub
subnet: "ocid1.subnet.oc1.ap-mumbai-1.xxxxx"

# newly created OCI compute comes with oci user
ssh_key: "ssh-rsa key-for-opc-user"
...

Create compute play

For this play we use inventory-localhost file that reads only "localhost". The play file is create-compute.yml
---
- name: Create compute play
  connection: local
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Load vars
      ansible.builtin.include_vars: "vars/main.yml"

    - name: Create temp OCI computes OL7.9
      oracle.oci.oci_compute_instance:
        compartment_id: "{{ compartment }}"
        config_profile_name: "{{ profile }}"
        availability_domain: "{{ ad }}"
        shape: "{{ shape }}"
        shape_config:
          memory_in_gbs: "8"
          ocpus: "1"
        create_vnic_details:
          subnet_id: "{{ subnet }}"
        source_details:
          source_type: image
          image_id: "{{ image_ol79 }}"
        metadata: {"ssh_authorized_keys": "{{ ssh_key }}"}
      register: compute

    - name: Show compute id
      debug:
        msg: "{{ compute.instance.id }}"

    - name: Copy compute_id var, to be used by delete play
      copy:
        content: "{{ compute.instance.id }}"
        dest: "/tmp/deleteme-compute-id"

    - name: Wait 15 sec
      ansible.builtin.pause:
        seconds: 15

    - name: Reset compute
      oracle.oci.oci_compute_instance_actions:
        action: reset
        instance_id: "{{ compute.instance.id }}"

    - name: Collect hosts from dynamic inventory
      ansible.builtin.command:
        "ansible-inventory -i inventory.oci.yml --graph"
      register: dynamic_inventory

    - name: Show inventory
      ansible.builtin.debug:
        msg:
          - "{{ dynamic_inventory.stdout_lines }}"
...

Test role play

The file test-play.yml reads,
---
- name: Test play
  hosts: all
  become: true
  tasks:
    - name: Test role {{ role }}
      ansible.builtin.include_role:
        name: "../roles/{{ role }}"
...

Delete compute play

For this play (same as for create compute play) we use inventory-localhost file that reads only "localhost". The play is delete-compute.yml
---
- name: Delete compute play
  connection: local
  hosts: localhost
  gather_facts: false
  tasks:

    - name: Get exported compute_id var
      ansible.builtin.slurp:
        src: "/tmp/deleteme-compute-id"
      register: compute_id

    - name: Show compute id
      ansible.builtin.debug:
        msg: "Delete compute ID: {{ compute_id['content'] | b64decode }}"

    - name: Delete OCI compute
      oracle.oci.oci_compute_instance:
        instance_id: "{{ compute_id['content'] | b64decode }}"
        state: absent

    - name: Collect hosts from dynamic inventory
      ansible.builtin.command:
        "ansible-inventory -i inventory.oci.yml --graph"
      register: dynamic_inventory

    - name: Show inventory
      ansible.builtin.debug:
        msg:
          - "{{ dynamic_inventory.stdout_lines }}"

    - name: Delete exported compute_id var
      ansible.builtin.file:
        path: "/tmp/deleteme-compute-id"
        state: absent
...

Bash wrapper script

The wrapper is test-role.sh
#/bin/bash

err() {
  echo "ERROR: $* "
  exit 1
}

verbosity=""
role=""

usage() {
  echo "Usage: $0 -r  [-v] [-vv] [-vvv] [-h]"
  echo " -r    Specify the role (mandatory)"
  echo " -v    Set verbosity level (up to 3 times)"
  echo " -h    Help"
  exit 1
}

while getopts ":r:vh" opt; do
  case "${opt}" in
    r)
      role="${OPTARG}"
      ;;
    v)
      verbosity+="v"
      ;;
    h)
      usage
      ;;
    *)
      err "Wrong option: ${OPTARG}, use -h for help."
      ;;
  esac
done

# Check for role
if [ -z "${role}" ]; then
  err "Role not specified. Use -h for help."
fi

echo -e "\n Run playbooks with verbosity "${verbosity}" \n"
#
echo -e "Run play to create OCI compute \n"

if [ -z "${verbosity}" ]; then
    ansible-playbook -i inventory-localhost create-compute.yml || \
    err "Failed to create OCI compute"
else
    ansible-playbook "-${verbosity}" -i inventory-localhost create-compute.yml || \
    err "Failed to create OCI compute"
fi

#
echo -e "Run play to test role "${role}" with diff mode \n"

if [ -z "${verbosity}" ]; then
    ansible-playbook --diff -i inventory.oci.yml test-role.yml \
    -e role="${role}" \
    -u opc --private-key "/home/opc/.ssh/id_rsa_opc" --ssh-common-args "-o StrictHostKeyChecking=no" || \
    err "Failed to test role"
else
    ansible-playbook "-${verbosity}" --diff -i inventory.oci.yml test-role.yml \
    -e role="${role}" \
    -u opc --private-key "/home/.ssh/private-key" --ssh-common-args "-o StrictHostKeyChecking=no" || \
    err "Failed to test role "${role}"."
fi

#
echo -e "Run play to delete OCI compute \n"

if [ -z "${verbosity}" ]; then
    ansible-playbook -i inventory-localhost delete-compute.yml || \
    err "Failed to delete OCI compute"
else
    ansible-playbook "-${verbosity}" -i inventory-localhost delete-compute.yml || \
    err "Failed to delete OCI compute"
fi

exit 0

Usage

Specify role as mandatory, and use verbosity if needed.
Usage: test-role.sh -r  [-v] [-vv] [-vvv] [-h]
 -r    Specify the role (mandatory)
 -v    Set verbosity level (up to 3 times)
 -h    Help

Python wrapper script

#!/bin/python3
import os
import argparse
import ansible_runner

def mgmt_compute_play(playbook_path, inventory_path):
    r = ansible_runner.run(
        verbosity = 2,
        envvars = {'PATH': '/sbin:/bin:/usr/sbin:/usr/bin'},
        playbook = playbook_path,
        inventory = inventory_path
    )
    print(r.stats)

def role_test_play(playbook_path, inventory_path, role, diff, check):
    if diff and not check:
        cmdline_opt = "--diff"
    elif check and not diff:
        cmdline_opt = "--check"
    elif check and diff:
        cmdline_opt = "--check --diff"
    else:
        cmdline_opt = ""
    r =ansible_runner.run(
        verbosity = 2,
        envvars = {'PATH': '/sbin:/bin:/usr/sbin:/usr/bin'},
        playbook = playbook_path,
        inventory = inventory_path,
        extravars = {'role': role, 'ansible_ssh_user': 'opc',
                     'ansible_ssh_private_key_file': '/home/opc/.ssh/id_rsa_opc',
                     'ansible_ssh_common_args': '-o StrictHostKeyChecking=no'},
        cmdline = cmdline_opt
    )
    print(r.stats)

def main():
    parser = argparse.ArgumentParser(description="Test role on OCI compute.")
    parser.add_argument("-r", "--role", required=True, help="Role name")
    parser.add_argument("-d", "--diff", help="Diff mode", action="store_true")
    parser.add_argument("-c", "--check", help="Check mode", action="store_true")
    args = parser.parse_args()
    role = args.role
    diff = args.diff
    check = args.check

    # Absolute path to playbook, inventory
    work_dir = os.path.dirname(os.path.abspath(__file__))
    playbook_path_create_comp = os.path.join(work_dir, "create-compute.yml")
    playbook_path_delete_comp = os.path.join(work_dir, "delete-compute.yml")
    playbook_path_role_test = os.path.join(work_dir, "test-role.yml")
    inventory_localhost_path = os.path.join(work_dir, "inventory-localhost")
    dynamic_inventory_path = os.path.join(work_dir, "inventory.oci.yml")
    # Create compute
    mgmt_compute_play(playbook_path_create_comp, inventory_localhost_path)
    # Role test
    role_test_play(playbook_path_role_test, dynamic_inventory_path, role, diff, check)
    # Delete compute
    mgmt_compute_play(playbook_path_delete_comp, inventory_localhost_path)

if __name__ == "__main__":
    main()

Usage

Specify role as mandatory, and use --diff and --check if needed.
$ python3 test-role.py -h
usage: test-role.py [-h] -r ROLE [-d] [-c]

Test role on OCI compute.

options:
  -h, --help            show this help message and exit
  -r ROLE, --role ROLE  Role name
  -d, --diff            Diff mode
  -c, --check           Check mode


Back to the main page