Last active
June 4, 2026 11:50
-
-
Save fahadysf/5c0acae7a7d22916dc6321bea85375fb to your computer and use it in GitHub Desktop.
OCI Routing Discovery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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