#!/usr/bin/env bash # ============================================================================== # OCI Always Free Tier & PAYG Compliance Auditor # Engineered by D4niel · d4niel.dev # ============================================================================== set -uo pipefail VERSION="v0.1.6" # ------------------------------------------------------------------------------ # Paleta de color y glifos ANSI 256 # ------------------------------------------------------------------------------ if [[ -t 1 ]]; then B=$'\e[1m'; D=$'\e[2m'; R=$'\e[0m' BLU=$'\e[38;5;39m'; CYN=$'\e[38;5;45m'; GRN=$'\e[38;5;79m' RED=$'\e[38;5;203m'; YEL=$'\e[38;5;221m'; GRY=$'\e[38;5;244m'; WHT=$'\e[97m' else B=''; D=''; R=''; BLU=''; CYN=''; GRN=''; RED=''; YEL=''; GRY=''; WHT='' fi CHK="${GRN}✔${R}"; CRS="${RED}✖${R}"; ARR="${BLU}➜${R}"; WRN="${YEL}⚠${R}"; INF="${BLU}ℹ${R}" hr(){ printf "${GRY} %s${R}\n" "────────────────────────────────────────────────────────────────"; } die(){ echo; printf " %b ${RED}%s${R}\n" "$CRS" "$1"; echo; exit 1; } # Restaurar cursor siempre al salir o cancelar con Ctrl+C TMP_DIR="$(mktemp -d)" cleanup(){ printf "\e[?25h" 2>/dev/null || true rm -rf "$TMP_DIR" } trap cleanup EXIT trap 'printf "\e[?25h" 2>/dev/null || true; echo; exit 130' INT TERM # ------------------------------------------------------------------------------ # Autodetección de PATH para OCI CLI y validación de dependencias # ------------------------------------------------------------------------------ for p in "$HOME/bin" "/root/bin" "$HOME/.local/bin" "/usr/local/bin"; do [[ -d "$p" && ":$PATH:" != *":$p:"* ]] && PATH="$p:$PATH" done export PATH check_dependencies(){ local missing=() for cmd in oci jq awk sed grep wc mktemp; do command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") done if (( ${#missing[@]} > 0 )); then echo printf " %b ${RED}${B}Faltan dependencias requeridas:${R} ${WHT}%s${R}\n" "$CRS" "${missing[*]}" for m in "${missing[@]}"; do case "$m" in oci) echo " ${ARR} Instala OCI CLI: ${CYN}bash -c \"\$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)\"${R}" echo " O enlázalo: ${CYN}sudo ln -s ~/bin/oci /usr/local/bin/oci${R}" ;; jq) echo " ${ARR} Instala jq: ${CYN}sudo apt-get install -y jq${R} ${GRY}(o dnf/yum install -y jq)${R}" ;; awk) echo " ${ARR} Instala awk: ${CYN}sudo apt-get install -y gawk${R}" ;; esac done echo exit 1 fi } check_dependencies # ------------------------------------------------------------------------------ # Límites de Oracle Cloud Always Free (ajustables por variables de entorno) # Nota: En PAYG el límite Always Free de Ampere A1 es hasta 4 OCPUs / 24 GB RAM # (en cuentas Free Trial nuevas puede ser 2 OCPUs / 12 GB RAM). # ------------------------------------------------------------------------------ LIMIT_AMD_MICRO_INSTANCES="${LIMIT_AMD_MICRO_INSTANCES:-2}" LIMIT_ARM_OCPUS="${LIMIT_ARM_OCPUS:-4.0}" LIMIT_ARM_RAM_GB="${LIMIT_ARM_RAM_GB:-24.0}" LIMIT_STORAGE_GB="${LIMIT_STORAGE_GB:-200}" LIMIT_VOLUME_BACKUPS="${LIMIT_VOLUME_BACKUPS:-5}" LIMIT_OBJECT_STORAGE_GB="${LIMIT_OBJECT_STORAGE_GB:-20}" LIMIT_AUTONOMOUS_DB="${LIMIT_AUTONOMOUS_DB:-2}" LIMIT_MYSQL_DB="${LIMIT_MYSQL_DB:-1}" LIMIT_NOSQL_TABLES="${LIMIT_NOSQL_TABLES:-3}" LIMIT_VCNS="${LIMIT_VCNS:-2}" LIMIT_LOAD_BALANCERS="${LIMIT_LOAD_BALANCERS:-1}" LIMIT_LB_MAX_MBPS="${LIMIT_LB_MAX_MBPS:-10}" LIMIT_NETWORK_LOAD_BALANCERS="${LIMIT_NETWORK_LOAD_BALANCERS:-1}" LIMIT_RESERVED_IPS="${LIMIT_RESERVED_IPS:-2}" LIMIT_OUTBOUND_DATA_TB="${LIMIT_OUTBOUND_DATA_TB:-10}" OCI_PROFILE="${OCI_CLI_PROFILE:-DEFAULT}" CONFIG_FILE="${OCI_CLI_CONFIG_FILE:-$HOME/.oci/config}" [[ ! -f "$CONFIG_FILE" && -f "/root/.oci/config" ]] && CONFIG_FILE="/root/.oci/config" [[ -f "$CONFIG_FILE" ]] || die "No se encontró el archivo de configuración OCI en $CONFIG_FILE (ejecuta 'oci setup config')." # ------------------------------------------------------------------------------ # Motores de pasos con Spinner animado (con sangría de 4 espacios) # ------------------------------------------------------------------------------ run_step(){ local label="$1"; shift local log; log="$(mktemp)" ( "$@" >"$log" 2>&1 ) & local pid=$! frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' i=0 printf "\e[?25l" 2>/dev/null || true while kill -0 "$pid" 2>/dev/null; do printf "\r ${BLU}%s${R} ${GRY}%s${R} " "${frames:i++%10:1}" "$label" sleep 0.08 done wait "$pid"; local rc=$? printf "\e[?25h" 2>/dev/null || true if [[ $rc -eq 0 ]]; then printf "\r %b ${R}%s\033[K\n" "$CHK" "$label" else printf "\r %b ${RED}%s${R}\033[K\n" "$CRS" "$label" echo "${RED} ── output ─────────────────────────────${R}" sed 's/^/ /' "$log" | tail -15 echo "${RED} ───────────────────────────────────────${R}" rm -f "$log"; return $rc fi rm -f "$log" } # Ejecuta una tarea en segundo plano con spinner y, al terminar, limpia la línea # para que el llamador imprima el dato resuelto 1 por 1. run_resolve_step(){ local label="$1"; shift local log; log="$(mktemp)" ( "$@" >"$log" 2>&1 ) & local pid=$! frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' i=0 printf "\e[?25l" 2>/dev/null || true while kill -0 "$pid" 2>/dev/null; do printf "\r ${BLU}%s${R} ${GRY}%s${R} " "${frames:i++%10:1}" "$label" sleep 0.08 done wait "$pid"; local rc=$? printf "\e[?25h" 2>/dev/null || true if [[ $rc -ne 0 ]]; then printf "\r %b ${RED}%s${R}\033[K\n" "$CRS" "$label" echo "${RED} ── output ─────────────────────────────${R}" sed 's/^/ /' "$log" | tail -15 echo "${RED} ───────────────────────────────────────${R}" rm -f "$log"; return $rc fi printf "\r\033[K" rm -f "$log" } # Wrapper seguro para OCI CLI: cuando una lista en OCI está vacía, el CLI # devuelve un string vacío "" con código 0. Este helper garantiza JSON válido. oci_json(){ local out out=$(oci --config-file "$CONFIG_FILE" --profile "$OCI_PROFILE" "$@" --output json 2>/dev/null) || true if [[ -z "${out//[[:space:]]/}" ]]; then printf '{"data":[]}\n' else printf '%s\n' "$out" fi } # ------------------------------------------------------------------------------ # Formateador con alineación de columnas precisa # ------------------------------------------------------------------------------ print_metric_row(){ local icon="$1" label="$2" value="$3" note="${4:-}" local target_dots=27 local len=${#label} local dot_count=$(( target_dots - len )) [[ $dot_count -lt 2 ]] && dot_count=2 local dots="" for (( d=0; d/dev/null || true echo printf " ${D}Engineered by ${R}${B}D4niel${R} ${GRY}·${R} ${CYN}https://d4niel.dev${R}\n" hr local t_disp="${TENANCY_DISPLAY:-}" if [[ -z "$t_disp" ]]; then local t_ocid="${TENANCY_OCID:-}" [[ -z "$t_ocid" && -f "$TMP_DIR/tenancy_ocid" ]] && t_ocid=$(cat "$TMP_DIR/tenancy_ocid") local t_name="${TENANCY_NAME:-}" [[ -z "$t_name" && -f "$TMP_DIR/tenancy_name" ]] && t_name=$(cat "$TMP_DIR/tenancy_name") t_disp=$(format_tenancy_display "$t_ocid" "$t_name") fi local logo=( " ${D}░▒▒▓▓██████████▓▒░${R} " " ${BLU}▓${CYN}██████████████████${BLU}▓▒${R} " " ${BLU}▓${CYN}█████████████████████${BLU}▓▒${R} " " ${BLU}▓${CYN}██████${WHT}▓▒▒▒▒▒░${CYN}██████████${BLU}▓░${R} " " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}░▓${CYN}████████${BLU}▒${R} " " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}░▓${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}▒${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}▒${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}▒${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}▒${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}░▓${CYN}███████${BLU}▒${R}" " ${BLU}▓${CYN}██████${WHT}▒${R} ${WHT}░▓${CYN}████████${BLU}▒${R} " " ${BLU}▓${CYN}██████${WHT}▓▒▒▒▒▒░${CYN}██████████${BLU}▓░${R} " " ${BLU}▓${CYN}█████████████████████${BLU}▓▒${R} " " ${BLU}▓${CYN}██████████████████${BLU}▓▒${R} " " ${D}░▒▒▓▓██████████▓▒░${R} " ) local info=( "" "" " ${B}${WHT}D4niel OCI Auditor${R} ${BLU}${VERSION}${R}" " ${GRY}─────────────────────────────────${R}" " ${GRY}Tenancy ${R} ${t_disp:-—}" " ${GRY}Region ${R} ${WHT}${CURRENT_REGION:-—}${R} ${HOME_COLOR:-$CYN}${HOME_LABEL}${R}" " ${GRY}Started ${R} ${WHT}$(date -u '+%Y-%m-%d %H:%M UTC')${R}" " ${GRY}Target ${R} ${WHT}Always Free & PAYG Shield${R}" "" "" "" "" "" "" "" ) local i for i in "${!logo[@]}"; do printf " %b %b\n" "${logo[$i]}" "${info[$i]:-}" done hr echo } # ------------------------------------------------------------------------------ # Funciones del Paso 1 (Resolución progresiva 1 por 1) # ------------------------------------------------------------------------------ parse_config_key(){ local key="$1" awk -F= -v prof="[$OCI_PROFILE]" -v k="$key" ' $0 ~ /^\[.*\]$/ { in_prof = ($0 == prof) } in_prof && $1 ~ "^[[:space:]]*" k "[[:space:]]*$" { val = $2 sub(/^[[:space:]]+/, "", val) sub(/[[:space:]]+$/, "", val) gsub(/\r/, "", val) print val exit } ' "$CONFIG_FILE" } resolve_step_config(){ local t_ocid; t_ocid=$(parse_config_key "tenancy") local reg; reg=$(parse_config_key "region") local key_file; key_file=$(parse_config_key "key_file") key_file="${key_file/#\~/$HOME}" [[ -n "$t_ocid" ]] || { echo "No se encontró 'tenancy' bajo el perfil [$OCI_PROFILE] en $CONFIG_FILE" >&2; return 1; } [[ -n "$reg" ]] || { echo "No se encontró 'region' bajo el perfil [$OCI_PROFILE] en $CONFIG_FILE" >&2; return 1; } if [[ -n "$key_file" && ! -f "$key_file" ]]; then echo "El archivo key_file configurado ($key_file) no existe." >&2 return 1 fi echo "$t_ocid" > "$TMP_DIR/tenancy_ocid" echo "$reg" > "$TMP_DIR/region" } resolve_step_regions(){ local t_ocid; t_ocid=$(cat "$TMP_DIR/tenancy_ocid") local t_name t_name=$(oci_json iam tenancy get --tenancy-id "$t_ocid" 2>/dev/null | jq -r '.data.name // empty' 2>/dev/null | tr -d '\r\n') echo "${t_name:-}" > "$TMP_DIR/tenancy_name" local c_reg; c_reg=$(cat "$TMP_DIR/region") local home_raw home_raw=$(oci --config-file "$CONFIG_FILE" --profile "$OCI_PROFILE" iam region-subscription list --output json) || return 1 local home_reg; home_reg=$(jq -r '.data[]? | select(."is-home-region" == true) | ."region-name" // empty' <<< "$home_raw" | tr -d '\r') echo "${home_reg:-$c_reg}" > "$TMP_DIR/home_region" jq -r '.data[]?."region-name" // empty' <<< "$home_raw" | tr -d '\r' > "$TMP_DIR/subscribed_regions.txt" } resolve_step_ads(){ local t_ocid; t_ocid=$(cat "$TMP_DIR/tenancy_ocid") local ad_raw ad_raw=$(oci --config-file "$CONFIG_FILE" --profile "$OCI_PROFILE" iam availability-domain list --compartment-id "$t_ocid" --output json) || return 1 jq -r '.data[]?.name // empty' <<< "$ad_raw" | tr -d '\r' > "$TMP_DIR/ads.txt" [[ -s "$TMP_DIR/ads.txt" ]] || { echo "No se obtuvieron Availability Domains para el tenancy." >&2; return 1; } } resolve_step_compartments(){ local t_ocid; t_ocid=$(cat "$TMP_DIR/tenancy_ocid") echo "$t_ocid" > "$TMP_DIR/compartments.txt" local comp_raw comp_raw=$(oci_json iam compartment list --compartment-id "$t_ocid" --compartment-id-in-subtree true --all --lifecycle-state ACTIVE) jq -r '.data[]?.id // empty' <<< "$comp_raw" | tr -d '\r' >> "$TMP_DIR/compartments.txt" awk 'NF' "$TMP_DIR/compartments.txt" | sort -u > "$TMP_DIR/compartments.tmp" && mv "$TMP_DIR/compartments.tmp" "$TMP_DIR/compartments.txt" } # ------------------------------------------------------------------------------ # Funciones del Paso 2 (Escaneo de Recursos y Blindaje PAYG) # ------------------------------------------------------------------------------ scan_compute(){ local all="[]" > "$TMP_DIR/amd_details.txt" > "$TMP_DIR/arm_details.txt" > "$TMP_DIR/paid_compute_details.txt" while IFS= read -r c; do [[ -z "$c" ]] && continue local raw; raw=$(oci_json compute instance list --compartment-id "$c" --all) local data; data=$(jq -c '.data // []' <<< "$raw") [[ -z "$data" ]] && data="[]" all=$(jq -c -s '.[0] + .[1]' <(printf '%s\n' "$all") <(printf '%s\n' "$data")) done < "$TMP_DIR/compartments.txt" printf '%s\n' "$all" > "$TMP_DIR/instances.json" while IFS= read -r inst; do [[ -z "$inst" || "$inst" == "null" ]] && continue local nm; nm=$(jq -r '."display-name" // "Instance"' <<< "$inst") local sh; sh=$(jq -r '.shape // "Unknown"' <<< "$inst") local st; st=$(jq -r '."lifecycle-state" // "UNKNOWN"' <<< "$inst") local oc; oc=$(jq -r '."shape-config".ocpus // 0' <<< "$inst") local rm; rm=$(jq -r '."shape-config"."memory-in-gbs" // 0' <<< "$inst") local ad; ad=$(jq -r '."availability-domain" // ""' <<< "$inst") local ad_short="${ad##*:}" local line="${nm} (${sh} · ${oc} OCPU / ${rm} GB · ${ad_short:+$ad_short · }${st})" if [[ "$sh" == "VM.Standard.E2.1.Micro" ]]; then echo "$line" >> "$TMP_DIR/amd_details.txt" elif [[ "$sh" == "VM.Standard.A1.Flex" ]]; then echo "$line" >> "$TMP_DIR/arm_details.txt" else echo "$line" >> "$TMP_DIR/paid_compute_details.txt" fi done < <(jq -c '.[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$all") } scan_storage(){ local total_bv=0 local total_blk=0 local orphan_vols=0 local high_vpu_vols=0 local auto_bkp_vols=0 > "$TMP_DIR/volumes_list.txt" > "$TMP_DIR/orphan_volumes.txt" > "$TMP_DIR/backups_list.txt" local attached_bv_ids="" local attached_blk_ids="" while IFS= read -r c; do [[ -z "$c" ]] && continue local va_raw; va_raw=$(oci_json compute volume-attachment list --compartment-id "$c" --all) local va_ids; va_ids=$(jq -r '.data[]? | select(."lifecycle-state" == "ATTACHED" or ."lifecycle-state" == "ATTACHING") | ."volume-id" // empty' <<< "$va_raw") [[ -n "$va_ids" ]] && attached_blk_ids+="$va_ids"$'\n' while IFS= read -r ad; do [[ -z "$ad" ]] && continue local bva_raw; bva_raw=$(oci_json compute boot-volume-attachment list --compartment-id "$c" --availability-domain "$ad" --all) local bva_ids; bva_ids=$(jq -r '.data[]? | select(."lifecycle-state" == "ATTACHED" or ."lifecycle-state" == "ATTACHING") | ."boot-volume-id" // empty' <<< "$bva_raw") [[ -n "$bva_ids" ]] && attached_bv_ids+="$bva_ids"$'\n' local bv_raw; bv_raw=$(oci_json bv boot-volume list --compartment-id "$c" --availability-domain "$ad" --all) while IFS= read -r item; do [[ -z "$item" || "$item" == "null" ]] && continue local vid; vid=$(jq -r '.id // empty' <<< "$item") local nm; nm=$(jq -r '."display-name" // "BootVolume"' <<< "$item") local sz; sz=$(jq -r '(."size-in-gbs" // 0) | floor' <<< "$item") local vpu; vpu=$(jq -r '(."vpus-per-gb" // 10) | floor' <<< "$item") sz=${sz:-0}; vpu=${vpu:-10} total_bv=$((total_bv + sz)) (( vpu > 20 )) && high_vpu_vols=$((high_vpu_vols + 1)) local pol_tag="" if [[ -n "$vid" ]]; then local pol_raw; pol_raw=$(oci_json bv volume-backup-policy-assignment get-volume-backup-policy-asset-assignment --asset-id "$vid") local pol_id; pol_id=$(jq -r '.data[0]?."policy-id" // empty' <<< "$pol_raw") if [[ -n "$pol_id" ]]; then auto_bkp_vols=$((auto_bkp_vols + 1)) pol_tag=" · Auto-Backup ON" fi fi local att_tag="" if [[ -n "$vid" ]] && ! grep -Fxq "$vid" <<< "$attached_bv_ids"; then orphan_vols=$((orphan_vols + 1)) att_tag=" · UNATTACHED!" echo "[Boot] $nm (${sz} GB en ${ad##*:})" >> "$TMP_DIR/orphan_volumes.txt" fi echo "[Boot] $nm (${sz} GB · ${vpu} VPU · ${ad##*:}${pol_tag}${att_tag})" >> "$TMP_DIR/volumes_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$bv_raw") done < "$TMP_DIR/ads.txt" local blk_raw; blk_raw=$(oci_json bv volume list --compartment-id "$c" --all) while IFS= read -r item; do [[ -z "$item" || "$item" == "null" ]] && continue local vid; vid=$(jq -r '.id // empty' <<< "$item") local nm; nm=$(jq -r '."display-name" // "BlockVolume"' <<< "$item") local sz; sz=$(jq -r '(."size-in-gbs" // 0) | floor' <<< "$item") local vpu; vpu=$(jq -r '(."vpus-per-gb" // 10) | floor' <<< "$item") local ad; ad=$(jq -r '."availability-domain" // "AD"' <<< "$item") sz=${sz:-0}; vpu=${vpu:-10} total_blk=$((total_blk + sz)) (( vpu > 20 )) && high_vpu_vols=$((high_vpu_vols + 1)) local pol_tag="" if [[ -n "$vid" ]]; then local pol_raw; pol_raw=$(oci_json bv volume-backup-policy-assignment get-volume-backup-policy-asset-assignment --asset-id "$vid") local pol_id; pol_id=$(jq -r '.data[0]?."policy-id" // empty' <<< "$pol_raw") if [[ -n "$pol_id" ]]; then auto_bkp_vols=$((auto_bkp_vols + 1)) pol_tag=" · Auto-Backup ON" fi fi local att_tag="" if [[ -n "$vid" ]] && ! grep -Fxq "$vid" <<< "$attached_blk_ids"; then orphan_vols=$((orphan_vols + 1)) att_tag=" · UNATTACHED!" echo "[Block] $nm (${sz} GB en ${ad##*:})" >> "$TMP_DIR/orphan_volumes.txt" fi echo "[Block] $nm (${sz} GB · ${vpu} VPU · ${ad##*:}${pol_tag}${att_tag})" >> "$TMP_DIR/volumes_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$blk_raw") done < "$TMP_DIR/compartments.txt" echo "$total_bv" > "$TMP_DIR/boot_volume_gb.txt" echo "$total_blk" > "$TMP_DIR/block_volume_gb.txt" echo "$orphan_vols" > "$TMP_DIR/orphan_vols_count.txt" echo "$high_vpu_vols" > "$TMP_DIR/high_vpu_count.txt" echo "$auto_bkp_vols" > "$TMP_DIR/auto_bkp_vols_count.txt" local total_bks=0 local total_bks_gb=0 while IFS= read -r c; do [[ -z "$c" ]] && continue local r1; r1=$(oci_json bv backup list --compartment-id "$c" --all) while IFS= read -r bitem; do [[ -z "$bitem" || "$bitem" == "null" ]] && continue total_bks=$((total_bks + 1)) local bnm; bnm=$(jq -r '."display-name" // "VolumeBackup"' <<< "$bitem") local bsz; bsz=$(jq -r '(."unique-size-in-gbs" // ."size-in-gbs" // 0) | floor' <<< "$bitem") local btp; btp=$(jq -r '.type // "BACKUP"' <<< "$bitem") local bst; bst=$(jq -r '."lifecycle-state" // "AVAILABLE"' <<< "$bitem") bsz=${bsz:-0} total_bks_gb=$((total_bks_gb + bsz)) echo "[Block Bkp] $bnm (${bsz} GB · ${btp} · ${bst})" >> "$TMP_DIR/backups_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and ."lifecycle-state" != "FAULTY")' <<< "$r1") local r2; r2=$(oci_json bv boot-volume-backup list --compartment-id "$c" --all) while IFS= read -r bitem; do [[ -z "$bitem" || "$bitem" == "null" ]] && continue total_bks=$((total_bks + 1)) local bnm; bnm=$(jq -r '."display-name" // "BootVolumeBackup"' <<< "$bitem") local bsz; bsz=$(jq -r '(."unique-size-in-gbs" // ."size-in-gbs" // 0) | floor' <<< "$bitem") local btp; btp=$(jq -r '.type // "BACKUP"' <<< "$bitem") local bst; bst=$(jq -r '."lifecycle-state" // "AVAILABLE"' <<< "$bitem") bsz=${bsz:-0} total_bks_gb=$((total_bks_gb + bsz)) echo "[Boot Bkp] $bnm (${bsz} GB · ${btp} · ${bst})" >> "$TMP_DIR/backups_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and ."lifecycle-state" != "FAULTY")' <<< "$r2") done < "$TMP_DIR/compartments.txt" echo "$total_bks" > "$TMP_DIR/backups_count.txt" echo "$total_bks_gb" > "$TMP_DIR/backups_gb.txt" } scan_object_storage(){ local ns_raw; ns_raw=$(oci_json os ns get) local ns; ns=$(jq -r '.data // empty' <<< "$ns_raw" | tr -d '\r') local total_bytes=0; local b_count=0 > "$TMP_DIR/buckets_list.txt" if [[ -n "$ns" ]]; then while IFS= read -r c; do [[ -z "$c" ]] && continue local raw; raw=$(oci_json os bucket list --compartment-id "$c" --namespace-name "$ns" --all) while IFS= read -r bn; do [[ -z "$bn" ]] && continue b_count=$((b_count + 1)) local inf; inf=$(oci_json os bucket get --namespace-name "$ns" --bucket-name "$bn" --fields approximateSize --fields approximateCount) local b; b=$(jq -r '.data."approximate-size" // 0' <<< "$inf") local cnt; cnt=$(jq -r '.data."approximate-count" // 0' <<< "$inf") local tier; tier=$(jq -r '.data."storage-tier" // "Standard"' <<< "$inf") b=${b:-0}; cnt=${cnt:-0} total_bytes=$((total_bytes + b)) local bgb; bgb=$(awk -v bytes="$b" 'BEGIN {printf "%.2f", bytes / (1024*1024*1024)}') echo "$bn (${bgb} GB · ${cnt} objetos · ${tier})" >> "$TMP_DIR/buckets_list.txt" done < <(jq -r '.data[]?.name // empty' <<< "$raw") done < "$TMP_DIR/compartments.txt" fi echo "$b_count" > "$TMP_DIR/buckets_count.txt" echo "$total_bytes" > "$TMP_DIR/buckets_bytes.txt" } scan_databases(){ local free_adb=0; local paid_adb=0; local mysql_free=0; local mysql_paid=0; local nosql=0 > "$TMP_DIR/adb_list.txt" > "$TMP_DIR/mysql_list.txt" > "$TMP_DIR/nosql_list.txt" while IFS= read -r c; do [[ -z "$c" ]] && continue local r_adb; r_adb=$(oci_json db autonomous-database list --compartment-id "$c" --all) while IFS= read -r item; do [[ -z "$item" || "$item" == "null" ]] && continue local nm; nm=$(jq -r '."display-name" // ."db-name" // "ADB"' <<< "$item") local st; st=$(jq -r '."lifecycle-state" // "AVAILABLE"' <<< "$item") local free; free=$(jq -r '."is-free-tier" // false' <<< "$item") local tbs; tbs=$(jq -r '."data-storage-size-in-gbs" // 20' <<< "$item") if [[ "$free" == "true" ]]; then free_adb=$((free_adb + 1)) echo "$nm (Always Free · ${tbs} GB · ${st})" >> "$TMP_DIR/adb_list.txt" else paid_adb=$((paid_adb + 1)) echo "$nm (PAID ADB! · ${tbs} GB · ${st})" >> "$TMP_DIR/adb_list.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$r_adb") local r_my; r_my=$(oci_json mysql db-system list --compartment-id "$c" --all) while IFS= read -r item; do [[ -z "$item" || "$item" == "null" ]] && continue local nm; nm=$(jq -r '."display-name" // "MySQL"' <<< "$item") local sh; sh=$(jq -r '."shape-name" // "Unknown"' <<< "$item") local st; st=$(jq -r '."lifecycle-state" // "ACTIVE"' <<< "$item") if [[ "$sh" == "MySQL.Free" ]]; then mysql_free=$((mysql_free + 1)) echo "$nm (${sh} · ${st})" >> "$TMP_DIR/mysql_list.txt" else mysql_paid=$((mysql_paid + 1)) echo "$nm (PAID: ${sh} · ${st})" >> "$TMP_DIR/mysql_list.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "DELETING")' <<< "$r_my") local r_no; r_no=$(oci_json nosql table list --compartment-id "$c" --all) while IFS= read -r item; do [[ -z "$item" || "$item" == "null" ]] && continue nosql=$((nosql + 1)) local nm; nm=$(jq -r '.name // "NoSQLTable"' <<< "$item") local st; st=$(jq -r '."lifecycle-state" // "ACTIVE"' <<< "$item") local stg; stg=$(jq -r '."table-limits"."max-storage-in-gbs" // 0' <<< "$item") echo "$nm (${stg} GB max · ${st})" >> "$TMP_DIR/nosql_list.txt" done < <(jq -c '(.data // .) | if type == "array" then . else (.items // []) end | .[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "DELETING")' <<< "$r_no") done < "$TMP_DIR/compartments.txt" echo "$free_adb" > "$TMP_DIR/adb_count.txt" echo "$paid_adb" > "$TMP_DIR/paid_adb_count.txt" echo "$mysql_free" > "$TMP_DIR/mysql_count.txt" echo "$mysql_paid" > "$TMP_DIR/paid_mysql_count.txt" echo "$nosql" > "$TMP_DIR/nosql_count.txt" } scan_networking(){ local vcns=0; local lbs=0; local paid_lbs=0; local nlbs=0; local res_ips=0; local unattached=0; local eph_ips=0 > "$TMP_DIR/vcns_list.txt" > "$TMP_DIR/res_ips_list.txt" > "$TMP_DIR/unattached_ips.txt" > "$TMP_DIR/ephemeral_ips.txt" > "$TMP_DIR/lb_details.txt" > "$TMP_DIR/nlb_details.txt" while IFS= read -r c; do [[ -z "$c" ]] && continue local r_vcn; r_vcn=$(oci_json network vcn list --compartment-id "$c" --all) while IFS= read -r vcn; do [[ -z "$vcn" || "$vcn" == "null" ]] && continue vcns=$((vcns + 1)) local vnm; vnm=$(jq -r '."display-name" // "VCN"' <<< "$vcn") local vcidr; vcidr=$(jq -r '(."cidr-blocks"[0] // ."cidr-block" // "—")' <<< "$vcn") local vst; vst=$(jq -r '."lifecycle-state" // "AVAILABLE"' <<< "$vcn") echo "$vnm (${vcidr} · ${vst})" >> "$TMP_DIR/vcns_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$r_vcn") local r_lb; r_lb=$(oci_json lb load-balancer list --compartment-id "$c" --all) while IFS= read -r lb; do [[ -z "$lb" || "$lb" == "null" ]] && continue lbs=$((lbs + 1)) local lbnm; lbnm=$(jq -r '."display-name" // "LoadBalancer"' <<< "$lb") local lbsh; lbsh=$(jq -r '."shape-name" // "unknown"' <<< "$lb") local max_bw; max_bw=$(jq -r '."shape-details"."maximum-bandwidth-in-mbps" // 0' <<< "$lb") local lbst; lbst=$(jq -r '."lifecycle-state" // "ACTIVE"' <<< "$lb") max_bw=${max_bw:-0} if [[ "$lbsh" != "flexible" && "$lbsh" != "10Mbps-Micro" ]] || (( max_bw > LIMIT_LB_MAX_MBPS )); then paid_lbs=$((paid_lbs + 1)) echo "$lbnm (${lbsh} · ${max_bw} Mbps > ${LIMIT_LB_MAX_MBPS} Mbps Free! · ${lbst})" >> "$TMP_DIR/lb_details.txt" else echo "$lbnm (${lbsh} · ${max_bw} Mbps · ${lbst})" >> "$TMP_DIR/lb_details.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "DELETING")' <<< "$r_lb") local r_nlb; r_nlb=$(oci_json nlb network-load-balancer list --compartment-id "$c" --all) while IFS= read -r nlb; do [[ -z "$nlb" || "$nlb" == "null" ]] && continue nlbs=$((nlbs + 1)) local nlbnm; nlbnm=$(jq -r '."display-name" // "NetworkLoadBalancer"' <<< "$nlb") local nlbst; nlbst=$(jq -r '."lifecycle-state" // "ACTIVE"' <<< "$nlb") echo "$nlbnm (${nlbst})" >> "$TMP_DIR/nlb_details.txt" done < <(jq -c '(.data // .) | if type == "array" then . else (.items // []) end | .[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "DELETING")' <<< "$r_nlb") local r_pip; r_pip=$(oci_json network public-ip list --scope REGION --compartment-id "$c" --all --lifetime RESERVED) while IFS= read -r ip; do [[ -z "$ip" || "$ip" == "null" ]] && continue res_ips=$((res_ips + 1)) local ass; ass=$(jq -r '."assigned-entity-id" // empty' <<< "$ip") local ipaddr; ipaddr=$(jq -r '."ip-address" // "unknown"' <<< "$ip") local ipnm; ipnm=$(jq -r '."display-name" // "ReservedIP"' <<< "$ip") local ipst; ipst=$(jq -r '."lifecycle-state" // ""' <<< "$ip") if [[ -z "$ass" && "$ipst" != "ASSIGNED" ]]; then unattached=$((unattached + 1)) echo "$ipaddr ($ipnm · Unattached)" >> "$TMP_DIR/unattached_ips.txt" echo "$ipaddr ($ipnm · UNATTACHED!)" >> "$TMP_DIR/res_ips_list.txt" else echo "$ipaddr ($ipnm · Assigned)" >> "$TMP_DIR/res_ips_list.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$r_pip") while IFS= read -r ad; do [[ -z "$ad" ]] && continue local r_eip; r_eip=$(oci_json network public-ip list --scope AVAILABILITY_DOMAIN --availability-domain "$ad" --compartment-id "$c" --all --lifetime EPHEMERAL) while IFS= read -r eip; do [[ -z "$eip" || "$eip" == "null" ]] && continue eph_ips=$((eph_ips + 1)) local eaddr; eaddr=$(jq -r '."ip-address" // "unknown"' <<< "$eip") local enm; enm=$(jq -r '."display-name" // empty' <<< "$eip") if [[ -n "$enm" ]]; then echo "$eaddr (${enm} · ${ad##*:})" >> "$TMP_DIR/ephemeral_ips.txt" else echo "$eaddr (${ad##*:})" >> "$TMP_DIR/ephemeral_ips.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING")' <<< "$r_eip") done < "$TMP_DIR/ads.txt" done < "$TMP_DIR/compartments.txt" echo "$vcns" > "$TMP_DIR/vcns_count.txt" echo "$lbs" > "$TMP_DIR/lbs_count.txt" echo "$paid_lbs" > "$TMP_DIR/paid_lbs_count.txt" echo "$nlbs" > "$TMP_DIR/nlbs_count.txt" echo "$res_ips" > "$TMP_DIR/res_ips.txt" echo "$unattached" > "$TMP_DIR/unattached.txt" echo "$eph_ips" > "$TMP_DIR/eph_ips.txt" } scan_egress_cost_and_alerts(){ local tid; tid=$(cat "$TMP_DIR/tenancy_ocid") local s_date; s_date=$(date -u +%Y-%m-01T00:00:00Z) local e_date e_date=$(date -u -d '+1 day' +%Y-%m-%dT00:00:00Z 2>/dev/null \ || date -u -v+1d +%Y-%m-%dT00:00:00Z 2>/dev/null \ || date -u +%Y-%m-%dT00:00:00Z) # 1. Consumo de transferencia de salida (Outbound Data Transfer) local raw_u; raw_u=$(oci_json usage-api usage-summary request-summarized-usages \ --tenant-id "$tid" \ --time-usage-started "$s_date" \ --time-usage-ended "$e_date" \ --granularity "DAILY" \ --query-type "USAGE" \ --group-by '["service", "skuName"]') local gb; gb=$(jq '[.data.items[]? | select((.["sku-name"] // "") | test("Outbound Data Transfer"; "i")) | (.["computed-quantity"] // 0)] | add // 0' <<< "$raw_u") echo "${gb:-0}" > "$TMP_DIR/egress_gb.txt" # 2. Costo monetario real acumulado en el mes (Blindaje PAYG) > "$TMP_DIR/cost_items.txt" local raw_c; raw_c=$(oci_json usage-api usage-summary request-summarized-usages \ --tenant-id "$tid" \ --time-usage-started "$s_date" \ --time-usage-ended "$e_date" \ --granularity "DAILY" \ --query-type "COST" \ --group-by '["service", "skuName"]') local cost; cost=$(jq '[.data.items[]? | (.["computed-amount"] // 0)] | add // 0' <<< "$raw_c") local curr; curr=$(jq -r '[.data.items[]? | select((.["computed-amount"] // 0) > 0) | .currency // empty][0] // "USD"' <<< "$raw_c") [[ -z "$curr" || "$curr" == "null" ]] && curr="USD" while IFS= read -r citem; do [[ -z "$citem" || "$citem" == "null" ]] && continue local svc; svc=$(jq -r '.service // "Service"' <<< "$citem") local sku; sku=$(jq -r '.sku // "SKU"' <<< "$citem") local amt; amt=$(jq -r '.amount // 0' <<< "$citem") printf "%s · %s (%.2f %s)\n" "$svc" "$sku" "$amt" "$curr" >> "$TMP_DIR/cost_items.txt" done < <(jq -c '[.data.items[]? | select((.["computed-amount"] // 0) > 0)] | group_by(.service + "|" + .["sku-name"]) | map({service: .[0].service, sku: .[0]["sku-name"], amount: (map(.["computed-amount"] // 0) | add)}) | .[] | select(.amount > 0.001)' <<< "$raw_c") echo "${cost:-0}" > "$TMP_DIR/month_cost.txt" echo "$curr" > "$TMP_DIR/month_currency.txt" # 3. Presupuestos de Facturación (OCI Cost Budgets) local budgets_count=0 > "$TMP_DIR/budgets_list.txt" local raw_b; raw_b=$(oci_json budgets budget list --compartment-id "$tid" --all) if [[ $(jq '.data | length' <<< "$raw_b") -eq 0 ]]; then raw_b=$(oci_json budgets budget budget list --compartment-id "$tid" --all) fi while IFS= read -r bitem; do [[ -z "$bitem" || "$bitem" == "null" ]] && continue budgets_count=$((budgets_count + 1)) local bnm; bnm=$(jq -r '."display-name" // "Budget"' <<< "$bitem") local bamt; bamt=$(jq -r '.amount // 0' <<< "$bitem") local bact; bact=$(jq -r '."actual-spend" // 0' <<< "$bitem") local balr; balr=$(jq -r '."alert-rule-count" // 0' <<< "$bitem") local bst; bst=$(jq -r '."lifecycle-state" // "ACTIVE"' <<< "$bitem") local bfmt; bfmt=$(awk -v a="${bamt:-0}" -v s="${bact:-0}" 'BEGIN {printf "Limit: $%.2f · Spend: $%.2f", a, s}') echo "$bnm (${bfmt} · ${balr} alert rule(s) · ${bst})" >> "$TMP_DIR/budgets_list.txt" done < <(jq -c '.data[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "INACTIVE")' <<< "$raw_b") echo "$budgets_count" > "$TMP_DIR/budgets_count.txt" # 4. Alarmas de Monitoreo (OCI Monitoring Alarms - ej. tráfico saliente VnicToNetworkBytes) local alarms_count=0 local firing_alarms=0 > "$TMP_DIR/alarms_list.txt" while IFS= read -r c; do [[ -z "$c" ]] && continue local st_raw; st_raw=$(oci_json monitoring alarm-status list-alarms-status --compartment-id "$c" --all) local al_raw; al_raw=$(oci_json monitoring alarm list --compartment-id "$c" --all) while IFS= read -r al; do [[ -z "$al" || "$al" == "null" ]] && continue alarms_count=$((alarms_count + 1)) local aid; aid=$(jq -r '.id // empty' <<< "$al") local anm; anm=$(jq -r '."display-name" // "Alarm"' <<< "$al") local asev; asev=$(jq -r '.severity // "INFO"' <<< "$al") local aen; aen=$(jq -r '."is-enabled" // true' <<< "$al") local runtime_st="OK" if [[ -n "$aid" ]]; then local found_st; found_st=$(jq -r --arg id "$aid" '.data[]? | select(.id == $id) | .status // empty' <<< "$st_raw" | head -1) [[ -n "$found_st" ]] && runtime_st="$found_st" fi [[ "$aen" != "true" ]] && runtime_st="DISABLED" [[ "$runtime_st" == "FIRING" ]] && firing_alarms=$((firing_alarms + 1)) local aquery="" if [[ -n "$aid" ]]; then local aget; aget=$(oci_json monitoring alarm get --alarm-id "$aid") aquery=$(jq -r '.data.query // empty' <<< "$aget" | tr -d '\r\n') fi if [[ -n "$aquery" ]]; then echo "$anm (${asev} · ${runtime_st} · ${aquery})" >> "$TMP_DIR/alarms_list.txt" else echo "$anm (${asev} · ${runtime_st})" >> "$TMP_DIR/alarms_list.txt" fi done < <(jq -c '.data[]? | select(."lifecycle-state" != "DELETED" and ."lifecycle-state" != "DELETING")' <<< "$al_raw") done < "$TMP_DIR/compartments.txt" echo "$alarms_count" > "$TMP_DIR/alarms_count.txt" echo "$firing_alarms" > "$TMP_DIR/firing_alarms_count.txt" } # ------------------------------------------------------------------------------ # Flujo de ejecución # ------------------------------------------------------------------------------ clear 2>/dev/null || true echo printf " ${D}Engineered by ${R}${B}D4niel${R} ${GRY}·${R} ${CYN}https://d4niel.dev${R}\n" hr printf " %b ${B}${WHT}Paso 1: Resolviendo Tenancy, Región y Compartimentos${R}\n" "$ARR" run_resolve_step "Validando configuración local de OCI ($CONFIG_FILE)..." resolve_step_config \ || die "No se pudo leer la configuración de OCI CLI." TENANCY_OCID=$(cat "$TMP_DIR/tenancy_ocid") CURRENT_REGION=$(cat "$TMP_DIR/region") printf " %b ${GRY}Tenancy OCID:${R} ${WHT}%s${R}\n" "$CHK" "$TENANCY_OCID" run_resolve_step "Consultando suscripción de regiones (Home Region)..." resolve_step_regions \ || die "Falló la autenticación con Oracle Cloud. Verifica tu API Key y permisos IAM." HOME_REGION=$(cat "$TMP_DIR/home_region") SUB_REGIONS_COUNT=$(wc -l < "$TMP_DIR/subscribed_regions.txt" | tr -d ' ') SUB_REGIONS_LIST=$(paste -sd ', ' "$TMP_DIR/subscribed_regions.txt") HOME_LABEL="(Home)" HOME_COLOR="$CYN" if [[ "$CURRENT_REGION" != "$HOME_REGION" ]]; then HOME_LABEL="(! Home is $HOME_REGION)" HOME_COLOR="$RED" fi if (( SUB_REGIONS_COUNT > 1 )); then printf " %b ${GRY}Home Region:${R} ${WHT}%s${R} ${YEL}(%s regiones suscritas: %s)${R}\n" "$WRN" "$HOME_REGION" "$SUB_REGIONS_COUNT" "$SUB_REGIONS_LIST" else printf " %b ${GRY}Home Region:${R} ${WHT}%s${R}\n" "$CHK" "$HOME_REGION" fi if [[ "$CURRENT_REGION" == "$HOME_REGION" ]]; then printf " %b ${GRY}Active Region:${R} ${WHT}%s${R} %b%s${R}\n" "$CHK" "$CURRENT_REGION" "$HOME_COLOR" "$HOME_LABEL" else printf " %b ${GRY}Active Region:${R} ${WHT}%s${R} %b%s${R}\n" "$WRN" "$CURRENT_REGION" "$HOME_COLOR" "$HOME_LABEL" fi run_resolve_step "Descubriendo Availability Domains en ${CURRENT_REGION}..." resolve_step_ads \ || die "No se pudieron listar los Availability Domains." AD_COUNT=$(wc -l < "$TMP_DIR/ads.txt" | tr -d ' ') printf " %b ${GRY}Availability Domains:${R} ${WHT}%s encontrados${R}\n" "$CHK" "$AD_COUNT" run_resolve_step "Listando compartimentos activos en el Tenancy..." resolve_step_compartments \ || die "No se pudieron listar los compartimentos." COMP_COUNT=$(wc -l < "$TMP_DIR/compartments.txt" | tr -d ' ') printf " %b ${GRY}Compartimentos:${R} ${WHT}Escaneando %s compartimento(s)${R}\n" "$CHK" "$COMP_COUNT" echo printf " %b ${B}${WHT}Paso 2: Recolectando recursos de Oracle Cloud${R}\n" "$ARR" run_step "Escaneando instancias de cómputo (AMD & ARM)" scan_compute run_step "Auditando volúmenes Block, Boot, Pólizas y Backups" scan_storage run_step "Verificando buckets de Object Storage" scan_object_storage run_step "Consultando bases de datos (ADB, MySQL, NoSQL)" scan_databases run_step "Auditando redes virtuales, Load Balancers e IPs" scan_networking run_step "Calculando transferencia, costos, Budgets y Alarmas" scan_egress_cost_and_alerts TENANCY_NAME=$(cat "$TMP_DIR/tenancy_name" 2>/dev/null || echo "") TENANCY_DISPLAY=$(format_tenancy_display "$TENANCY_OCID" "$TENANCY_NAME") banner # ------------------------------------------------------------------------------ # Impresión de Resultados y Validación PAYG / Always Free # ------------------------------------------------------------------------------ declare -a VIOLATIONS=() declare -a WARNINGS=() if [[ "$CURRENT_REGION" != "$HOME_REGION" ]]; then VIOLATIONS+=("Active region ($CURRENT_REGION) is NOT your Home Region ($HOME_REGION) — Always Free resources outside Home Region are billed!") fi if (( SUB_REGIONS_COUNT > 1 )); then WARNINGS+=("Multiple regions subscribed ($SUB_REGIONS_LIST) — remember Always Free applies ONLY in $HOME_REGION") fi # 1. Cómputo INST_JSON=$(cat "$TMP_DIR/instances.json") AMD_COUNT=$(jq '[.[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and .shape == "VM.Standard.E2.1.Micro")] | length' <<< "$INST_JSON") ARM_OCPUS=$(jq '[.[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and .shape == "VM.Standard.A1.Flex") | (."shape-config".ocpus // 0)] | add // 0' <<< "$INST_JSON") ARM_RAM=$(jq '[.[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and .shape == "VM.Standard.A1.Flex") | (."shape-config"."memory-in-gbs" // 0)] | add // 0' <<< "$INST_JSON") PAID_INSTANCES=$(jq '[.[]? | select(."lifecycle-state" != "TERMINATED" and ."lifecycle-state" != "TERMINATING" and .shape != "VM.Standard.E2.1.Micro" and .shape != "VM.Standard.A1.Flex")] | length' <<< "$INST_JSON") echo " ${B}${WHT}Compute Resources${R}" if (( PAID_INSTANCES > 0 )); then print_metric_row "$CRS" "Paid Instances" "$PAID_INSTANCES detected" "(Non-Free shapes!)" print_subitems "$TMP_DIR/paid_compute_details.txt" "$RED" VIOLATIONS+=("Non-Free Compute Shapes detected ($PAID_INSTANCES active)") fi if (( AMD_COUNT > LIMIT_AMD_MICRO_INSTANCES )); then print_metric_row "$CRS" "AMD Micro (x86)" "$AMD_COUNT / $LIMIT_AMD_MICRO_INSTANCES" "(Exceeded!)" VIOLATIONS+=("AMD Micro instances exceeded ($AMD_COUNT > $LIMIT_AMD_MICRO_INSTANCES)") else print_metric_row "$CHK" "AMD Micro (x86)" "$AMD_COUNT / $LIMIT_AMD_MICRO_INSTANCES" "(E2.1.Micro)" fi print_subitems "$TMP_DIR/amd_details.txt" "$GRY" if (( $(awk -v u="$ARM_OCPUS" -v l="$LIMIT_ARM_OCPUS" 'BEGIN {print (u > l)}') )); then print_metric_row "$CRS" "Ampere ARM OCPUs" "$(printf '%.1f / %.1f' "$ARM_OCPUS" "$LIMIT_ARM_OCPUS")" "(Exceeded!)" VIOLATIONS+=("Ampere ARM OCPUs exceeded ($ARM_OCPUS > $LIMIT_ARM_OCPUS)") else print_metric_row "$CHK" "Ampere ARM OCPUs" "$(printf '%.1f / %.1f' "$ARM_OCPUS" "$LIMIT_ARM_OCPUS")" "(A1.Flex)" fi if (( $(awk -v u="$ARM_RAM" -v l="$LIMIT_ARM_RAM_GB" 'BEGIN {print (u > l)}') )); then print_metric_row "$CRS" "Ampere ARM Memory" "$(printf '%.1f / %.1f GB' "$ARM_RAM" "$LIMIT_ARM_RAM_GB")" "(Exceeded!)" VIOLATIONS+=("Ampere ARM Memory exceeded (${ARM_RAM}GB > ${LIMIT_ARM_RAM_GB}GB)") else print_metric_row "$CHK" "Ampere ARM Memory" "$(printf '%.1f / %.1f GB' "$ARM_RAM" "$LIMIT_ARM_RAM_GB")" "(A1.Flex)" fi print_subitems "$TMP_DIR/arm_details.txt" "$GRY" 0 echo # 2. Almacenamiento (Block & Boot Volumes + Backups + Object Storage) BV_GB=$(cat "$TMP_DIR/boot_volume_gb.txt") BLK_GB=$(cat "$TMP_DIR/block_volume_gb.txt") ORPHAN_VOLS=$(cat "$TMP_DIR/orphan_vols_count.txt") HIGH_VPU_VOLS=$(cat "$TMP_DIR/high_vpu_count.txt") AUTO_BKP_VOLS=$(cat "$TMP_DIR/auto_bkp_vols_count.txt") TOT_STG=$((BV_GB + BLK_GB)) BKPS=$(cat "$TMP_DIR/backups_count.txt") BKPS_GB=$(cat "$TMP_DIR/backups_gb.txt") BUCKETS=$(cat "$TMP_DIR/buckets_count.txt") B_BYTES=$(cat "$TMP_DIR/buckets_bytes.txt") B_GB=$(awk -v b="${B_BYTES:-0}" 'BEGIN {printf "%.2f", b / (1024*1024*1024)}') STG_PCT=$(( TOT_STG * 100 / LIMIT_STORAGE_GB )) echo " ${B}${WHT}Block & Boot Storage${R}" if (( TOT_STG > LIMIT_STORAGE_GB )); then print_metric_row "$CRS" "Total Volume Storage" "$TOT_STG / $LIMIT_STORAGE_GB GB" "(Over capacity!)" VIOLATIONS+=("Total Block/Boot Storage exceeded (${TOT_STG}GB > ${LIMIT_STORAGE_GB}GB)") else print_metric_row "$CHK" "Total Volume Storage" "$TOT_STG / $LIMIT_STORAGE_GB GB" "(${STG_PCT}% · Boot: ${BV_GB}G, Block: ${BLK_GB}G)" fi print_subitems "$TMP_DIR/volumes_list.txt" "$GRY" 1 if (( ORPHAN_VOLS > 0 )); then WARNINGS+=("$ORPHAN_VOLS unattached volume(s) consuming storage quota") fi if (( AUTO_BKP_VOLS > 0 )); then WARNINGS+=("$AUTO_BKP_VOLS volume(s) have Automatic Backup Policy enabled — watch the 5 backup free limit") fi if (( HIGH_VPU_VOLS > 0 )); then print_metric_row "$WRN" "High-VPU Volumes" "$HIGH_VPU_VOLS volume(s)" "(>20 VPUs may incur fees on PAYG)" VIOLATIONS+=("Volume(s) configured with >20 VPUs/GB ($HIGH_VPU_VOLS detected)") fi if (( BKPS > LIMIT_VOLUME_BACKUPS )); then print_metric_row "$CRS" "Volume Backups" "$BKPS / $LIMIT_VOLUME_BACKUPS" "(Exceeded! ~${BKPS_GB} GB)" VIOLATIONS+=("Volume Backups exceeded ($BKPS > $LIMIT_VOLUME_BACKUPS)") else print_metric_row "$CHK" "Volume Backups" "$BKPS / $LIMIT_VOLUME_BACKUPS" "backups (~${BKPS_GB} GB)" fi print_subitems "$TMP_DIR/backups_list.txt" "$GRY" 1 if (( $(awk -v u="$B_GB" -v l="$LIMIT_OBJECT_STORAGE_GB" 'BEGIN {print (u > l)}') )); then print_metric_row "$CRS" "Object Storage" "$B_GB / $LIMIT_OBJECT_STORAGE_GB GB" "(Exceeded! $BUCKETS buckets)" VIOLATIONS+=("Object Storage exceeded (${B_GB}GB > ${LIMIT_OBJECT_STORAGE_GB}GB)") else print_metric_row "$CHK" "Object Storage" "$B_GB / $LIMIT_OBJECT_STORAGE_GB GB" "($BUCKETS buckets)" fi print_subitems "$TMP_DIR/buckets_list.txt" "$GRY" 0 echo # 3. Redes, Load Balancers e IPs VCNS=$(cat "$TMP_DIR/vcns_count.txt") LBS=$(cat "$TMP_DIR/lbs_count.txt") PAID_LBS=$(cat "$TMP_DIR/paid_lbs_count.txt") NLBS=$(cat "$TMP_DIR/nlbs_count.txt") RES_IPS=$(cat "$TMP_DIR/res_ips.txt") UNATT_IPS=$(cat "$TMP_DIR/unattached.txt") EPH_IPS=$(cat "$TMP_DIR/eph_ips.txt") echo " ${B}${WHT}Networking & Public IPs${R}" if (( VCNS > LIMIT_VCNS )); then print_metric_row "$CRS" "Virtual Cloud Networks" "$VCNS / $LIMIT_VCNS" "(Exceeded!)" VIOLATIONS+=("VCN count exceeded ($VCNS > $LIMIT_VCNS)") else print_metric_row "$CHK" "Virtual Cloud Networks" "$VCNS / $LIMIT_VCNS" "VCNs" fi print_subitems "$TMP_DIR/vcns_list.txt" "$GRY" 1 if (( RES_IPS > LIMIT_RESERVED_IPS )); then print_metric_row "$CRS" "Reserved Public IPs" "$RES_IPS / $LIMIT_RESERVED_IPS" "(Exceeded!)" VIOLATIONS+=("Reserved Public IPs exceeded ($RES_IPS > $LIMIT_RESERVED_IPS)") else print_metric_row "$CHK" "Reserved Public IPs" "$RES_IPS / $LIMIT_RESERVED_IPS" "reserved" fi print_subitems "$TMP_DIR/res_ips_list.txt" "$GRY" 1 if (( UNATT_IPS > 0 )); then print_metric_row "$CRS" "Idle Reserved IPs" "$UNATT_IPS idle" "(HOURLY CHARGES!)" print_subitems "$TMP_DIR/unattached_ips.txt" "$RED" 1 VIOLATIONS+=("Idle reserved public IP detected ($UNATT_IPS unattached — incurs hourly billing)") else print_metric_row "$CHK" "Idle Reserved IPs" "0 idle" "(no extra fees)" fi print_metric_row "$INF" "Ephemeral IPs (VNICs)" "$EPH_IPS active" "" print_subitems "$TMP_DIR/ephemeral_ips.txt" "$GRY" 1 if (( LBS > LIMIT_LOAD_BALANCERS || PAID_LBS > 0 )); then print_metric_row "$CRS" "Flexible Load Balancers" "$LBS / $LIMIT_LOAD_BALANCERS" "(Exceeded or >${LIMIT_LB_MAX_MBPS} Mbps!)" print_subitems "$TMP_DIR/lb_details.txt" "$RED" 1 (( LBS > LIMIT_LOAD_BALANCERS )) && VIOLATIONS+=("Flexible Load Balancers exceeded ($LBS > $LIMIT_LOAD_BALANCERS)") (( PAID_LBS > 0 )) && VIOLATIONS+=("Load Balancer configured with paid shape or >${LIMIT_LB_MAX_MBPS} Mbps bandwidth") else print_metric_row "$CHK" "Flexible Load Balancers" "$LBS / $LIMIT_LOAD_BALANCERS" "(<= ${LIMIT_LB_MAX_MBPS} Mbps)" print_subitems "$TMP_DIR/lb_details.txt" "$GRY" 1 fi if (( NLBS > LIMIT_NETWORK_LOAD_BALANCERS )); then print_metric_row "$CRS" "Network Load Balancers" "$NLBS / $LIMIT_NETWORK_LOAD_BALANCERS" "(Exceeded!)" VIOLATIONS+=("Network Load Balancers exceeded ($NLBS > $LIMIT_NETWORK_LOAD_BALANCERS)") else print_metric_row "$CHK" "Network Load Balancers" "$NLBS / $LIMIT_NETWORK_LOAD_BALANCERS" "NLB" fi print_subitems "$TMP_DIR/nlb_details.txt" "$GRY" 0 echo # 4. Bases de datos, Transferencia, Facturación, Budgets & Alarmas ADBS=$(cat "$TMP_DIR/adb_count.txt") PAID_ADBS=$(cat "$TMP_DIR/paid_adb_count.txt") MYSQLS=$(cat "$TMP_DIR/mysql_count.txt") PAID_MYSQLS=$(cat "$TMP_DIR/paid_mysql_count.txt") NOSQLS=$(cat "$TMP_DIR/nosql_count.txt") EGRESS_RAW=$(cat "$TMP_DIR/egress_gb.txt") EGRESS_GB=$(awk -v e="${EGRESS_RAW:-0}" 'BEGIN {printf "%.2f", e}') LIMIT_EGRESS_GB=$(( LIMIT_OUTBOUND_DATA_TB * 1024 )) MONTH_COST_RAW=$(cat "$TMP_DIR/month_cost.txt") MONTH_CURR=$(cat "$TMP_DIR/month_currency.txt") MONTH_COST_FMT=$(awk -v c="${MONTH_COST_RAW:-0}" 'BEGIN {printf "%.2f", c}') BUDGETS_CNT=$(cat "$TMP_DIR/budgets_count.txt") ALARMS_CNT=$(cat "$TMP_DIR/alarms_count.txt") FIRING_CNT=$(cat "$TMP_DIR/firing_alarms_count.txt") echo " ${B}${WHT}Databases${R}" if (( ADBS > LIMIT_AUTONOMOUS_DB || PAID_ADBS > 0 )); then print_metric_row "$CRS" "Autonomous Databases" "$ADBS / $LIMIT_AUTONOMOUS_DB" "(+${PAID_ADBS} Paid ADB!)" print_subitems "$TMP_DIR/adb_list.txt" "$RED" 1 (( ADBS > LIMIT_AUTONOMOUS_DB )) && VIOLATIONS+=("Always Free Autonomous Databases exceeded ($ADBS > $LIMIT_AUTONOMOUS_DB)") (( PAID_ADBS > 0 )) && VIOLATIONS+=("Paid Autonomous Database detected ($PAID_ADBS active)") else print_metric_row "$CHK" "Autonomous Databases" "$ADBS / $LIMIT_AUTONOMOUS_DB" "databases" print_subitems "$TMP_DIR/adb_list.txt" "$GRY" 1 fi if (( MYSQLS > LIMIT_MYSQL_DB || PAID_MYSQLS > 0 )); then print_metric_row "$CRS" "MySQL HeatWave DB" "$MYSQLS / $LIMIT_MYSQL_DB" "(+${PAID_MYSQLS} Paid MySQL!)" print_subitems "$TMP_DIR/mysql_list.txt" "$RED" 1 (( MYSQLS > LIMIT_MYSQL_DB )) && VIOLATIONS+=("MySQL Free instances exceeded ($MYSQLS > $LIMIT_MYSQL_DB)") (( PAID_MYSQLS > 0 )) && VIOLATIONS+=("Paid MySQL DB System detected ($PAID_MYSQLS active)") else print_metric_row "$CHK" "MySQL HeatWave DB" "$MYSQLS / $LIMIT_MYSQL_DB" "instances" print_subitems "$TMP_DIR/mysql_list.txt" "$GRY" 1 fi if (( NOSQLS > LIMIT_NOSQL_TABLES )); then print_metric_row "$CRS" "NoSQL Tables" "$NOSQLS / $LIMIT_NOSQL_TABLES" "(Exceeded!)" VIOLATIONS+=("NoSQL Always Free tables exceeded ($NOSQLS > $LIMIT_NOSQL_TABLES)") else print_metric_row "$CHK" "NoSQL Tables" "$NOSQLS / $LIMIT_NOSQL_TABLES" "tables" fi print_subitems "$TMP_DIR/nosql_list.txt" "$GRY" 0 echo # 5. Transferencia, Facturación, Budgets & Alarmas echo " ${B}${WHT}Data Transfer & Billing Shield${R}" if (( $(awk -v u="$EGRESS_GB" -v l="$LIMIT_EGRESS_GB" 'BEGIN {print (u > l)}') )); then print_metric_row "$CRS" "Outbound Egress" "$EGRESS_GB GB / $LIMIT_OUTBOUND_DATA_TB TB" "(Exceeded!)" VIOLATIONS+=("Outbound Data Transfer exceeded (${EGRESS_GB} GB > ${LIMIT_OUTBOUND_DATA_TB} TB)") else print_metric_row "$CHK" "Outbound Egress" "$EGRESS_GB GB / $LIMIT_OUTBOUND_DATA_TB TB" "(Current month)" fi if (( $(awk -v c="${MONTH_COST_RAW:-0}" 'BEGIN {print (c > 0.001)}') )); then print_metric_row "$CRS" "Current Month Spend" "${MONTH_COST_FMT} ${MONTH_CURR}" "(ACTIVE CHARGES!)" print_subitems "$TMP_DIR/cost_items.txt" "$RED" 1 VIOLATIONS+=("Accumulated billing charges detected this month (${MONTH_COST_FMT} ${MONTH_CURR})") else print_metric_row "$CHK" "Current Month Spend" "0.00 ${MONTH_CURR}" "(Billing API verified)" fi if (( BUDGETS_CNT > 0 )); then print_metric_row "$CHK" "OCI Cost Budgets" "$BUDGETS_CNT active" "(Billing protection)" print_subitems "$TMP_DIR/budgets_list.txt" "$GRY" 1 else print_metric_row "$WRN" "OCI Cost Budgets" "0 configured" "(Recommended for PAYG)" WARNINGS+=("No OCI Cost Budget configured — consider setting a \$1.00 budget alert in Billing -> Budgets") fi if (( FIRING_CNT > 0 )); then print_metric_row "$WRN" "Cloud Monitoring Alarms" "$ALARMS_CNT configured" "(${FIRING_CNT} FIRING!)" print_subitems "$TMP_DIR/alarms_list.txt" "$YEL" 0 WARNINGS+=("$FIRING_CNT Monitoring Alarm(s) currently in FIRING state") elif (( ALARMS_CNT > 0 )); then print_metric_row "$CHK" "Cloud Monitoring Alarms" "$ALARMS_CNT active" "(All OK)" print_subitems "$TMP_DIR/alarms_list.txt" "$GRY" 0 else print_metric_row "$WRN" "Cloud Monitoring Alarms" "0 configured" "(No metric alerts)" fi echo # ------------------------------------------------------------------------------ # Resumen Final # ------------------------------------------------------------------------------ hr if [[ ${#VIOLATIONS[@]} -eq 0 ]]; then printf " %b ${B}${GRN}All audited resources are within Always Free tier limits${R} in ${WHT}%s${R}\n" "$CHK" "$CURRENT_REGION" echo " ${GRY}Status:${R} ${GRN}Compliant · Zero unexpected charges expected${R}" if (( ${#WARNINGS[@]} > 0 )); then for w in "${WARNINGS[@]}"; do printf " ${YEL}⚠ %s${R}\n" "$w" done fi else printf " %b ${B}${RED}%d potential billing issue(s) detected${R} in ${WHT}%s${R}\n" "$CRS" "${#VIOLATIONS[@]}" "$CURRENT_REGION" for v in "${VIOLATIONS[@]}"; do printf " ${RED}✖ %s${R}\n" "$v" done if (( ${#WARNINGS[@]} > 0 )); then for w in "${WARNINGS[@]}"; do printf " ${YEL}⚠ %s${R}\n" "$w" done fi echo " ${GRY}Action:${R} ${RED}Review resources in OCI Console to avoid invoices${R}" fi echo printf " ${D}Engineered by D4niel · d4niel.dev${R}\n" echo