Files
azure-diagnostics/scripts/test-messaging-connectivity.sh

191 lines
6.8 KiB
Bash

#!/usr/bin/env bash
# test-messaging-connectivity.sh
# Probes reachability of an Azure Service Bus / Event Hubs namespace and prints a
# normalized per-check report: DNS resolution, HTTPS reachability, and TCP
# connectivity to the well-known messaging ports (AMQP 5671/5672, HTTPS 443, and
# — with --kafka — Event Hubs Kafka 9093).
#
# A blocked port is a valid diagnostic result, not a failure: the script exits 0
# unless the arguments are invalid. Choosing which namespace to test and
# diagnosing a blocked port (IP firewall vs. corporate proxy vs. NSG) require
# judgment and stay in the skill prose.
#
# Usage:
# ./test-messaging-connectivity.sh <namespace> [--kafka]
#
# The namespace may be a full FQDN or a bare namespace name; when no dot is
# present, ".servicebus.windows.net" is appended automatically.
#
# Examples:
# ./test-messaging-connectivity.sh contoso # contoso.servicebus.windows.net
# ./test-messaging-connectivity.sh contoso.servicebus.windows.net # Service Bus / Event Hubs (AMQP + HTTPS)
# ./test-messaging-connectivity.sh contoso --kafka # also probe Event Hubs Kafka port 9093
set -uo pipefail
INCLUDE_KAFKA=0
NAMESPACE=""
while [ $# -gt 0 ]; do
case "$1" in
--kafka)
INCLUDE_KAFKA=1
;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'
exit 0
;;
--*)
echo "Unknown option: $1" >&2
echo "Usage: $0 <namespace> [--kafka]" >&2
exit 2
;;
*)
if [ -z "$NAMESPACE" ]; then
NAMESPACE="$1"
else
echo "Unexpected argument: $1" >&2
echo "Usage: $0 <namespace> [--kafka]" >&2
exit 2
fi
;;
esac
shift
done
if [ -z "$NAMESPACE" ]; then
echo "Usage: $0 <namespace> [--kafka]" >&2
exit 2
fi
# Accept a bare namespace name or a full FQDN.
FQDN="$NAMESPACE"
case "$FQDN" in
*.*) : ;; # already looks like an FQDN
*) FQDN="${FQDN}.servicebus.windows.net" ;;
esac
echo "Testing messaging connectivity for: $FQDN"
echo ""
# ── DNS resolution ────────────────────────────────────────────────────────────
resolve_ip() {
local host="$1" ip=""
if command -v getent >/dev/null 2>&1; then
ip=$(getent ahosts "$host" 2>/dev/null | awk '{print $1}' | head -n1)
fi
if [ -z "$ip" ] && command -v host >/dev/null 2>&1; then
ip=$(host "$host" 2>/dev/null | awk '/has address/ {print $NF; exit}')
fi
if [ -z "$ip" ] && command -v python3 >/dev/null 2>&1; then
ip=$(python3 -c "import socket,sys; print(socket.gethostbyname(sys.argv[1]))" "$host" 2>/dev/null)
fi
if [ -z "$ip" ] && command -v nslookup >/dev/null 2>&1; then
# Skip the leading "Server/Address" block; the answer's address follows
# the "Name:" line.
ip=$(nslookup "$host" 2>/dev/null | awk '/^Name:/ {seen=1; next} seen && /^Address/ {print $NF; exit}')
fi
printf '%s' "$ip"
}
RESOLVED_IP="$(resolve_ip "$FQDN")"
if [ -n "$RESOLVED_IP" ]; then
DNS_RESULT="resolved ($RESOLVED_IP)"
else
DNS_RESULT="NOT RESOLVED"
fi
# ── TCP port probe ────────────────────────────────────────────────────────────
probe_tcp() {
local host="$1" port="$2"
if command -v nc >/dev/null 2>&1; then
if nc -z -w 5 "$host" "$port" >/dev/null 2>&1; then
return 0
fi
return 1
fi
# Fallback: bash /dev/tcp with a background timeout.
( exec 3<>"/dev/tcp/$host/$port" ) >/dev/null 2>&1 &
local pid=$!
local waited=0
while kill -0 "$pid" 2>/dev/null; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge 5 ]; then
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
return 1
fi
done
wait "$pid"
return $?
}
port_result() {
if probe_tcp "$FQDN" "$1"; then
echo "reachable"
else
echo "BLOCKED"
fi
}
# ── HTTPS reachability ────────────────────────────────────────────────────────
# On success the namespace returns an Atom feed or HTTP 401 — either proves the
# endpoint is reachable. A connection failure (curl exit != 0) means blocked.
https_result() {
if ! command -v curl >/dev/null 2>&1; then
# No curl: fall back to a plain TCP probe of 443.
if probe_tcp "$FQDN" 443; then
echo "reachable (TCP 443 open; curl unavailable for HTTP check)"
else
echo "BLOCKED"
fi
return
fi
local code
code=$(curl -s -o /dev/null -m 15 -w '%{http_code}' "https://$FQDN/" 2>/dev/null)
local rc=$?
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
echo "reachable (HTTP $code)"
else
echo "BLOCKED (curl exit $rc)"
fi
}
DNS_OK=0; [ -n "$RESOLVED_IP" ] && DNS_OK=1
if [ "$DNS_OK" -eq 1 ]; then
HTTPS_RESULT="$(https_result)"
P443="$(port_result 443)"
P5671="$(port_result 5671)"
P5672="$(port_result 5672)"
if [ "$INCLUDE_KAFKA" -eq 1 ]; then
P9093="$(port_result 9093)"
fi
else
HTTPS_RESULT="skipped (DNS failed)"
P443="skipped (DNS failed)"
P5671="skipped (DNS failed)"
P5672="skipped (DNS failed)"
P9093="skipped (DNS failed)"
fi
# ── Report ────────────────────────────────────────────────────────────────────
printf '%-28s %-10s %s\n' "Check" "Port" "Result"
printf '%-28s %-10s %s\n' "-----" "----" "------"
printf '%-28s %-10s %s\n' "DNS resolution" "-" "$DNS_RESULT"
printf '%-28s %-10s %s\n' "HTTPS reachability" "443" "$HTTPS_RESULT"
printf '%-28s %-10s %s\n' "AMQP over TLS" "5671" "$P5671"
printf '%-28s %-10s %s\n' "AMQP" "5672" "$P5672"
printf '%-28s %-10s %s\n' "HTTPS / WebSockets" "443" "$P443"
if [ "$INCLUDE_KAFKA" -eq 1 ]; then
printf '%-28s %-10s %s\n' "Event Hubs Kafka" "9093" "$P9093"
fi
echo ""
if [ "$DNS_OK" -eq 0 ]; then
echo "Summary: could not resolve $FQDN. Check the namespace name and DNS/private-endpoint configuration before testing ports."
else
echo "Summary: DNS resolved to $RESOLVED_IP. 'reachable' ports accept TCP connections; any 'BLOCKED' port points to an IP firewall, NSG, corporate proxy, or private-endpoint restriction to investigate. Port 443 (WebSockets) can be used as a fallback when AMQP ports 5671/5672 are blocked."
fi