Back to the main page

Test Ansible roles with OCI Resource manager (terraform)

Intro

These are examples to test an Ansible role on an OCI compute instance.
The idea is to use Resource Manager (terraform) service to create Stack (terraform conf) with associated resources (compute), test the role, destroy stack associated resources and delete the stack.
This can be integrated into CI/CD pipeline.

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-rm-stack-compute.yml
|   |-- delete-compute-rm-stack.yml
|   |-- inventory-localhost
|   |-- inventory.oci.yml
|   |-- readme
|   |-- test-role.py
|   |-- test-role.yml
|   |-- vars
|   |   |-- main.yml
|   |-- tf-files
|       |-- core.tf
|       |-- core-vars.tf
|       |-- provider.tf
|       |-- provider-vars.tf

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

Terraform conf

File provider-vars.tf (vars for terraform provider, API for OCI)
variable "tenancy" {
  default     = "ocid1.tenancy.oc1.."
  description = "your tenancy"
}
variable "region" {
  default = "xx-xxx-1"
  description = "Your Region"
}
variable "user" {
  default     = "ocid1.user.oc1.."
  description = "your account"
  sensitive = true
}
variable "fingerprint" {
  default     = "xx:xx:xx:xx:xx"
  description = "your fingerprint"
  sensitive = true
}
variable "private_key" {
  default     = "/home/some-key.pem"
  description = "your private key"
  sensitive = true
}
File provider.tf
provider "oci" {
  tenancy_ocid     = var.tenancy
  region           = var.region
  user_ocid        = var.user
  fingerprint      = var.fingerprint
  private_key_path = var.private_key
}
File core-vars.tf
variable "ad" {
  default = "DSdu:xx-xxxx-1-AD-1"
  description = "your region"
}
variable "compartment" {
  default = "ocid1.compartment.oc1."
  description = "your compartment"
}
variable "shape" {
  default = "VM.Standard.E4.Flex"
  description = "Core shape"
}
variable "image_ol8" {
  default = "ocid1.image.oc1."
  description = "OL 8 image"
}
variable "subnet" {
  default = "ocid1.subnet.oc1."
  description = "your subet"
}
variable "ssh_key" {
  default = "ssh-rsa AAAAxxxxx-some-ssh-public-key"
  description = "ssh public key for default opc user"
}
File core.tf
resource "oci_core_instance" "play_test_compute" {
  count = 1  # you can create more computes
  agent_config {
    are_all_plugins_disabled = "true"
    is_management_disabled = "true"
    is_monitoring_disabled = "true"
  }
  compartment_id = var.compartment
  availability_domain = var.ad
  create_vnic_details {
    subnet_id = var.subnet
    assign_public_ip = "false"
  }
  shape = var.shape
  shape_config {
    # baseline_ocpu_utilization = "BASELINE_1_1"
    memory_in_gbs = "8"
    ocpus = "1"
  }
  source_details {
    source_type = "image"
    source_id = var.image_ol8
  }
  metadata = {
    ssh_authorized_keys  = var.ssh_key
  }
}

Playbooks

Variables

File vars/main.yml (Ansible needs this var to upload terraform files to defined compartment)
---
# your compartment name or description
compartment: "ocid1.compartment.oc1."
...

Create RM stack, plan and apply it to create associated resources (compute)

File create-rm-stack-compute.yml
---
- name: Create RM stack and resources
  connection: local
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Load vars
      ansible.builtin.include_vars: "vars/main.yml"

    - name: Zip archive terraform files
      community.general.archive:
        path: tf-files/*.tf
        dest: /tmp/core-stack.zip
        format: zip
      register: stack_zip

    - name: Read the contents of the zip file
      ansible.builtin.set_fact:
        zip_content: "{{ lookup('file', \"{{ stack_zip.dest }}\") }}"

    - name: Create stack
      oracle.oci.oci_resource_manager_stack:
        state: present
        compartment_id: "{{ compartment }}"
        description: "RM stack for OL compute to test Ansible role"
        config_source:
          config_source_type: ZIP_UPLOAD
          zip_file_base64_encoded: "{{ zip_content | b64encode }}"
      register: _stack

    - name: Show stack ID
      ansible.builtin.debug:
        msg:
          - "Stack ID: {{ _stack.stack.id }}"

    - name: Save RM stack ID, to be used by delete play
      ansible.builtin.copy:
        content: "{{ _stack.stack.id }}"
        dest: "/tmp/deleteme-stack-id"

    - name: Plan Stack
      oracle.oci.oci_resource_manager_job:
        stack_id: "{{ _stack.stack.id }}"
        job_operation_details: "{'operation': 'PLAN'}"
      register: _plan

    - name: List stack plan info
      ansible.builtin.debug:
        msg:
          - "Plan Stack: {{ _plan }}"

    - name: Apply Stack
      oracle.oci.oci_resource_manager_job:
        stack_id: "{{ _stack.stack.id }}"
        job_operation_details:
          "{
            'operation': 'APPLY',
            'execution_plan_strategy': 'FROM_PLAN_JOB_ID',
            'execution_plan_job_id': '{{ _plan.job.id }}'
          }"
      register: _apply

    - name: List stack apply info
      ansible.builtin.debug:
        msg:
          - "Plan Stack: {{ _apply }}"

    - name: Delete Zip terraform archive
      ansible.builtin.file:
        path: "/tmp/core-stack.zip"
        state: absent

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

    - name: Get resources (computes) in stack
      oracle.oci.oci_resource_manager_stack_associated_resource_facts:
        stack_id: "{{ _stack.stack.id }}"
      register: _resources

    - name: List resources (computes) in stack
      ansible.builtin.debug:
        msg:
          - "{{ _resources.stack_associated_resources | json_query('[*].resource_id') }}"
...

Test role play

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

Destroy associated resources, and delete stack

File delete-compute-rm-stack.yml
---
# https://docs.oracle.com/en-us/iaas/Content/ResourceManager/Tasks/create-job-destroy.htm
- name: Remove RM resources and stack
  connection: local
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Get exported stack id
      ansible.builtin.slurp:
        src: "/tmp/deleteme-stack-id"
      register: _stack_id

    - name: Show stack id to delete
      ansible.builtin.debug:
        msg: "Delete stack: {{ _stack_id['content'] | b64decode }}"

    - name: Destroy resources (computes) in stack
      oracle.oci.oci_resource_manager_job:
        stack_id: "{{ _stack_id['content'] | b64decode }}"
        job_operation_details:
          "{
            'operation': 'DESTROY',
            'execution_plan_strategy': 'AUTO_APPROVED'
          }"

    - name: Delete stack
      oracle.oci.oci_resource_manager_stack:
        state: absent
        stack_id: "{{ _stack_id['content'] | b64decode }}"

    - name: Delete RM stack ID
      ansible.builtin.file:
        path: "/tmp/deleteme-stack-id"
        state: absent
...

Python wrapper script

File test-play.py
#!/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': '/bin/:/sbin:/bin:/usr/sbin:/usr/bin'},
        playbook = playbook_path,
        inventory = inventory_path
    )
    print(r.stats)

def role_test(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': '/bin/:/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 using OCI Resource Manager - Stack - 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__))
    # create compute
    playbook_path_create_comp = os.path.join(work_dir, "create-rm-stack-compute.yml")
    playbook_path_delete_comp = os.path.join(work_dir, "delete-compute-rm-stack.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(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 using OCI Resource Manager - Stack - 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