Skip to content

Instantly share code, notes, and snippets.

@fahadysf
Last active June 4, 2026 11:50
Show Gist options
  • Select an option

  • Save fahadysf/5c0acae7a7d22916dc6321bea85375fb to your computer and use it in GitHub Desktop.

Select an option

Save fahadysf/5c0acae7a7d22916dc6321bea85375fb to your computer and use it in GitHub Desktop.
OCI Routing Discovery
#!/usr/bin/env bash
# OCI routing discovery — starts from DRG name(s) and walks the entire reachable topology.
#
# Required env:
# DRG_NAMES Comma-separated DRG display names (or DRG_OCIDS)
# DRG_OCIDS Comma-separated DRG OCIDs (or DRG_NAMES)
#
# Optional env:
# OUTPUT_DIR Output directory default: ./oci-discovery-YYYYmmdd-HHMMSS
# OCI_PROFILE OCI CLI profile name default: DEFAULT
# OCI_REGION Region override default: profile region
# COMPARTMENT_OCID Limit scope to a compartment default: tenancy root + subtree
# PARALLEL Max parallel oci calls default: 4
# SKIP_NVA_RESOLVE Skip private-IP -> instance default: 0
# OCI_MAX_RETRIES Retries for 429/5xx/network default: 5 (0 disables)
# OCI_RETRY_BASE_SECONDS Backoff base seconds default: 1 (exponential + jitter)
# OCI_FAIL_FAST Exit on first non-retryable err default: 0 (1 = abort)
#
# Output:
# $OUTPUT_DIR/
# summary.json topology index
# summary.txt human-readable route map
# compartments.json
# drgs/<drg-ocid>/
# drg.json, attachments.json, route-tables.json, route-distributions.json
# drg-rt-<rt-ocid>.rules.json
# drg-rd-<rd-ocid>.statements.json
# vcns/<vcn-ocid>/
# vcn.json, subnets.json, route-tables.json, security-lists.json, nsgs.json
# igws.json, nats.json, sgws.json, lpgs.json
# rt-<rt-ocid>.json (with rules expanded)
# nsg-<nsg-ocid>.rules.json
# vpn/
# ipsec-<ocid>.json, ipsec-<ocid>.tunnels.json, ipsec-<ocid>.tunnel-<tid>.routes.json
# cpe-<ocid>.json
# fastconnect/
# vc-<ocid>.json
# rpc/
# rpc-<ocid>.json
# nvas/
# private-ip-<ocid>.json, vnic-<vnic-ocid>.json, instance-<inst-ocid>.json
# forced-paths.json every route rule whose next-hop is a private IP
#
set -euo pipefail
# --- prereqs ----------------------------------------------------------------
command -v oci >/dev/null || { echo "oci CLI not found" >&2; exit 2; }
command -v jq >/dev/null || { echo "jq not found" >&2; exit 2; }
PROFILE="${OCI_PROFILE:-DEFAULT}"
PARALLEL="${PARALLEL:-4}"
SKIP_NVA_RESOLVE="${SKIP_NVA_RESOLVE:-0}"
OUTPUT_DIR="${OUTPUT_DIR:-./oci-discovery-$(date +%Y%m%d-%H%M%S)}"
OCI_MAX_RETRIES="${OCI_MAX_RETRIES:-5}"
OCI_RETRY_BASE_SECONDS="${OCI_RETRY_BASE_SECONDS:-1}"
OCI_ARGS=(--profile "$PROFILE")
[[ -n "${OCI_REGION:-}" ]] && OCI_ARGS+=(--region "$OCI_REGION")
if [[ -z "${DRG_NAMES:-}" && -z "${DRG_OCIDS:-}" ]]; then
echo "Set DRG_NAMES='drg-a,drg-b' or DRG_OCIDS='ocid1.drg...,ocid1.drg...'" >&2
exit 2
fi
mkdir -p "$OUTPUT_DIR"/{drgs,vcns,vpn,fastconnect,rpc,nvas,debug}
cd "$OUTPUT_DIR"
DEBUG_DIR="$(pwd)/debug"
: > "$DEBUG_DIR/INDEX.tsv" # seq, rc, dur_ms, bytes, cmd
echo "[+] Output: $(pwd)"
echo "[+] Debug: $DEBUG_DIR (one .cmd/.out.json/.err/.rc per oci call)"
# --- helpers ----------------------------------------------------------------
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
# Detect retryable OCI errors from stderr text (429 throttle, 5xx, network blips).
_oci_is_retryable() {
grep -qE '(TooManyRequests|InternalServerError|BadGateway|ServiceUnavailable|GatewayTimeout|"?[Ss]tatus"?[" ]*:[" ]*(429|500|502|503|504)|Connection reset|Connection refused|timed out|Temporary failure|EOF occurred|RemoteDisconnected)' "$1"
}
# oci wrapper — logs cmd+stdout+stderr+rc+duration per attempt; retries 429/5xx with
# exponential backoff + jitter; never aborts unless OCI_FAIL_FAST=1 AND non-retryable.
ocij() {
local base tag t0 t1 dur rc bytes first attempt delay jitter
attempt=0
while :; do
base=$(mktemp "$DEBUG_DIR/call-XXXXXXXX")
tag="${base##*/}"
rm -f "$base" # empty marker — suffixed files (.cmd/.out.json/.err/.rc) take over
if (( attempt > 0 )); then
printf 'oci %s %s # retry %d/%d\n' "${OCI_ARGS[*]}" "$*" "$attempt" "$OCI_MAX_RETRIES" > "$base.cmd"
else
printf 'oci %s %s\n' "${OCI_ARGS[*]}" "$*" > "$base.cmd"
fi
t0=$(date +%s)
set +e
oci "${OCI_ARGS[@]}" "$@" > "$base.out.json" 2> "$base.err"
rc=$?
set -e
t1=$(date +%s)
dur=$(( (t1 - t0) * 1000 ))
echo "$rc" > "$base.rc"
bytes=$(wc -c < "$base.out.json" | tr -d ' ')
printf '%s\t%d\t%d\t%s\toci %s%s\n' \
"$tag" "$rc" "$dur" "$bytes" "$*" \
"$([[ $attempt -gt 0 ]] && echo " (retry $attempt)")" >> "$DEBUG_DIR/INDEX.tsv"
if (( rc == 0 )); then
if [[ -s "$base.out.json" ]]; then cat "$base.out.json"; else echo '{"data":[]}'; fi
return 0
fi
# rc != 0 — retry if (a) attempts remain AND (b) stderr matches retryable patterns
if (( attempt < OCI_MAX_RETRIES )) && _oci_is_retryable "$base.err"; then
attempt=$((attempt + 1))
# exponential backoff: base * 2^(attempt-1), plus 0-999ms jitter
delay=$(( OCI_RETRY_BASE_SECONDS * (1 << (attempt - 1)) ))
jitter=$(( RANDOM % 1000 ))
first=$(head -n1 "$base.err" 2>/dev/null || true)
log " .. oci rc=$rc — retry $attempt/$OCI_MAX_RETRIES in ${delay}.$(printf '%03d' $jitter)s :: ${first:0:120}"
sleep "${delay}.$(printf '%03d' $jitter)"
continue
fi
# Terminal failure (non-retryable or retries exhausted)
first=$(head -n1 "$base.err" 2>/dev/null || true)
if (( attempt > 0 )); then
log " !! oci rc=$rc ($tag) after $attempt retries: oci $* :: ${first:-<no stderr>}"
else
log " !! oci rc=$rc ($tag): oci $* :: ${first:-<no stderr>}"
fi
if [[ "${OCI_FAIL_FAST:-0}" == "1" ]]; then
echo "OCI_FAIL_FAST=1 — aborting on oci error (see $base.err)" >&2
exit 3
fi
echo '{"data":[]}'
return 0
done
}
# Resource search (tenancy-wide)
search() {
local q="$1"
ocij search resource structured-search --query-text "$q"
}
# --- 0. tenancy / compartments ---------------------------------------------
log "Discovering tenancy and compartments..."
TENANCY=$(oci "${OCI_ARGS[@]}" iam compartment list --include-root --all \
--query "data[?\"compartment-id\"==null].id | [0]" --raw-output 2>/dev/null || true)
[[ -z "$TENANCY" ]] && { echo "Could not detect tenancy OCID. Configure ~/.oci/config." >&2; exit 2; }
if [[ -n "${COMPARTMENT_OCID:-}" ]]; then
ocij iam compartment list -c "$COMPARTMENT_OCID" --compartment-id-in-subtree true \
--access-level ACCESSIBLE --all --include-root > compartments.json
else
ocij iam compartment list -c "$TENANCY" --compartment-id-in-subtree true \
--access-level ACCESSIBLE --all --include-root > compartments.json
fi
jq -r '.data[] | .id' compartments.json > .compartments.list
log " $(wc -l < .compartments.list) compartments"
# --- 1. Resolve DRG OCIDs ---------------------------------------------------
log "Resolving DRG OCIDs..."
: > .drg.list
if [[ -n "${DRG_OCIDS:-}" ]]; then
tr ',' '\n' <<<"$DRG_OCIDS" | sed 's/^ *//;s/ *$//' | grep -v '^$' >> .drg.list
fi
if [[ -n "${DRG_NAMES:-}" ]]; then
while IFS= read -r name; do
[[ -z "$name" ]] && continue
search "query drg resources where displayName = '$name'" \
| jq -r '.data.items[]?.identifier' >> .drg.list
done < <(tr ',' '\n' <<<"$DRG_NAMES" | sed 's/^ *//;s/ *$//')
fi
sort -u .drg.list -o .drg.list
[[ ! -s .drg.list ]] && { echo "No DRGs resolved." >&2; exit 1; }
log " $(wc -l < .drg.list) DRG(s)"
# --- 2. Walk each DRG -------------------------------------------------------
: > .vcn.list
: > .ipsec.list
: > .vc.list
: > .rpc.list
: > .cpe.list
while IFS= read -r DRG; do
log "DRG: $DRG"
D="drgs/$DRG"; mkdir -p "$D"
ocij network drg get --drg-id "$DRG" > "$D/drg.json"
# drg-attachment list REQUIRES --compartment-id; --drg-id is just a filter.
# Always query from tenancy root with subtree — an attachment may live in any
# compartment the principal can see, not necessarily the DRG's own.
ocij network drg-attachment list --compartment-id "$TENANCY" --compartment-id-in-subtree true \
--drg-id "$DRG" --all > "$D/attachments.json"
ocij network drg-route-table list --drg-id "$DRG" --all > "$D/route-tables.json"
ocij network drg-route-distribution list --drg-id "$DRG" --all > "$D/route-distributions.json"
ATTCOUNT=$(jq -r '.data | length' "$D/attachments.json" 2>/dev/null || echo 0)
RTCOUNT=$(jq -r '.data | length' "$D/route-tables.json" 2>/dev/null || echo 0)
RDCOUNT=$(jq -r '.data | length' "$D/route-distributions.json" 2>/dev/null || echo 0)
log " attachments=$ATTCOUNT drg-route-tables=$RTCOUNT route-distributions=$RDCOUNT"
jq -r '.data[0] // {} | keys | " attachment[0] keys: " + (.|join(", "))' "$D/attachments.json" >&2 || true
# DRG route table rules (parallel via background jobs — routed through ocij so they're logged)
i=0
while IFS= read -r RT; do
[[ -z "$RT" ]] && continue
( ocij network drg-route-rule list --drg-route-table-id "$RT" --all > "$D/drg-rt-$RT.rules.json" ) &
(( ++i % PARALLEL == 0 )) && wait
done < <(jq -r '.data[]?.id' "$D/route-tables.json")
wait
# DRG route distribution statements — correct flag is --route-distribution-id (verified in CLI 3.84)
i=0
while IFS= read -r RD; do
[[ -z "$RD" ]] && continue
( ocij network drg-route-distribution-statement list --route-distribution-id "$RD" --all > "$D/drg-rd-$RD.statements.json" ) &
(( ++i % PARALLEL == 0 )) && wait
done < <(jq -r '.data[]?.id' "$D/route-distributions.json")
wait
# Normalize every attachment into {type,id} regardless of CLI version / legacy shape.
# Handles:
# - new nested: network-details (kebab) network_details (snake) networkDetails (camel)
# - legacy flat: vcn-id (DRG v1 era)
# - IPSec attachments where the id may live under .ids[] or .id
jq -r '
.data[]? | . as $a
| (."network-details" // .network_details // .networkDetails // null) as $nd
| (
if $nd then { type: $nd.type, ids: ([ $nd.id ] + ($nd.ids // []) | map(select(. != null and . != ""))) }
elif $a."vcn-id" then { type: "VCN", ids: [ $a."vcn-id" ] }
else { type: null, ids: [] }
end
)
| select(.type != null)
| .type as $t | .ids[] | "\($t)\t\(.)"
' "$D/attachments.json" > "$D/.normalized.tsv"
# For IPSEC_TUNNEL attachments the tunnel OCID at .id is per-tunnel; the parent
# ip-sec-connection OCID lives at network-details.ipsec-connection-id. We collect
# the *connection* OCID because `ip-sec-tunnel list --ipsc-id <conn>` returns ALL
# tunnels (typically 2) of that connection — strictly more complete than just the
# attached tunnel.
jq -r '
.data[]? | ."network-details" // {}
| select(.type=="IPSEC_TUNNEL") | ."ipsec-connection-id" // empty
' "$D/attachments.json" >> .ipsec.list
awk -F'\t' '
$1=="VCN" { print $2 >> ".vcn.list" }
$1=="VIRTUAL_CIRCUIT" { print $2 >> ".vc.list" }
$1=="REMOTE_PEERING_CONNECTION" { print $2 >> ".rpc.list" }
$1=="REMOTE_PEERING" { print $2 >> ".rpc.list" }
$1=="LOOPBACK" { print $2 >> ".loopback.list" }
{ c[$1]++ }
END { for (t in c) printf " %-22s %d\n", t, c[t] > "/dev/stderr" }
' "$D/.normalized.tsv"
done < .drg.list
for f in .vcn.list .ipsec.list .vc.list .rpc.list .loopback.list; do
[[ -f "$f" ]] || : > "$f"
sort -u "$f" -o "$f"
done
log " via DRG attachments — VCNs:$(wc -l<.vcn.list) IPSecConn:$(wc -l<.ipsec.list) VC:$(wc -l<.vc.list) RPC:$(wc -l<.rpc.list)"
# --- 2b. Tenancy-wide direct discovery -------------------------------------
# A VCN that egresses via IGW (or peers another VCN via LPG only) has no DRG
# attachment — so listing only via DRG would miss it. List every VCN /
# IPSec connection / VC / RPC / CPE we can see in every in-scope compartment
# and merge with the DRG-derived sets.
log "Direct discovery across compartments..."
DISCOVER_DIRECT="${DISCOVER_DIRECT:-1}"
if [[ "$DISCOVER_DIRECT" == "1" ]]; then
# Write per-compartment temp files (no shared appends → no race).
DIRECT_DIR=$(mktemp -d ".direct-XXXX")
i=0
while IFS= read -r COMP; do
[[ -z "$COMP" ]] && continue
(
ocij network vcn list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.vcn"
ocij network ip-sec-connection list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.ipsec"
ocij network virtual-circuit list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.vc"
ocij network remote-peering-connection list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.rpc"
ocij network cpe list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.cpe"
ocij network drg list -c "$COMP" --all | jq -r '.data[]?.id' > "$DIRECT_DIR/$COMP.drg"
) &
(( ++i % PARALLEL == 0 )) && wait
done < .compartments.list
wait
# Merge direct-discovery lists into the canonical anchor lists.
# NOTE: 'drg' is intentionally excluded — the user's input list of DRGs is the
# anchor of the entire walk and must not be expanded by direct discovery.
for src in vcn ipsec vc rpc cpe; do
if compgen -G "$DIRECT_DIR/*.$src" >/dev/null; then
cat "$DIRECT_DIR"/*.$src 2>/dev/null > .${src}.list.direct
sort -u .${src}.list.direct -o .${src}.list.direct
[[ -f .${src}.list ]] && cat .${src}.list >> .${src}.list.direct
sort -u .${src}.list.direct -o .${src}.list
fi
done
# DRG-visible list is computed but kept separate from the walk anchor.
if compgen -G "$DIRECT_DIR/*.drg" >/dev/null; then
cat "$DIRECT_DIR"/*.drg 2>/dev/null | sort -u > .drg.list.direct
fi
log " after direct — VCNs:$(wc -l<.vcn.list) IPSecConn:$(wc -l<.ipsec.list) VC:$(wc -l<.vc.list) RPC:$(wc -l<.rpc.list) CPE:$(wc -l<.cpe.list 2>/dev/null||echo 0) DRG-visible:$(wc -l<.drg.list.direct 2>/dev/null||echo 0)"
if [[ -f .drg.list.direct ]]; then
extra=$(comm -23 .drg.list.direct .drg.list 2>/dev/null | wc -l | tr -d ' ')
(( extra > 0 )) && log " note: $extra additional DRG(s) visible in tenancy but not in your input (see $DIRECT_DIR/*.drg)"
fi
fi
# --- 3. VCNs ----------------------------------------------------------------
walk_vcn() {
local VCN="$1"
local V="vcns/$VCN"; mkdir -p "$V"
# get VCN (need compartment for subsequent list calls) — route through ocij so it's logged
ocij network vcn get --vcn-id "$VCN" > "$V/vcn.json"
local COMP
COMP=$(jq -r '.data."compartment-id" // empty' "$V/vcn.json")
if [[ -z "$COMP" ]]; then
log " !! vcn $VCN: no compartment-id in response (likely auth/lookup failure — check debug/)"
return
fi
ocij network subnet list -c "$COMP" --vcn-id "$VCN" --all > "$V/subnets.json"
ocij network route-table list -c "$COMP" --vcn-id "$VCN" --all > "$V/route-tables.json"
ocij network security-list list -c "$COMP" --vcn-id "$VCN" --all > "$V/security-lists.json"
ocij network nsg list -c "$COMP" --vcn-id "$VCN" --all > "$V/nsgs.json"
ocij network internet-gateway list -c "$COMP" --vcn-id "$VCN" --all > "$V/igws.json"
ocij network nat-gateway list -c "$COMP" --vcn-id "$VCN" --all > "$V/nats.json"
ocij network service-gateway list -c "$COMP" --vcn-id "$VCN" --all > "$V/sgws.json"
ocij network local-peering-gateway list -c "$COMP" --vcn-id "$VCN" --all > "$V/lpgs.json"
# Route tables — each already contains rules in the list response, but dump per-RT for clarity
jq -c '.data[]?' "$V/route-tables.json" | while IFS= read -r rt; do
local RT_OCID; RT_OCID=$(jq -r '.id' <<<"$rt")
jq -n --argjson r "$rt" '{data:$r}' > "$V/rt-$RT_OCID.json"
done
# NSG rules
jq -r '.data[]?.id' "$V/nsgs.json" | while IFS= read -r NSG; do
[[ -z "$NSG" ]] && continue
ocij network nsg rules list --nsg-id "$NSG" --all > "$V/nsg-$NSG.rules.json"
done
}
log "Walking VCNs (parallel=$PARALLEL)..."
# Use background jobs with a simple semaphore — keeps OCI_ARGS array intact
i=0
while IFS= read -r VCN; do
[[ -z "$VCN" ]] && continue
walk_vcn "$VCN" &
(( ++i % PARALLEL == 0 )) && wait
done < .vcn.list
wait
# --- 4. VPN / FastConnect / RPC --------------------------------------------
log "Collecting IPSec / VC / RPC / CPE details..."
while IFS= read -r IPSC; do
[[ -z "$IPSC" ]] && continue
ocij network ip-sec-connection get --ipsc-id "$IPSC" > "vpn/ipsec-$IPSC.json"
# Correct subcommand is `ip-sec-tunnel list` (NOT `ip-sec-connection-tunnel list`)
ocij network ip-sec-tunnel list --ipsc-id "$IPSC" --all > "vpn/ipsec-$IPSC.tunnels.json"
jq -r '.data[]?.id' "vpn/ipsec-$IPSC.tunnels.json" | while IFS= read -r TUN; do
[[ -z "$TUN" ]] && continue
# Correct subcommand is `tunnel-route list-ip-sec-connection` — returns BGP routes
# advertised to and received from the on-prem peer (both directions when --advertiser omitted)
ocij network tunnel-route list-ip-sec-connection \
--ipsc-id "$IPSC" --tunnel-id "$TUN" --all > "vpn/ipsec-$IPSC.tunnel-$TUN.routes.json"
done
# CPE for this IPSec
CPE=$(jq -r '.data."cpe-id" // empty' "vpn/ipsec-$IPSC.json")
[[ -n "$CPE" ]] && echo "$CPE" >> .cpe.list
done < .ipsec.list
sort -u .cpe.list -o .cpe.list 2>/dev/null || true
while IFS= read -r CPE; do
[[ -z "$CPE" ]] && continue
ocij network cpe get --cpe-id "$CPE" > "vpn/cpe-$CPE.json"
done < .cpe.list
while IFS= read -r VC; do
[[ -z "$VC" ]] && continue
ocij network virtual-circuit get --virtual-circuit-id "$VC" > "fastconnect/vc-$VC.json"
done < .vc.list
while IFS= read -r RPC; do
[[ -z "$RPC" ]] && continue
ocij network remote-peering-connection get --remote-peering-connection-id "$RPC" > "rpc/rpc-$RPC.json"
done < .rpc.list
# --- 5. Forced paths through NVAs ------------------------------------------
log "Scanning for forced paths (PRIVATE_IP next-hops)..."
# Build a single forced-paths.json across all VCN route tables AND DRG-attachment ingress RTs
jq -s '
[ .[] | .data[]? as $rt
| $rt."route-rules"[]?
| select(."destination-type"=="PRIVATE_IP")
| { rt_id: $rt.id, rt_name: $rt."display-name", vcn_id: $rt."vcn-id",
destination: .destination, next_hop_private_ip: ."network-entity-id",
description: .description }
]
' vcns/*/route-tables.json 2>/dev/null > forced-paths.vcn.json || echo '[]' > forced-paths.vcn.json
# Resolve each private-IP next-hop -> VNIC -> instance
if [[ "$SKIP_NVA_RESOLVE" != "1" ]]; then
jq -r '.[].next_hop_private_ip' forced-paths.vcn.json | sort -u | while IFS= read -r PIP; do
[[ -z "$PIP" ]] && continue
ocij network private-ip get --private-ip-id "$PIP" > "nvas/private-ip-$PIP.json"
VNIC=$(jq -r '.data."vnic-id" // empty' "nvas/private-ip-$PIP.json")
if [[ -n "$VNIC" ]]; then
ocij network vnic get --vnic-id "$VNIC" > "nvas/vnic-$VNIC.json"
INST=$(jq -r '.data."instance-id" // empty' "nvas/vnic-$VNIC.json")
if [[ -n "$INST" ]]; then
ocij compute instance get --instance-id "$INST" > "nvas/instance-$INST.json"
fi
fi
done
fi
# Enrich forced-paths with NVA instance info. Guard against empty nvas/ glob.
if compgen -G "nvas/private-ip-*.json" >/dev/null; then
jq -s '
(.[0]) as $paths
| (.[1:]) as $vnics
| $paths | map(
. as $p
| ($vnics | map(select(.data.id==$p.next_hop_private_ip)) | .[0] // null) as $pip
| . + { vnic_id: ($pip.data."vnic-id" // null) }
)
' forced-paths.vcn.json nvas/private-ip-*.json > forced-paths.json
else
cp forced-paths.vcn.json forced-paths.json
fi
# --- 6. Summary -------------------------------------------------------------
log "Building summary..."
jq -n \
--slurpfile drgs <(for d in drgs/*/drg.json; do [[ -f "$d" ]] && cat "$d"; done | jq -s '.') \
--slurpfile vcns <(for v in vcns/*/vcn.json; do [[ -f "$v" ]] && cat "$v"; done | jq -s '.') \
--slurpfile forced forced-paths.json \
'{
generated: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
counts: {
drgs: ($drgs[0] | length),
vcns: ($vcns[0] | length),
forced_path_rules: ($forced[0] | length)
},
drgs: ($drgs[0] | map({id: .data.id, name: .data."display-name", compartment: .data."compartment-id"})),
vcns: ($vcns[0] | map({id: .data.id, name: .data."display-name", cidrs: (.data."cidr-blocks" // [.data."cidr-block"]), compartment: .data."compartment-id"})),
forced_paths: $forced[0]
}' > summary.json 2>/dev/null || echo '{}' > summary.json
# Human-readable
{
echo "OCI Routing Discovery — $(date)"
echo "================================"
echo
echo "DRGs:"
jq -r '.drgs[] | " - \(.name) \(.id)"' summary.json
echo
echo "VCNs:"
jq -r '.vcns[] | " - \(.name) cidrs=\(.cidrs|join(",")) \(.id)"' summary.json
echo
echo "Forced paths through NVAs (private-IP next hops):"
jq -r '.forced_paths[] | " - dst=\(.destination) via private-ip=\(.next_hop_private_ip) (vnic=\(.vnic_id // "?")) in rt=\(.rt_name)"' summary.json
echo
echo "Files:"
echo " drgs/<drg>/ attachments, route-tables (+rules), route-distributions"
echo " vcns/<vcn>/ subnets, route-tables, gateways, security-lists, NSGs"
echo " vpn/ IPSec connections, tunnels, advertised routes, CPEs"
echo " fastconnect/ virtual circuits"
echo " rpc/ remote peering connections"
echo " nvas/ private-ip/vnic/instance for forced-path next hops"
echo " forced-paths.json every PRIVATE_IP route rule across all VCNs"
echo " summary.json topology index"
} > summary.txt
# --- debug summary ----------------------------------------------------------
log "Building debug summary..."
DBG_TOTAL=$(wc -l < "$DEBUG_DIR/INDEX.tsv" | tr -d ' ')
DBG_ERRORS=$(awk -F'\t' '$2 != 0' "$DEBUG_DIR/INDEX.tsv" | wc -l | tr -d ' ')
DBG_ZERODATA=$(awk -F'\t' '$2 == 0 && $4 < 50' "$DEBUG_DIR/INDEX.tsv" | wc -l | tr -d ' ')
DBG_RETRIES=$(grep -c '(retry ' "$DEBUG_DIR/INDEX.tsv" 2>/dev/null || echo 0)
{
echo "OCI Discovery debug summary — $(date)"
echo
echo "total oci calls : $DBG_TOTAL"
echo "retries : $DBG_RETRIES (transient 429/5xx/network — retried with backoff)"
echo "non-zero rc : $DBG_ERRORS (includes intermediate retry attempts)"
echo "empty responses : $DBG_ZERODATA (rc=0 but <50 bytes — likely empty .data array)"
echo
if (( DBG_ERRORS > 0 )); then
echo "Failing calls (rc, dur_ms, bytes, command, first error line):"
awk -F'\t' '$2 != 0' "$DEBUG_DIR/INDEX.tsv" | while IFS=$'\t' read -r tag rc dur bytes cmd; do
first=$(head -n1 "$DEBUG_DIR/$tag.err" 2>/dev/null || true)
printf ' rc=%s %sms %sb %s\n err: %s\n' "$rc" "$dur" "$bytes" "$cmd" "${first:-<empty>}"
done
echo
echo "Top 10 distinct error lines:"
cat "$DEBUG_DIR"/*.err 2>/dev/null | sed -E 's/[0-9a-f-]{36}/<uuid>/g; s/ocid1\.[a-z]+\.oc1\.[^ ]*/<ocid>/g' \
| sort | uniq -c | sort -rn | head -10
fi
} > "$DEBUG_DIR/SUMMARY.txt" 2>&1
log " debug: total=$DBG_TOTAL errors=$DBG_ERRORS empty=$DBG_ZERODATA"
# --- cleanup hidden lists ---------------------------------------------------
rm -f .compartments.list .drg.list .vcn.list .ipsec.list \
.vc.list .rpc.list .cpe.list .loopback.list \
.vcn.list.direct .ipsec.list.direct .vc.list.direct .rpc.list.direct \
.cpe.list.direct .drg.list.direct \
forced-paths.vcn.json
find drgs -name '.normalized.tsv' -delete 2>/dev/null || true
rm -rf .direct-* 2>/dev/null || true
log "Done."
log " Summary : $(pwd)/summary.txt"
log " Debug : $(pwd)/debug/SUMMARY.txt (raw per-call logs in $(pwd)/debug/)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment