๐Ÿ“Œ OCI Multi-Region Compute Host Lifecycle Inventory Utility

6/1/2021

๐Ÿ“Œ 1. Description & Overview

Managing a large OCI tenancy with multiple compartments and regions requires continuous visibility into instance lifecycle states for governance, audit, and cost optimization.

๐Ÿ›ก๏ธ Strict API Rate-Limiting Guardrail

To prevent API throttling (HTTP 429 TooManyRequests), tenancy quota exhaustion, and execution timeouts:

[!CAUTION] API Guardrail Rule: The script enforces a MINIMUM of 1 region and a MAXIMUM of 3 regions per execution.

  • If 0 regions or more than 3 regions are provided, the script immediately aborts execution before making any compute or compartment API requests.

๐Ÿ”‘ 2. Required IAM Policies

To run this inventory tool using either Instance Principals (recommended for automated VMs) or User API Keys:

  1. Dynamic Group: InventoryCollectorDynamicGroup
    ANY {instance.id = 'ocid1.instance.oc1.iad.your_master_vm_ocid'}
  2. IAM Policy Statements (Scoped to Tenancy Root):
    Allow dynamic-group InventoryCollectorDynamicGroup to read instances in tenancy
    Allow dynamic-group InventoryCollectorDynamicGroup to read compartments in tenancy

Option B: User / Group Policy Setup

If executing via CLI configured with user credentials (~/.oci/config):

Allow group AuditInventoryGroup to read instances in tenancy
Allow group AuditInventoryGroup to read compartments in tenancy

๐Ÿ› ๏ธ 3. Prerequisites & Setup

  1. Install OCI CLI:
    sudo bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"
  2. Export Path: Ensure oci is in your environment PATH:
    export PATH="/usr/local/bin:/usr/bin:/bin:${PATH}"
  3. Python 3: Ensure Python 3 is installed for stream JSON parsing.

Key Guardrail Implementation & Features

Strict Region Limit Guardrail (1 to 3 Regions): The script inspects the input region parameters before making any OCI API calls:

