From fb9756b147d0cf129abf7a6fc150836a9fcc3635 Mon Sep 17 00:00:00 2001 From: alex-local Date: Fri, 12 Sep 2025 11:33:11 +0200 Subject: [PATCH] awx-local-setup: harden scripts, add chmod helper, fix CI schedule --- .github/ansible-code-bot.yml | 2 - cleanup.sh | 35 ++++++++--- make-scripts-executable.sh | 9 +++ scripts/add_static_hosts.sh | 64 ++++++++++++++++--- scripts/create_cred_and_ping.sh | 106 ++++++++++++++++++++++---------- setup_awx_local.sh | 37 ++--------- 6 files changed, 167 insertions(+), 86 deletions(-) create mode 100755 make-scripts-executable.sh diff --git a/.github/ansible-code-bot.yml b/.github/ansible-code-bot.yml index e7ae499..0abf12a 100644 --- a/.github/ansible-code-bot.yml +++ b/.github/ansible-code-bot.yml @@ -1,4 +1,2 @@ schedule: interval: weekly -schedule: - interval: weekly diff --git a/cleanup.sh b/cleanup.sh index 18494fe..91b9c6e 100755 --- a/cleanup.sh +++ b/cleanup.sh @@ -2,26 +2,43 @@ set -euo pipefail # cleanup.sh -# Stops port-forward and optionally deletes the awx namespace and CRs. +# Stops known port-forward/service and optionally deletes the awx namespace and CRs. -PORTF_PID_FILE=/tmp/awx-pf.pid -NAMESPACE=awx +PORTF_PID_FILE=${PORTF_PID_FILE:-/tmp/awx-pf.pid} +NAMESPACE=${NAMESPACE:-awx} +NONINTERACTIVE=${NONINTERACTIVE:-0} +echo "Looking for port-forward PID file: $PORTF_PID_FILE" if [ -f "$PORTF_PID_FILE" ]; then - PID=$(cat $PORTF_PID_FILE) - echo "Killing port-forward PID $PID" - kill $PID 2>/dev/null || true - rm -f $PORTF_PID_FILE + PID=$(cat "$PORTF_PID_FILE" 2>/dev/null || true) + if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then + echo "Stopping port-forward PID $PID" + kill "$PID" 2>/dev/null || true + else + echo "Stale PID file or process not running; removing PID file" + fi + rm -f "$PORTF_PID_FILE" rm -f /tmp/awx-port-forward.log || true else echo "No port-forward PID file present" fi -read -p "Delete the AWX namespace and all AWX resources? [y/N]: " yn +if [ "$NONINTERACTIVE" -eq 1 ]; then + yn=Y +else + read -r -p "Delete the AWX namespace and all AWX resources? [y/N]: " yn +fi + if [[ "$yn" =~ ^[Yy]$ ]]; then echo "Deleting namespace $NAMESPACE" - kubectl delete ns $NAMESPACE --wait=true || true + if command -v kubectl >/dev/null 2>&1; then + kubectl delete ns "$NAMESPACE" --wait=true || true + else + echo "kubectl not found; skipping namespace delete" >&2 + fi echo "Note: CRDs are not deleted automatically. Remove them manually if desired." +else + echo "Skipping namespace deletion" fi echo "Cleanup complete." diff --git a/make-scripts-executable.sh b/make-scripts-executable.sh new file mode 100755 index 0000000..afae28c --- /dev/null +++ b/make-scripts-executable.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Make key scripts in awx-local-setup executable +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +chmod +x "$ROOT"/setup_awx_local.sh || true +chmod +x "$ROOT"/scripts/create_cred_and_ping.sh || true +chmod +x "$ROOT"/scripts/add_static_hosts.sh || true +chmod +x "$ROOT"/cleanup.sh || true +echo "Made scripts executable: setup_awx_local.sh, create_cred_and_ping.sh, add_static_hosts.sh, cleanup.sh" diff --git a/scripts/add_static_hosts.sh b/scripts/add_static_hosts.sh index b637c30..fd8b0df 100755 --- a/scripts/add_static_hosts.sh +++ b/scripts/add_static_hosts.sh @@ -11,32 +11,76 @@ AWX_USER="${AWX_USER:-admin}" AWX_PASS="${AWX_PASS:-Aase#1234!}" INV_ID="${INV_ID:-2}" -IPS=("192.168.1.144" "192.168.1.23" "192.168.1.24") +# Define IPs here or pass via environment variable (comma-separated) +if [ -n "${IPS_CSV:-}" ]; then + IFS=',' read -r -a IPS <<< "$IPS_CSV" +else + IPS=("192.168.1.144" "192.168.1.23" "192.168.1.24") +fi + +for cmd in curl jq mktemp; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command '$cmd' not found" >&2 + exit 1 + fi +done echo "Using AWX at $AWX_HOST (inventory id $INV_ID)" +TMPDIR=$(mktemp -d) +cleanup() { rm -rf "$TMPDIR" || true; } +trap cleanup EXIT INT TERM + +curl_auth() { + local method="$1" url="$2" datafile="${3:-}" outfile="${4:-/dev/stdout}" + local hdrs=( -H "Content-Type: application/json" ) + if [ -n "${AWX_TOKEN:-}" ]; then + hdrs+=( -H "Authorization: Bearer ${AWX_TOKEN}" ) + elif [ -n "${AWX_AUTH_HEADER:-}" ]; then + hdrs+=( -H "Authorization: ${AWX_AUTH_HEADER}" ) + else + hdrs+=( -u "${AWX_USER}:${AWX_PASS}" ) + fi + if [ -n "$datafile" ]; then + curl -sS -w "%{http_code}" "${hdrs[@]}" -X "$method" "$url" -d "@${datafile}" -o "$outfile" || true + else + curl -sS -w "%{http_code}" "${hdrs[@]}" -X "$method" "$url" -o "$outfile" || true + fi +} + for ip in "${IPS[@]}"; do name="host-$(echo "$ip" | tr '.' '-')" vars=$(printf "ansible_host: %s\n" "$ip") - # URL-encode the name for query - encoded_name=$(jq -nr --arg s "$name" '$s|@uri') - echo "Processing $name -> $ip" - host_id=$(curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/hosts/?name=$encoded_name&inventory=$INV_ID" | jq -r '.results[0].id // empty') + + # Check existing host + encoded_name=$(jq -nr --arg s "$name" '$s|@uri') + out="$TMPDIR/host_query.json" + code=$(curl_auth GET "$AWX_HOST/api/v2/hosts/?name=$encoded_name&inventory=$INV_ID" "" "$out") + if [ "$code" -lt 200 ] || [ "$code" -ge 500 ]; then + echo "Warning: unexpected response ($code) from AWX when querying host $name" >&2 + fi + + host_id=$(jq -r '.results[0].id // empty' "$out") if [ -n "$host_id" ]; then echo "Host exists (id=$host_id) — updating variables" - payload=$(jq -n --arg vars "$vars" '{variables:$vars}') - curl -sS -u "$AWX_USER:$AWX_PASS" -H "Content-Type: application/json" -X PATCH "$AWX_HOST/api/v2/hosts/$host_id/" -d "$payload" | jq . + payload="$TMPDIR/patch_payload.json" + jq -n --arg vars "$vars" '{variables:$vars}' > "$payload" + code=$(curl_auth PATCH "$AWX_HOST/api/v2/hosts/$host_id/" "$payload" "$TMPDIR/patch_resp.json") + echo "PATCH returned $code"; jq -M . "$TMPDIR/patch_resp.json" || true else echo "Host not found — creating" - payload=$(jq -n --arg name "$name" --arg inv "$INV_ID" --arg vars "$vars" '{name:$name, inventory:($inv|tonumber), variables:$vars}') - curl -sS -u "$AWX_USER:$AWX_PASS" -H "Content-Type: application/json" -X POST "$AWX_HOST/api/v2/hosts/" -d "$payload" | jq . + payload="$TMPDIR/create_payload.json" + jq -n --arg name "$name" --arg inv "$INV_ID" --arg vars "$vars" '{name:$name, inventory:($inv|tonumber), variables:$vars}' > "$payload" + code=$(curl_auth POST "$AWX_HOST/api/v2/hosts/" "$payload" "$TMPDIR/create_resp.json") + echo "POST returned $code"; jq -M . "$TMPDIR/create_resp.json" || true fi echo done echo "Listing hosts in inventory $INV_ID:" -curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/inventories/$INV_ID/hosts/?page_size=100" | jq '.results[] | {id: .id, name: .name, variables: .variables}' +curl_auth GET "$AWX_HOST/api/v2/inventories/$INV_ID/hosts/?page_size=100" "" "$TMPDIR/list.json" +jq '.results[] | {id: .id, name: .name, variables: .variables}' "$TMPDIR/list.json" || true diff --git a/scripts/create_cred_and_ping.sh b/scripts/create_cred_and_ping.sh index 0b1dbc5..20b552b 100755 --- a/scripts/create_cred_and_ping.sh +++ b/scripts/create_cred_and_ping.sh @@ -1,10 +1,12 @@ +echo "Created credential id=$NEW_CRED_ID" #!/usr/bin/env bash set -euo pipefail # create_cred_and_ping.sh # Creates an AWX Machine credential from a local private key and runs an ad-hoc ping -# against three static hosts. Configure AWX_HOST/AWX_USER/AWX_PASS via env if needed. +# against static hosts. Configure AWX_HOST/AWX_USER/AWX_PASS or AWX_TOKEN/AWX_AUTH_HEADER via env. +# Defaults AWX_HOST="${AWX_HOST:-http://192.168.49.2:30081}" AWX_USER="${AWX_USER:-admin}" AWX_PASS="${AWX_PASS:-Aase#1234!}" @@ -12,17 +14,57 @@ KEY_PATH="${KEY_PATH:-$HOME/.ssh/awx_deploy_key}" CREDS_NAME="${CREDS_NAME:-auto-machine-awx_deploy_key}" USERNAME="${USERNAME:-admin}" ORG_ID="${ORG_ID:-1}" +CREDTYPE="${CREDTYPE:-1}" +INV_ID="${INV_ID:-2}" +LIMIT="${LIMIT:-host-192-168-1-144,host-192-168-1-23,host-192-168-1-24}" +POLL_TIMEOUT="${POLL_TIMEOUT:-180}" + +# Dependencies +for cmd in curl jq python3 mktemp; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command '$cmd' not found in PATH" >&2 + exit 1 + fi +done if [ ! -f "$KEY_PATH" ]; then echo "ERROR: private key not found at $KEY_PATH" >&2 exit 1 fi -TMP_JSON="/tmp/awx_new_cred.json" -TMP_BODY="/tmp/awx_new_cred_body.json" -TMP_CODE="/tmp/awx_new_cred_code.txt" +# Temp files +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "${TMP_DIR:-/tmp}" || true +} +trap cleanup EXIT INT TERM -cat > /tmp/make_cred.py <<'PY' +TMP_JSON="$TMP_DIR/awx_new_cred.json" +TMP_BODY="$TMP_DIR/awx_new_cred_body.json" +ADHOC_JSON="$TMP_DIR/awx_adhoc.json" +RESP_FILE="$TMP_DIR/awx_adhoc_resp.json" + +# Helper to perform authenticated curl; supports AWX_TOKEN or AWX_AUTH_HEADER or basic auth fallback +curl_auth() { + local method="$1" url="$2" datafile="${3:-}" outfile="${4:-/dev/stdout}" + local hdrs=( -H "Content-Type: application/json" ) + if [ -n "${AWX_TOKEN:-}" ]; then + hdrs+=( -H "Authorization: Bearer ${AWX_TOKEN}" ) + elif [ -n "${AWX_AUTH_HEADER:-}" ]; then + hdrs+=( -H "Authorization: ${AWX_AUTH_HEADER}" ) + else + hdrs+=( -u "${AWX_USER}:${AWX_PASS}" ) + fi + + if [ -n "$datafile" ]; then + curl -sS -w "%{http_code}" "${hdrs[@]}" -X "$method" "$url" -d "@${datafile}" -o "$outfile" || true + else + curl -sS -w "%{http_code}" "${hdrs[@]}" -X "$method" "$url" -o "$outfile" || true + fi +} + +echo "Generating credential payload..." +python3 - "$KEY_PATH" "$CREDS_NAME" "$USERNAME" "$CREDTYPE" "$ORG_ID" > "$TMP_JSON" <<'PY' import json,sys keypath=sys.argv[1] name=sys.argv[2] @@ -35,16 +77,13 @@ payload={'name': name, 'credential_type': ct, 'organization': org, 'inputs': {'u print(json.dumps(payload)) PY -python3 /tmp/make_cred.py "$KEY_PATH" "$CREDS_NAME" "$USERNAME" 1 "$ORG_ID" > "$TMP_JSON" - echo "Creating AWX credential from $KEY_PATH..." -curl -sS -u "$AWX_USER:$AWX_PASS" -H "Content-Type: application/json" -X POST "$AWX_HOST/api/v2/credentials/" -d "@$TMP_JSON" -o "$TMP_BODY" -w "%{http_code}" > "$TMP_CODE" || true -CODE=$(cat "$TMP_CODE" || true) +CODE=$(curl_auth POST "$AWX_HOST/api/v2/credentials/" "$TMP_JSON" "$TMP_BODY") echo "HTTP status: $CODE" cat "$TMP_BODY" || true if [ "$CODE" -lt 200 ] || [ "$CODE" -ge 300 ]; then - echo "Failed to create credential (status=$CODE)." >&2 + echo "Failed to create credential (status=$CODE). See response above." >&2 exit 1 fi @@ -56,31 +95,26 @@ fi echo "Created credential id=$NEW_CRED_ID" -# Prepare ad-hoc payload -INV_ID="${INV_ID:-2}" -LIMIT="host-192-168-1-144,host-192-168-1-23,host-192-168-1-24" -ADHOC_JSON="/tmp/awx_adhoc.json" -cat > "$ADHOC_JSON" < "$ADHOC_JSON" echo "Launching ad-hoc ping..." -RESP_FILE="/tmp/awx_adhoc_resp.json" -curl -sS -u "$AWX_USER:$AWX_PASS" -H "Content-Type: application/json" -X POST "$AWX_HOST/api/v2/ad_hoc_commands/" -d "@$ADHOC_JSON" -o "$RESP_FILE" || true +ADHOC_CODE=$(curl_auth POST "$AWX_HOST/api/v2/ad_hoc_commands/" "$ADHOC_JSON" "$RESP_FILE") +echo "Launch HTTP status: $ADHOC_CODE" cat "$RESP_FILE" || true ADHOC_ID=$(jq -r '.id // empty' "$RESP_FILE" || true) -if [ -z "$ADHOC_ID" ]; then echo "Failed to launch ad-hoc command" >&2; exit 1; fi +if [ -z "$ADHOC_ID" ]; then + echo "Failed to launch ad-hoc command; response above." >&2 + exit 1 +fi -echo "Launched ad-hoc id=$ADHOC_ID; polling until finished (180s max)" -END=$((SECONDS+180)) +echo "Launched ad-hoc id=$ADHOC_ID; polling until finished (${POLL_TIMEOUT}s max)" +END=$((SECONDS+POLL_TIMEOUT)) +STATUS="" while [ $SECONDS -lt $END ]; do - STATUS=$(curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/" | jq -r '.status // empty' || true) + # fetch status + curl_auth GET "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/" "" "$TMP_DIR/adhoc_status.json" >/dev/null + STATUS=$(jq -r '.status // empty' "$TMP_DIR/adhoc_status.json" || true) echo "Status: ${STATUS:-unknown}" if [ -n "$STATUS" ] && [ "$STATUS" != "running" ] && [ "$STATUS" != "pending" ]; then break @@ -88,12 +122,18 @@ while [ $SECONDS -lt $END ]; do sleep 2 done -echo "Final status: ${STATUS:-unknown}" +if [ -z "$STATUS" ]; then + echo "Timed out waiting for ad-hoc command to complete" >&2 +else + echo "Final status: $STATUS" +fi echo "--- events (first 500) ---" -curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/events/?page_size=500" | jq '.results[] | {counter: .counter, event: .event, host_name: .host_name, stdout: .stdout}' || true +curl_auth GET "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/events/?page_size=500" "" "$TMP_DIR/events.json" >/dev/null +jq -r '.results[] | {counter: .counter, event: .event, host_name: .host_name, stdout: .stdout}' "$TMP_DIR/events.json" || true echo "--- stdout (text) ---" -curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/stdout/?format=txt" || true +curl_auth GET "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/stdout/?format=txt" "" "$TMP_DIR/stdout.txt" >/dev/null +cat "$TMP_DIR/stdout.txt" || true -echo "Done. If SSH auth still fails, ensure the private key has access to the target hosts (or use a key with passwordless access)." +echo "Done. If SSH auth fails, ensure the private key has access to the target hosts or use a key with passwordless access." diff --git a/setup_awx_local.sh b/setup_awx_local.sh index b9b9e07..d87265e 100755 --- a/setup_awx_local.sh +++ b/setup_awx_local.sh @@ -71,13 +71,10 @@ else fi # create leader-election RBAC if missing -cat <<'EOF' | ( - if [ "${DRY_RUN}" = "1" ] || [ "${SKIP_CLUSTER:-0}" = "1" ]; then - cat >/dev/null - else - kubectl apply -n ${AWX_NAMESPACE} -f - - fi -) +if [ "${DRY_RUN}" = "1" ] || [ "${SKIP_CLUSTER:-0}" = "1" ]; then + echo "DRY RUN: skipping creation of leader-election RBAC" +else + kubectl apply -n ${AWX_NAMESPACE} -f - <<'EOF' apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -86,19 +83,7 @@ rules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get","create","update","patch","delete","watch","list"] -EOF - -if [ "${DRY_RUN}" = "1" ] || [ "${SKIP_CLUSTER:-0}" = "1" ]; then - echo "DRY RUN: would ensure rolebinding awx-operator-leader-election in ${AWX_NAMESPACE}" -else - kubectl get rolebinding awx-operator-leader-election -n ${AWX_NAMESPACE} >/dev/null 2>&1 || cat <<'EOF' | kubectl apply -n ${AWX_NAMESPACE} -f - -EOF -fi - -if [ "${DRY_RUN}" = "1" ] || [ "${SKIP_CLUSTER:-0}" = "1" ]; then - : -else - cat <<'EOF' | kubectl apply -n ${AWX_NAMESPACE} -f - +--- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -112,18 +97,6 @@ roleRef: apiGroup: rbac.authorization.k8s.io EOF fi -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: awx-operator-leader-election -subjects: -- kind: ServiceAccount - name: controller-manager -roleRef: - kind: Role - name: awx-operator-leader-election - apiGroup: rbac.authorization.k8s.io -EOF # wait for operator deployment to be available echo "Waiting for controller-manager deployment to be available..."