#!/usr/bin/env bash ######################################################################################### # # # OCI Free Tier Monitor # # Monitor Oracle Cloud Infrastructure Always Free tier usage # # # # Author: Phil Connor # # Contact: contact@mylinux.work # # License: MIT # # Version: 1.00 # # # ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────────────── WARN_PCT="${OFT_WARN_PCT:-80}" CRIT_PCT="${OFT_CRIT_PCT:-95}" FORMAT="text" INTERVAL="${OFT_INTERVAL:-3600}" SLACK_WEBHOOK="${OFT_SLACK_WEBHOOK:-}" COMPARTMENT_ID="${OCI_COMPARTMENT_ID:-}" OCI_PROFILE="${OCI_CLI_PROFILE:-DEFAULT}" VERBOSE="${VERBOSE:-false}" COLOR="${COLOR:-auto}" RUN_MODE="" SCRIPT_NAME="$(basename "$0")" # ── Always Free Limits ──────────────────────────────────────────────────────── LIMIT_AMD_INSTANCES=2 LIMIT_ARM_OCPU=4 LIMIT_ARM_RAM_GB=24 LIMIT_BLOCK_STORAGE_GB=200 LIMIT_VOLUME_BACKUPS=5 LIMIT_OBJECT_STORAGE_GB=10 LIMIT_AUTONOMOUS_DB=2 LIMIT_LOAD_BALANCERS=1 # ── Colors ──────────────────────────────────────────────────────────────────── setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET="" return fi if [[ "$COLOR" == "auto" && ! -t 1 ]]; then RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET="" return fi RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' } # ── Logging ─────────────────────────────────────────────────────────────────── log() { printf "${GREEN}%s${RESET}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${RESET}\n" "$*" >&2; } err() { printf "${RED}✗ %s${RESET}\n" "$*" >&2; } verbose() { [[ "$VERBOSE" == "true" ]] && printf "${DIM} %s${RESET}\n" "$*" >&2 || true; } die() { err "$*"; exit 1; } # ── Help ────────────────────────────────────────────────────────────────────── show_help() { cat < /var/lib/node_exporter/textfile/oci.prom EOF } # ── Argument Parsing ────────────────────────────────────────────────────────── parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --check) RUN_MODE="check"; shift ;; --report) RUN_MODE="report"; shift ;; --watch) RUN_MODE="watch"; shift ;; --alerts) RUN_MODE="alerts"; shift ;; --format) FORMAT="$2"; shift 2 ;; --warn) WARN_PCT="$2"; shift 2 ;; --crit) CRIT_PCT="$2"; shift 2 ;; --interval) INTERVAL="$2"; shift 2 ;; --slack-webhook) SLACK_WEBHOOK="$2"; shift 2 ;; --compartment-id) COMPARTMENT_ID="$2"; shift 2 ;; --no-color) COLOR="never"; shift ;; --verbose) VERBOSE="true"; shift ;; --help) show_help; exit 0 ;; *) die "Unknown option: $1" ;; esac done if [[ -z "$RUN_MODE" ]]; then err "No mode specified"; echo ""; show_help; exit 1; fi } # ── Dependency Check ────────────────────────────────────────────────────────── check_deps() { local missing=() command -v oci >/dev/null 2>&1 || missing+=("oci") command -v jq >/dev/null 2>&1 || missing+=("jq") command -v curl >/dev/null 2>&1 || missing+=("curl") if [[ ${#missing[@]} -gt 0 ]]; then die "Missing required tools: ${missing[*]}" fi verbose "Dependencies satisfied: oci, jq, curl" } # ── OCI Helpers ─────────────────────────────────────────────────────────────── oci_cmd() { oci --profile "$OCI_PROFILE" "$@" 2>/dev/null } get_compartment_id() { if [[ -n "$COMPARTMENT_ID" ]]; then verbose "Using compartment: $COMPARTMENT_ID" return fi COMPARTMENT_ID=$(oci_cmd iam compartment list \ --include-root \ --query 'data[?contains("lifecycle-state", `ACTIVE`)] | [0]."compartment-id"' \ --raw-output 2>/dev/null || true) if [[ -z "$COMPARTMENT_ID" ]]; then COMPARTMENT_ID=$(oci_cmd iam region-subscription list \ --query 'data[0]."tenancy-id"' --raw-output 2>/dev/null || true) fi if [[ -z "$COMPARTMENT_ID" ]]; then die "Cannot determine compartment/tenancy ID. Use --compartment-id." fi verbose "Resolved root compartment: $COMPARTMENT_ID" } get_tenancy_name() { local name name=$(oci_cmd iam tenancy get --tenancy-id "$COMPARTMENT_ID" \ --query 'data.name' --raw-output 2>/dev/null || echo "unknown") echo "$name" } # ── Resource Check Functions ────────────────────────────────────────────────── declare -A USAGE declare -A LIMITS declare -A UNITS check_compute() { verbose "Checking compute instances..." local instances instances=$(oci_cmd compute instance list \ --compartment-id "$COMPARTMENT_ID" \ --lifecycle-state RUNNING \ --query 'data' 2>/dev/null || echo "[]") local amd_count=0 local arm_ocpu=0 local arm_ram=0 while IFS= read -r line; do local shape shape=$(echo "$line" | jq -r '.shape // ""') if [[ "$shape" == *"A1"* ]] || [[ "$shape" == *"Ampere"* ]]; then local ocpu mem ocpu=$(echo "$line" | jq -r '."shape-config"."ocpus" // 0') mem=$(echo "$line" | jq -r '."shape-config"."memory-in-gbs" // 0') arm_ocpu=$(echo "$arm_ocpu + $ocpu" | bc) arm_ram=$(echo "$arm_ram + $mem" | bc) else ((amd_count++)) || true fi done < <(echo "$instances" | jq -c '.[]') USAGE[compute_amd]=$amd_count LIMITS[compute_amd]=$LIMIT_AMD_INSTANCES UNITS[compute_amd]="inst" USAGE[compute_arm_ocpu]=$arm_ocpu LIMITS[compute_arm_ocpu]=$LIMIT_ARM_OCPU UNITS[compute_arm_ocpu]="OCPU" USAGE[compute_arm_ram]=$arm_ram LIMITS[compute_arm_ram]=$LIMIT_ARM_RAM_GB UNITS[compute_arm_ram]="GB" verbose " AMD instances: ${amd_count}/${LIMIT_AMD_INSTANCES}" verbose " Arm OCPU: ${arm_ocpu}/${LIMIT_ARM_OCPU}" verbose " Arm RAM: ${arm_ram}GB/${LIMIT_ARM_RAM_GB}GB" } check_block_storage() { verbose "Checking block storage..." local volumes volumes=$(oci_cmd bv volume list \ --compartment-id "$COMPARTMENT_ID" \ --lifecycle-state AVAILABLE \ --query 'data' 2>/dev/null || echo "[]") local total_gb=0 while IFS= read -r size; do total_gb=$(echo "$total_gb + $size" | bc) done < <(echo "$volumes" | jq -r '.[]."size-in-gbs" // 0') local backups backups=$(oci_cmd bv volume-backup list \ --compartment-id "$COMPARTMENT_ID" \ --lifecycle-state AVAILABLE \ --query 'length(data)' 2>/dev/null || echo "0") USAGE[block_storage]=$total_gb LIMITS[block_storage]=$LIMIT_BLOCK_STORAGE_GB UNITS[block_storage]="GB" USAGE[volume_backups]=$backups LIMITS[volume_backups]=$LIMIT_VOLUME_BACKUPS UNITS[volume_backups]="" verbose " Block storage: ${total_gb}GB/${LIMIT_BLOCK_STORAGE_GB}GB" verbose " Volume backups: ${backups}/${LIMIT_VOLUME_BACKUPS}" } check_object_storage() { verbose "Checking object storage..." local namespace namespace=$(oci_cmd os ns get --query 'data' --raw-output 2>/dev/null || echo "") if [[ -z "$namespace" ]]; then warn "Cannot determine object storage namespace" USAGE[object_storage]=0 LIMITS[object_storage]=$LIMIT_OBJECT_STORAGE_GB UNITS[object_storage]="GB" return fi local buckets buckets=$(oci_cmd os bucket list \ --compartment-id "$COMPARTMENT_ID" \ --namespace-name "$namespace" \ --query 'data[].name' 2>/dev/null || echo "[]") local total_bytes=0 while IFS= read -r bucket; do [[ -z "$bucket" || "$bucket" == "null" ]] && continue local size size=$(oci_cmd os bucket get \ --namespace-name "$namespace" \ --bucket-name "$bucket" \ --fields "approximateSize" \ --query 'data."approximate-size"' --raw-output 2>/dev/null || echo "0") [[ "$size" == "null" ]] && size=0 total_bytes=$(echo "$total_bytes + $size" | bc) done < <(echo "$buckets" | jq -r '.[]') local total_gb total_gb=$(echo "scale=1; $total_bytes / 1073741824" | bc) USAGE[object_storage]=$total_gb LIMITS[object_storage]=$LIMIT_OBJECT_STORAGE_GB UNITS[object_storage]="GB" verbose " Object storage: ${total_gb}GB/${LIMIT_OBJECT_STORAGE_GB}GB" } check_autonomous_db() { verbose "Checking autonomous databases..." local count count=$(oci_cmd db autonomous-database list \ --compartment-id "$COMPARTMENT_ID" \ --lifecycle-state AVAILABLE \ --query 'length(data)' 2>/dev/null || echo "0") USAGE[autonomous_db]=$count LIMITS[autonomous_db]=$LIMIT_AUTONOMOUS_DB UNITS[autonomous_db]="" verbose " Autonomous DB: ${count}/${LIMIT_AUTONOMOUS_DB}" } check_load_balancer() { verbose "Checking load balancers..." local count count=$(oci_cmd lb load-balancer list \ --compartment-id "$COMPARTMENT_ID" \ --lifecycle-state ACTIVE \ --query 'length(data)' 2>/dev/null || echo "0") USAGE[load_balancers]=$count LIMITS[load_balancers]=$LIMIT_LOAD_BALANCERS UNITS[load_balancers]="" verbose " Load balancers: ${count}/${LIMIT_LOAD_BALANCERS}" } # ── Calculation ─────────────────────────────────────────────────────────────── calculate_pct() { local used="$1" limit="$2" if [[ "$limit" == "0" ]]; then echo "0" return fi echo "scale=0; ($used * 100) / $limit" | bc } get_status() { local pct="$1" if (( pct >= CRIT_PCT )); then echo "critical" elif (( pct >= WARN_PCT )); then echo "warning" else echo "ok" fi } # ── Collect All Data ────────────────────────────────────────────────────────── RESOURCE_ORDER=(compute_amd compute_arm_ocpu compute_arm_ram block_storage volume_backups object_storage autonomous_db load_balancers) RESOURCE_LABELS=( "Compute (AMD)" "Compute (Arm OCPU)" "Compute (Arm RAM)" "Block Storage" "Volume Backups" "Object Storage" "Autonomous DB" "Load Balancers" ) collect_all() { check_compute check_block_storage check_object_storage check_autonomous_db check_load_balancer } # ── Output Formatters ───────────────────────────────────────────────────────── format_text() { local alerts_only="${1:-false}" local tenancy tenancy=$(get_tenancy_name) printf "\n${BOLD}OCI Free Tier Monitor${RESET}\n" printf "Tenancy: %s\n" "$tenancy" printf "Time: %s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" printf "\n ── Resource Usage ──\n\n" printf " %-22s %-12s %-12s %-8s %s\n" "Resource" "Used" "Limit" "Usage" "Status" printf " %s\n" "$(printf '%.0s─' {1..64})" local ok_count=0 warn_count=0 crit_count=0 for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" local label="${RESOURCE_LABELS[$i]}" local used="${USAGE[$key]}" local limit="${LIMITS[$key]}" local unit="${UNITS[$key]}" local pct pct=$(calculate_pct "$used" "$limit") local status status=$(get_status "$pct") case "$status" in ok) ((ok_count++)) || true; status_str="${GREEN}✓ OK${RESET}" ;; warning) ((warn_count++)) || true; status_str="${YELLOW}⚠ Warning${RESET}" ;; critical) ((crit_count++)) || true; status_str="${RED}✗ Critical${RESET}" ;; esac if [[ "$alerts_only" == "true" && "$status" == "ok" ]]; then continue fi local used_str="${used}${unit:+ $unit}" local limit_str="${limit}${unit:+ $unit}" printf " %-22s %-12s %-12s %3s%% ${status_str}\n" \ "$label" "$used_str" "$limit_str" "$pct" done printf "\n Summary: %s OK, %s Warning, %s Critical\n\n" \ "$ok_count" "$warn_count" "$crit_count" } format_json() { local alerts_only="${1:-false}" local resources="[]" for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" local label="${RESOURCE_LABELS[$i]}" local used="${USAGE[$key]}" local limit="${LIMITS[$key]}" local pct pct=$(calculate_pct "$used" "$limit") local status status=$(get_status "$pct") if [[ "$alerts_only" == "true" && "$status" == "ok" ]]; then continue fi resources=$(echo "$resources" | jq \ --arg key "$key" \ --arg label "$label" \ --argjson used "$used" \ --argjson limit "$limit" \ --argjson pct "$pct" \ --arg status "$status" \ '. + [{"resource": $key, "label": $label, "used": $used, "limit": $limit, "percent": $pct, "status": $status}]') done jq -n \ --arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --argjson warn "$WARN_PCT" \ --argjson crit "$CRIT_PCT" \ --argjson resources "$resources" \ '{"timestamp": $time, "warn_threshold": $warn, "crit_threshold": $crit, "resources": $resources}' } format_prometheus() { echo "# HELP oci_free_tier_usage_percent OCI Free Tier resource usage percentage" echo "# TYPE oci_free_tier_usage_percent gauge" for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" local used="${USAGE[$key]}" local limit="${LIMITS[$key]}" local pct pct=$(calculate_pct "$used" "$limit") echo "oci_free_tier_usage_percent{resource=\"${key}\"} ${pct}" done echo "# HELP oci_free_tier_used OCI Free Tier resource current usage" echo "# TYPE oci_free_tier_used gauge" for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" echo "oci_free_tier_used{resource=\"${key}\"} ${USAGE[$key]}" done echo "# HELP oci_free_tier_limit OCI Free Tier resource limit" echo "# TYPE oci_free_tier_limit gauge" for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" echo "oci_free_tier_limit{resource=\"${key}\"} ${LIMITS[$key]}" done echo "# HELP oci_free_tier_warn_threshold Warning threshold percentage" echo "# TYPE oci_free_tier_warn_threshold gauge" echo "oci_free_tier_warn_threshold ${WARN_PCT}" echo "# HELP oci_free_tier_crit_threshold Critical threshold percentage" echo "# TYPE oci_free_tier_crit_threshold gauge" echo "oci_free_tier_crit_threshold ${CRIT_PCT}" } format_output() { local alerts_only="${1:-false}" case "$FORMAT" in text) format_text "$alerts_only" ;; json) format_json "$alerts_only" ;; prometheus) format_prometheus ;; *) die "Unknown format: $FORMAT" ;; esac } # ── Slack Notification ──────────────────────────────────────────────────────── send_slack_alert() { [[ -z "$SLACK_WEBHOOK" ]] && return local breached=() for i in "${!RESOURCE_ORDER[@]}"; do local key="${RESOURCE_ORDER[$i]}" local label="${RESOURCE_LABELS[$i]}" local used="${USAGE[$key]}" local limit="${LIMITS[$key]}" local pct pct=$(calculate_pct "$used" "$limit") local status status=$(get_status "$pct") if [[ "$status" != "ok" ]]; then breached+=("${label}: ${used}/${limit} (${pct}%) [${status}]") fi done if [[ ${#breached[@]} -eq 0 ]]; then verbose "No threshold breaches — skipping Slack notification" return fi local message="OCI Free Tier Alert — $(date -u +%Y-%m-%dT%H:%M:%SZ)\n" for line in "${breached[@]}"; do message+="• ${line}\n" done local payload payload=$(jq -n --arg text "$message" '{"text": $text}') if curl -sf -o /dev/null -X POST \ -H "Content-Type: application/json" \ -d "$payload" "$SLACK_WEBHOOK"; then verbose "Slack notification sent" else warn "Failed to send Slack notification" fi } # ── Exit Code ───────────────────────────────────────────────────────────────── get_exit_code() { local max_status="ok" for key in "${RESOURCE_ORDER[@]}"; do local used="${USAGE[$key]}" local limit="${LIMITS[$key]}" local pct pct=$(calculate_pct "$used" "$limit") local status status=$(get_status "$pct") if [[ "$status" == "critical" ]]; then max_status="critical" break elif [[ "$status" == "warning" ]]; then max_status="warning" fi done case "$max_status" in ok) echo 0 ;; warning) echo 1 ;; critical) echo 2 ;; esac } # ── Mode Functions ──────────────────────────────────────────────────────────── run_check() { collect_all format_output "false" send_slack_alert local code code=$(get_exit_code) exit "$code" } run_report() { collect_all format_output "false" send_slack_alert } run_alerts() { collect_all format_output "true" send_slack_alert local code code=$(get_exit_code) exit "$code" } run_watch() { log "Starting watch mode (interval: ${INTERVAL}s)" while true; do collect_all format_output "false" send_slack_alert verbose "Next check in ${INTERVAL}s" sleep "$INTERVAL" done } # ── Main ────────────────────────────────────────────────────────────────────── main() { parse_args "$@" setup_colors check_deps get_compartment_id case "$RUN_MODE" in check) run_check ;; report) run_report ;; alerts) run_alerts ;; watch) run_watch ;; esac } main "$@"