REGION_COUNT=${#REGIONS[@]}
if [ "${REGION_COUNT}" -lt 1 ] || [ "${REGION_COUNT}" -gt 3 ]; then
    echo "[-] STRICT GUARDRAIL VIOLATION: Execution Aborted!"
    echo "    Reason  : Script permits MINIMUM 1 region and MAXIMUM 3 regions per execution."
    echo "    Passed  : ${REGION_COUNT} region(s) [${REGIONS[*]:-None}]"
    echo "    Purpose : Prevents OCI API rate-limit overload and tenancy throttling."
    exit 1
fi

Tenancy-Wide Discovery Across All Compartments:

  • Recursively queries all active compartments in the tenancy (โ€“compartment-id-in-subtree true) for each allowed region (up to 3).

Comprehensive Lifecycle Tracking:

  • Categorizes compute hosts by state (RUNNING, STOPPED, TERMINATED, PROVISIONING), shape, OCPU/RAM specs, Availability Domain, and Compartment ownership.

Dual Output Artifacts:

  • CSV File: oci_compute_inventory_YYYYMMDD_HHMMSS.csv for Excel/BI reporting.
  • Text Summary Report: oci_compute_inventory_report_YYYYMMDD_HHMMSS.txt for console logs and metrics auditing.

Verified Guardrail Behavior

Test 1: Passing > 3 Regions (Triggers Execution Abort)

$ ./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1
=======================================================================
        OCI COMPUTE HOST LIFECYCLE INVENTORY UTILITY                   
=======================================================================
[+] Specified Regions Count : 4
[+] Specified Regions List  : us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1
=======================================================================
[-] STRICT GUARDRAIL VIOLATION: Execution Aborted!
    Reason  : Script permits MINIMUM 1 region and MAXIMUM 3 regions per execution.
    Passed  : 4 region(s) [us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1]
    Purpose : Prevents OCI API rate-limit overload and tenancy throttling.

Test 2: Passing 2 Regions (Valid Execution Range)

./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1

Required IAM Policy Statements

To allow your Master Management VM to query compute instances tenancy-wide via Instance Principals:

Allow dynamic-group InventoryCollectorDynamicGroup to read instances in tenancy
Allow dynamic-group InventoryCollectorDynamicGroup to read compartments in tenancy

๐Ÿ“œ 4. Full Bash Script (oci_compute_inventory.sh)

Save the script as oci_compute_inventory.sh:

#!/usr/bin/env bash
# ==============================================================================
# Script: oci_compute_inventory.sh
# Purpose: Multi-Region & Multi-Compartment Compute Host Lifecycle Inventory
# Strict Guardrail: Enforces MINIMUM 1 region and MAXIMUM 3 regions per execution
#                   to prevent OCI API rate-limiting / throttling overload.
# ==============================================================================

set -euo pipefail

# ------------------------------------------------------------------------------
# ENVIRONMENT & CRON PATH SETUP
# ------------------------------------------------------------------------------
export PATH="/usr/local/bin:/usr/bin:/bin:${PATH:-}"

# Configuration Defaults
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_CSV="oci_compute_inventory_${TIMESTAMP}.csv"
OUTPUT_REPORT="oci_compute_inventory_report_${TIMESTAMP}.txt"
OCI_AUTH_TYPE="${OCI_AUTH_TYPE:-instance_principal}" # "instance_principal" or "api_key"
TENANCY_OCID="${TENANCY_OCID:-}"

# Parse Input Regions from Arguments
REGIONS=()
for arg in "$@"; do
    if [[ "$arg" != -* ]]; then
        REGIONS+=("$arg")
    fi
done

# ------------------------------------------------------------------------------
# STRICT GUARDRAIL CHECK: MIN 1 REGION, MAX 3 REGIONS
# ------------------------------------------------------------------------------
REGION_COUNT=${#REGIONS[@]}

echo "======================================================================="
echo "        OCI COMPUTE HOST LIFECYCLE INVENTORY UTILITY                   "
echo "======================================================================="
echo "[+] Specified Regions Count : ${REGION_COUNT}"
echo "[+] Specified Regions List  : ${REGIONS[*]:-None}"
echo "======================================================================="

if [ "${REGION_COUNT}" -lt 1 ] || [ "${REGION_COUNT}" -gt 3 ]; then
    echo ""
    echo "[-] STRICT GUARDRAIL VIOLATION: Execution Aborted!"
    echo "    Reason  : Script permits MINIMUM 1 region and MAXIMUM 3 regions per execution."
    echo "    Passed  : ${REGION_COUNT} region(s) [${REGIONS[*]:-None}]"
    echo "    Purpose : Prevents OCI API rate-limit overload and tenancy throttling."
    echo ""
    echo "Usage Examples:"
    echo "  $0 us-ashburn-1"
    echo "  $0 us-ashburn-1 us-phoenix-1"
    echo "  $0 us-ashburn-1 us-phoenix-1 eu-frankfurt-1"
    echo ""
    exit 1
fi

# Pre-flight Check: OCI CLI binary existence
if ! command -v oci &>/dev/null; then
    echo "[-] ERROR: OCI CLI binary ('oci') not found in PATH." >&2
    exit 1
fi

AUTH_FLAG=""
if [ "${OCI_AUTH_TYPE}" == "instance_principal" ]; then
    AUTH_FLAG="--auth instance_principal"
fi

# Retrieve Tenancy OCID if not provided
if [ -z "${TENANCY_OCID}" ]; then
    echo "[+] Retrieving Tenancy OCID..."
    TENANCY_OCID=$(oci iam compartment list ${AUTH_FLAG} --all --query "data[0].\"compartment-id\"" --output raw 2>/dev/null || echo "")
fi

# ------------------------------------------------------------------------------
# STEP 1: FETCH ALL COMPARTMENTS IN TENANCY
# ------------------------------------------------------------------------------
echo "[+] Step 1: Fetching all active compartments across the tenancy..."
COMPARTMENT_JSON=$(oci iam compartment list \
    ${AUTH_FLAG} \
    --compartment-id-in-subtree true \
    --access-level ACCESSIBLE \
    --all \
    --query "data[?\"lifecycle-state\"=='ACTIVE'].{id: id, name: name}" \
    --output json 2>/dev/null || echo "[]")

COMPARTMENT_COUNT=$(echo "${COMPARTMENT_JSON}" | grep -c '"id"' || echo 0)
echo "    -> Located ${COMPARTMENT_COUNT} active compartment(s)."

# Create CSV Header
echo "Region,Compartment_Name,Compartment_OCID,Instance_Name,Instance_OCID,Lifecycle_State,Shape,OCPU_Count,RAM_GB,Availability_Domain,Time_Created" > "${OUTPUT_CSV}"

# Create Report Header
{
    echo "======================================================================="
    echo "              OCI COMPUTE LIFECYCLE INVENTORY REPORT                   "
    echo " Date of Execution : $(date)"
    echo " Target Regions    : ${REGIONS[*]}"
    echo " Total Compartments: ${COMPARTMENT_COUNT}"
    echo "======================================================================="
    echo ""
} > "${OUTPUT_REPORT}"

TOTAL_INSTANCES=0
RUNNING_COUNT=0
STOPPED_COUNT=0
OTHER_COUNT=0

# ------------------------------------------------------------------------------
# STEP 2: ITERATE OVER ALLOWED REGIONS (MAX 3) AND COMPARTMENTS
# ------------------------------------------------------------------------------
echo "[+] Step 2: Querying compute instances across specified regions..."

for region in "${REGIONS[@]}"; do
    echo -e "\n---> Scanning Region: ${region}" | tee -a "${OUTPUT_REPORT}"
    REG_RUNNING=0
    REG_STOPPED=0
    REG_OTHER=0

    python3 -c '
import sys, json
try:
    data = json.loads(sys.argv[1])
    for item in data:
        print(f"{item[\"id\"]}|{item[\"name\"]}")
except Exception:
    pass
' "${COMPARTMENT_JSON}" | while IFS='|' read -r comp_id comp_name; do

        JMES_QUERY="data[].{id: id, name: display_name, state: lifecycle_state, shape: shape, ocpus: shape_config.ocpus, ram: shape_config.memory_in_gbs, ad: availability_domain, created: time_created}"
        
        INSTANCES_JSON=$(oci compute instance list \
            ${AUTH_FLAG} \
            --region "${region}" \
            --compartment-id "${comp_id}" \
            --all \
            --query "${JMES_QUERY}" \
            --output json 2>/dev/null || echo "[]")

        python3 -c '
import sys, json
try:
    data = json.loads(sys.argv[1])
    for inst in data:
        name = inst.get("name") or "N/A"
        ocid = inst.get("id") or "N/A"
        state = inst.get("state") or "UNKNOWN"
        shape = inst.get("shape") or "N/A"
        ocpus = inst.get("ocpus") or "N/A"
        ram = inst.get("ram") or "N/A"
        ad = inst.get("ad") or "N/A"
        created = inst.get("created") or "N/A"
        print(f"{name}|{ocid}|{state}|{shape}|{ocpus}|{ram}|{ad}|{created}")
except Exception:
    pass
' "${INSTANCES_JSON}" | while IFS='|' read -r inst_name inst_id inst_state inst_shape inst_ocpus inst_ram inst_ad inst_created; do

            echo "${region},\"${comp_name}\",${comp_id},\"${inst_name}\",${inst_id},${inst_state},${inst_shape},${inst_ocpus},${inst_ram},${inst_ad},${inst_created}" >> "${OUTPUT_CSV}"
            echo "  [${inst_state}] Instance: ${inst_name} | Shape: ${inst_shape} | AD: ${inst_ad} | Comp: ${comp_name}" >> "${OUTPUT_REPORT}"

            TOTAL_INSTANCES=$((TOTAL_INSTANCES + 1))

            case "${inst_state}" in
                RUNNING)
                    RUNNING_COUNT=$((RUNNING_COUNT + 1))
                    REG_RUNNING=$((REG_RUNNING + 1))
                    ;;
                STOPPED)
                    STOPPED_COUNT=$((STOPPED_COUNT + 1))
                    REG_STOPPED=$((REG_STOPPED + 1))
                    ;;
                *)
                    OTHER_COUNT=$((OTHER_COUNT + 1))
                    REG_OTHER=$((REG_OTHER + 1))
                    ;;
            esac
        done
    done

    echo "    Region Summary (${region}): Running=${REG_RUNNING}, Stopped=${REG_STOPPED}, Other=${REG_OTHER}" | tee -a "${OUTPUT_REPORT}"
done

# ------------------------------------------------------------------------------
# STEP 3: SUMMARY & REPORT FINALIZATION
# ------------------------------------------------------------------------------
{
    echo ""
    echo "======================================================================="
    echo "                         TENANCY SUMMARY METRICS                       "
    echo "======================================================================="
    echo " Regions Scanned             : ${REGIONS[*]} (${REGION_COUNT} region/s)"
    echo " Total Compute Hosts Found   : ${TOTAL_INSTANCES}"
    echo "   - RUNNING State           : ${RUNNING_COUNT}"
    echo "   - STOPPED State           : ${STOPPED_COUNT}"
    echo "   - OTHER (Provision/Term)  : ${OTHER_COUNT}"
    echo "======================================================================="
    echo " Detailed CSV Output File    : ${OUTPUT_CSV}"
    echo "======================================================================="
} | tee -a "${OUTPUT_REPORT}"

echo ""
echo "[+] SUCCESS: Compute inventory completed successfully."
echo "[+] Report File : ${OUTPUT_REPORT}"
echo "[+] CSV Data    : ${OUTPUT_CSV}"

๐Ÿš€ 5. How to Run & Execution Examples

1. Single Region Run (1 Region - Valid)

chmod +x oci_compute_inventory.sh
./oci_compute_inventory.sh us-ashburn-1

2. Dual Region Run (2 Regions - Valid)

./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1

3. Triple Region Run (3 Regions - Valid Maximum)

./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1 eu-frankfurt-1

4. Over Limit Example (> 3 Regions - Aborts Execution)

$ ./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1
=======================================================================
        OCI COMPUTE HOST LIFECYCLE INVENTORY UTILITY                   
=======================================================================
[+] Specified Regions Count : 4
[+] Specified Regions List  : us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1
=======================================================================

[-] STRICT GUARDRAIL VIOLATION: Execution Aborted!
    Reason  : Script permits MINIMUM 1 region and MAXIMUM 3 regions per execution.
    Passed  : 4 region(s) [us-ashburn-1 us-phoenix-1 eu-frankfurt-1 uk-london-1]
    Purpose : Prevents OCI API rate-limit overload and tenancy throttling.

๐Ÿ“Š 6. Output Files & Formats

The script automatically generates two artifacts:

  1. Text Summary Report (oci_compute_inventory_report_YYYYMMDD_HHMMSS.txt): Contains human-readable regional summaries and lifecycle counts.
  2. CSV Spreadsheet (oci_compute_inventory_YYYYMMDD_HHMMSS.csv): Structured dataset containing: Region, Compartment_Name, Compartment_OCID, Instance_Name, Instance_OCID, Lifecycle_State, Shape, OCPU_Count, RAM_GB, Availability_Domain, Time_Created

โš ๏ธ 7. Precautions & Important OCI Caveats

[!WARNING]

1. API Rate Limiting & Throttling

OCI IAM and Compute endpoints enforce per-tenancy API call rate limits. Executing recursive API queries across more than 3 regions simultaneously can trigger 429 TooManyRequests errors. Always respect the 1-to-3 region limit per batch run.

[!NOTE]

2. Terminated Instance Lifecycle State

OCI retains metadata for TERMINATED compute instances for up to 30 days after deletion. The script filters and counts active states (RUNNING, STOPPED, PROVISIONING) separately from legacy TERMINATED records.

[!TIP]

3. Automated Scheduling for Large Tenancies

If your tenancy has 6 subscribed regions, break your cron jobs into two staggered executions:

  • Batch 1 (02:00 AM): ./oci_compute_inventory.sh us-ashburn-1 us-phoenix-1 ca-toronto-1
  • Batch 2 (02:30 AM): ./oci_compute_inventory.sh eu-frankfurt-1 uk-london-1 ap-tokyo-1