diff --git a/apt-update.yml b/apt-update.yml index 41f2a00..e69de29 100644 --- a/apt-update.yml +++ b/apt-update.yml @@ -1,28 +0,0 @@ ---- -- name: APT Update Playbook - hosts: all - become: yes - gather_facts: yes - - tasks: - - name: Update apt package cache - ansible.builtin.apt: - update_cache: yes - cache_valid_time: 3600 - register: apt_update_result - when: ansible_os_family == "Debian" - - - name: Display update result for Debian/Ubuntu systems - ansible.builtin.debug: - msg: "APT cache updated successfully on {{ inventory_hostname }}" - when: ansible_os_family == "Debian" and apt_update_result is succeeded - - - name: Skip non-Debian systems - ansible.builtin.debug: - msg: "Skipping {{ inventory_hostname }} - not a Debian/Ubuntu system ({{ ansible_os_family }})" - when: ansible_os_family != "Debian" - - - name: Display failure message - ansible.builtin.debug: - msg: "APT update failed on {{ inventory_hostname }}" - when: ansible_os_family == "Debian" and apt_update_result is failed diff --git a/apt-upgrade.yml b/apt-upgrade.yml index 3313541..e69de29 100644 --- a/apt-upgrade.yml +++ b/apt-upgrade.yml @@ -1,56 +0,0 @@ ---- -- name: APT Update and Upgrade Playbook - hosts: all - become: yes - gather_facts: yes - - tasks: - - name: Update apt package cache - ansible.builtin.apt: - update_cache: yes - cache_valid_time: 3600 - register: apt_update_result - when: ansible_os_family == "Debian" - - - name: Upgrade all packages to latest version - ansible.builtin.apt: - upgrade: dist - autoremove: yes - autoclean: yes - register: apt_upgrade_result - when: ansible_os_family == "Debian" and apt_update_result is succeeded - - - name: Check if reboot is required - ansible.builtin.stat: - path: /var/run/reboot-required - register: reboot_required_file - when: ansible_os_family == "Debian" - - - name: Display upgrade summary - ansible.builtin.debug: - msg: | - APT operations completed on {{ inventory_hostname }}: - - Cache updated: {{ apt_update_result.changed | default(false) }} - - Packages upgraded: {{ apt_upgrade_result.changed | default(false) }} - - Reboot required: {{ reboot_required_file.stat.exists | default(false) }} - when: ansible_os_family == "Debian" - - - name: Reboot system if required - ansible.builtin.reboot: - msg: "Reboot initiated by Ansible for package updates" - connect_timeout: 5 - reboot_timeout: 300 - pre_reboot_delay: 0 - post_reboot_delay: 30 - test_command: uptime - when: ansible_os_family == "Debian" and reboot_required_file.stat.exists | default(false) - - - name: Skip non-Debian systems - ansible.builtin.debug: - msg: "Skipping {{ inventory_hostname }} - not a Debian/Ubuntu system ({{ ansible_os_family }})" - when: ansible_os_family != "Debian" - - - name: Display failure message - ansible.builtin.debug: - msg: "APT operations failed on {{ inventory_hostname }}" - when: ansible_os_family == "Debian" and (apt_update_result is failed or apt_upgrade_result is failed) diff --git a/ee/Dockerfile b/ee/Dockerfile new file mode 100644 index 0000000..f465e85 --- /dev/null +++ b/ee/Dockerfile @@ -0,0 +1,11 @@ +# Custom AWX Execution Environment with NetBox support +# Extends the official AWX EE and installs pynetbox and the netbox collection +FROM quay.io/ansible/awx-ee:latest + +USER root +# Install python dependency and the NetBox Ansible collection +RUN pip install --no-cache-dir pynetbox \ + && ansible-galaxy collection install netbox.netbox || true + +# Drop back to non-root user used by the base image +USER 1000 diff --git a/ee/README.md b/ee/README.md new file mode 100644 index 0000000..ba3dbac --- /dev/null +++ b/ee/README.md @@ -0,0 +1,31 @@ +Custom AWX Execution Environment: NetBox support + +This folder contains a minimal Dockerfile and helper to create an AWX Execution +Environment that includes the NetBox Ansible collection and pynetbox. + +Files: +- Dockerfile - derived from quay.io/ansible/awx-ee:latest, installs pynetbox and netbox.netbox +- build-and-load-ee.sh - builds the image and loads it into Minikube + +Usage: +1. Build and load into minikube: + + IMAGE_NAME=local/awx-ee-netbox:latest ./awx-local-setup/ee/build-and-load-ee.sh + +2. Register this Execution Environment in AWX (UI or awx-cli / API). Use the image + name you built (local/awx-ee-netbox:latest) as the Execution Environment image. + +3. Commit an inventory plugin file (example: inventory/netbox_inventory.yml) to + your project repository that uses the netbox.netbox.inventory plugin and + references the NetBox token and URL. + +4. Create an AWX inventory source of type `scm` pointing to the project and set + `source_path` to the plugin file created in step 3. Ensure the inventory + source uses the custom Execution Environment. + +Notes: +- If your Minikube cluster uses a remote container runtime you may need to push + the built image to a registry reachable by the cluster and set IMAGE_NAME + accordingly. +- For production, build and push to a private registry and configure AWX to + pull the image using pull secrets. diff --git a/ee/build-and-load-ee.sh b/ee/build-and-load-ee.sh new file mode 100755 index 0000000..b05572c --- /dev/null +++ b/ee/build-and-load-ee.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +# build-and-load-ee.sh +# Build the custom AWX Execution Environment and load it into Minikube + +IMAGE_NAME=${IMAGE_NAME:-local/awx-ee-netbox:latest} +MINIKUBE_PROFILE=${MINIKUBE_PROFILE:-minikube} + +echo "Building EE image: $IMAGE_NAME" +docker build -t "$IMAGE_NAME" -f "$(dirname "$0")/ee/Dockerfile" . + +# If minikube is running with docker driver we can use 'docker' directly, else load into minikube +if minikube status -p "$MINIKUBE_PROFILE" >/dev/null 2>&1; then + DRIVER=$(minikube status -p "$MINIKUBE_PROFILE" --format='{{.Host}}' || true) +fi + +if minikube image load "$IMAGE_NAME" -p "$MINIKUBE_PROFILE"; then + echo "Loaded $IMAGE_NAME into minikube" +else + echo "Failed to use minikube image load; try docker push to a registry and update AWX EE settings" +fi diff --git a/integrate_github_netbox.sh b/integrate_github_netbox.sh new file mode 100755 index 0000000..860f420 --- /dev/null +++ b/integrate_github_netbox.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -euo pipefail +# integrate_github_netbox.sh +# Create AWX Project (Git) and NetBox inventory source via AWX API. +# Usage (dry-run by default): +# ./integrate_github_netbox.sh --github-repo https://github.com/owner/repo.git \ +# --github-pat GH_PAT --netbox-url https://netbox.example/api/ --netbox-token NETBOX_TOKEN [--apply] + +AWX_URL=${AWX_URL:-http://localhost:30081} +AWX_USER=${AWX_USER:-admin} +AWX_PW="${AWX_PW:-}" +GITHUB_REPO="" +GITHUB_PAT="" +NETBOX_URL="" +NETBOX_TOKEN="" +PROJECT_NAME="awx-github-project" +INV_NAME="netbox-inventory" +DRY_RUN=1 + +usage(){ + cat </dev/null 2>&1; then + echo "missing dependency: $cmd" >&2 + exit 1 + fi +done + +if [ -z "$AWX_PW" ]; then + # try to read from k8s secret + if command -v kubectl >/dev/null 2>&1; then + if AWX_PW_RAW=$(kubectl -n awx get secret awx-admin-password -o jsonpath='{.data.password}' 2>/dev/null || true); then + if [ -n "$AWX_PW_RAW" ]; then + AWX_PW=$(echo "$AWX_PW_RAW" | base64 --decode) + fi + fi + fi +fi + +# Require github repo, netbox url and token. Either GITHUB_PAT OR a local SSH key file must exist. +if [ -z "$GITHUB_REPO" ] || [ -z "$NETBOX_URL" ] || [ -z "${NETBOX_TOKEN:-}" ] || { [ -z "${GITHUB_PAT:-}" ] && [ ! -f "$HOME/.ssh/awx_deploy_key" ]; }; then + echo "Missing required inputs." >&2 + echo "Provide --github-repo, --netbox-url, --netbox-token and either --github-pat or a private key at $HOME/.ssh/awx_deploy_key." >&2 + usage + exit 1 +fi + +AUTH="${AWX_USER}:${AWX_PW}" + +api_get(){ + local path=$1 + curl -sS -u "$AUTH" "$AWX_URL/api/v2/$path" +} + +api_post(){ + local path=$1 + local data=$2 + if [ "$DRY_RUN" -ne 0 ]; then + echo "[DRY-RUN] POST $AWX_URL/api/v2/$path -> payload:" >&2 + echo "$data" | jq . >&2 || true + return 0 + fi + curl -sS -u "$AUTH" -H 'Content-Type: application/json' -X POST "$AWX_URL/api/v2/$path" -d "$data" | jq . +} + +echo "AWX: $AWX_URL (user: $AWX_USER)" +echo "Repo: $GITHUB_REPO" +echo "Project: $PROJECT_NAME" +echo "Inventory: $INV_NAME" +echo "Dry-run: $DRY_RUN" + +echo "Listing credential types (to pick SCM type)..." +api_get credential_types/ | jq -r '.results[] | "id:\(.id) name:\(.name) kind:\(.kind)"' + +# User-friendly heuristic to pick SCM credential_type id +SCM_CT_ID=$(api_get credential_types/ | jq -r '.results[] | select((.name|test("Source Control|Git|SCM";"i")) or (.kind == "scm")) | .id' | head -n1) +if [ -z "$SCM_CT_ID" ]; then + echo "Could not auto-detect SCM credential_type id. You must create credentials manually using AWX UI or list credential_types." >&2 +else + echo "Detected SCM credential_type id: $SCM_CT_ID" +fi + +echo "Creating Git credential (token or ssh key)" +if [ -n "${GITHUB_PAT:-}" ]; then + git_cred_payload=$(jq -n --arg name "github-pat" --argjson ct "$SCM_CT_ID" --arg inputs "{\"token\":\"$GITHUB_PAT\"}" '{name:$name, credential_type:$ct, inputs:( $inputs | fromjson ) }') +else + # fall back to SSH private key if available + if [ -f "$HOME/.ssh/awx_deploy_key" ]; then + SSH_KEY_DATA=$(sed -n '1,2000p' "$HOME/.ssh/awx_deploy_key") + # use --arg to safely inject SSH private key text into JSON + git_cred_payload=$(jq -n --arg name "github-ssh" --argjson ct "$SCM_CT_ID" --arg ssh "$SSH_KEY_DATA" '{name:$name, credential_type:$ct, inputs:{ssh_key_data:$ssh}}') + else + echo "No GITHUB_PAT and no $HOME/.ssh/awx_deploy_key - cannot create SCM credential" >&2 + exit 1 + fi +fi +CREDS_OUT=$(api_post credentials/ "$git_cred_payload" || true) +SCM_CRED_ID=$(echo "$CREDS_OUT" | jq -r '.id // empty' 2>/dev/null || true) +if [ -z "$SCM_CRED_ID" ]; then + SCM_CRED_ID=null + echo "SCM credential id: (none - dry-run or failed)" +else + echo "SCM credential id: $SCM_CRED_ID" +fi + +echo "Creating Project pointing to $GITHUB_REPO" +proj_payload=$(jq -n --arg name "$PROJECT_NAME" --arg scm_type "git" --arg scm_url "$GITHUB_REPO" --argjson cred "$SCM_CRED_ID" '{name:$name, scm_type:$scm_type, scm_url:$scm_url, scm_update_on_launch:true, scm_branch:null, scm_clean:false, scm_ref:null, scm_credential:$cred }') +api_post projects/ "$proj_payload" + +echo "Creating Inventory: $INV_NAME" +inv_payload=$(jq -n --arg name "$INV_NAME" '{name:$name, organization:1}') +INV_OUT=$(api_post inventories/ "$inv_payload" || true) +INV_ID=$(echo "$INV_OUT" | jq -r '.id // empty') +if [ -z "$INV_ID" ]; then + echo "Could not create inventory (check output)." >&2 +else + echo "Inventory id: $INV_ID" +fi + +echo "Creating NetBox inventory source for inventory id $INV_ID" +source_vars=$(cat < $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') + + 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 . + 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 . + 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}' diff --git a/scripts/awx_trigger_inventory_update.sh b/scripts/awx_trigger_inventory_update.sh new file mode 100644 index 0000000..6083576 --- /dev/null +++ b/scripts/awx_trigger_inventory_update.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +# awx_trigger_inventory_update.sh +# Trigger an AWX inventory source update and print its stdout. +# Usage: +# AWX_HOST=http://... AWX_USER=admin AWX_PASS=secret SRC_ID=9 ./awx_trigger_inventory_update.sh +# Defaults are set to the local minikube/awx values used in this workspace. + +AWX_HOST="${AWX_HOST:-http://192.168.49.2:30081}" +AWX_USER="${AWX_USER:-admin}" +AWX_PASS="${AWX_PASS:-Aase#1234!}" +SRC_ID="${SRC_ID:-9}" +TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-60}" + +AUTH=("-u" "${AWX_USER}:${AWX_PASS}") + +tmpfile=$(mktemp /tmp/awx_inv_resp.XXXX.json) +cleanup() { rm -f "$tmpfile"; } +trap cleanup EXIT + +echo "Triggering inventory source update for source id=$SRC_ID on $AWX_HOST" + +if ! curl -sS "${AUTH[@]}" -H "Content-Type: application/json" -X POST "$AWX_HOST/api/v2/inventory_sources/$SRC_ID/update/" -o "$tmpfile"; then + echo "Warning: request failed (curl exit non-zero). See contents of $tmpfile:" >&2 + cat "$tmpfile" >&2 || true +fi + +# Try to read inventory_update id from the POST response +INV_UP_ID=$(jq -r '.inventory_update.id // .id // empty' "$tmpfile" 2>/dev/null || true) + +# If not present, poll the inventory_source object to find the active update id +if [ -z "$INV_UP_ID" ]; then + echo "No inventory_update id in initial response, polling inventory_source for current/last update id..." + poll_end=$((SECONDS + 15)) + while [ $SECONDS -lt $poll_end ]; do + src_json=$(curl -sS "${AUTH[@]}" "$AWX_HOST/api/v2/inventory_sources/$SRC_ID/" || true) + # prefer current_update -> summary_fields.current_update -> last_job -> summary_fields.last_job -> last_update + INV_UP_ID=$(echo "$src_json" | jq -r '.current_update.id // .summary_fields.current_update.id // .last_job.id // .summary_fields.last_job.id // .last_update.id // .summary_fields.last_update.id // empty' 2>/dev/null || true) + if [ -n "$INV_UP_ID" ]; then + break + fi + sleep 1 + done +fi + +# Final fallback: use newest inventory_updates list +if [ -z "$INV_UP_ID" ]; then + echo "Falling back to newest inventory_update from list..." + INV_UP_ID=$(curl -sS "${AUTH[@]}" "$AWX_HOST/api/v2/inventory_sources/$SRC_ID/inventory_updates/?page_size=1" | jq -r '.results[0].id // empty' || true) +fi + +if [ -z "$INV_UP_ID" ]; then + echo "ERROR: could not determine inventory_update id. Response was:" >&2 + jq -C '.' "$tmpfile" || cat "$tmpfile" || true + exit 1 +fi + +echo "Inventory update id: $INV_UP_ID" + +end=$((SECONDS + TIMEOUT_SECONDS)) +status="" +while [ $SECONDS -lt $end ]; do + status=$(curl -sS "${AUTH[@]}" "$AWX_HOST/api/v2/inventory_updates/$INV_UP_ID/" | jq -r '.status // empty' || true) + echo "Status: ${status:-unknown}" + case "$status" in + pending|running) + sleep 2 + ;; + *) + break + ;; + esac +done + +echo "Final status: ${status:-unknown}" + +echo "\n--- STDOUT (raw text) for inventory_update $INV_UP_ID ---" +curl -sS "${AUTH[@]}" "$AWX_HOST/api/v2/inventory_updates/$INV_UP_ID/stdout/?format=txt" || true diff --git a/scripts/create_cred_and_ping.sh b/scripts/create_cred_and_ping.sh new file mode 100755 index 0000000..0b1dbc5 --- /dev/null +++ b/scripts/create_cred_and_ping.sh @@ -0,0 +1,99 @@ +#!/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. + +AWX_HOST="${AWX_HOST:-http://192.168.49.2:30081}" +AWX_USER="${AWX_USER:-admin}" +AWX_PASS="${AWX_PASS:-Aase#1234!}" +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}" + +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" + +cat > /tmp/make_cred.py <<'PY' +import json,sys +keypath=sys.argv[1] +name=sys.argv[2] +username=sys.argv[3] +ct=int(sys.argv[4]) +org=int(sys.argv[5]) +with open(keypath,'r',encoding='utf-8') as f: + key=f.read() +payload={'name': name, 'credential_type': ct, 'organization': org, 'inputs': {'username': username, 'ssh_key_data': key}} +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) +echo "HTTP status: $CODE" +cat "$TMP_BODY" || true + +if [ "$CODE" -lt 200 ] || [ "$CODE" -ge 300 ]; then + echo "Failed to create credential (status=$CODE)." >&2 + exit 1 +fi + +NEW_CRED_ID=$(jq -r '.id // empty' "$TMP_BODY") +if [ -z "$NEW_CRED_ID" ]; then + echo "No credential id returned; aborting" >&2 + exit 1 +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" <&2; exit 1; fi + +echo "Launched ad-hoc id=$ADHOC_ID; polling until finished (180s max)" +END=$((SECONDS+180)) +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) + echo "Status: ${STATUS:-unknown}" + if [ -n "$STATUS" ] && [ "$STATUS" != "running" ] && [ "$STATUS" != "pending" ]; then + break + fi + sleep 2 +done + +echo "Final status: ${STATUS:-unknown}" + +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 + +echo "--- stdout (text) ---" +curl -sS -u "$AWX_USER:$AWX_PASS" "$AWX_HOST/api/v2/ad_hoc_commands/$ADHOC_ID/stdout/?format=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)." diff --git a/scripts/try_usernames_awx.sh b/scripts/try_usernames_awx.sh new file mode 100755 index 0000000..8d47132 --- /dev/null +++ b/scripts/try_usernames_awx.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +# try_usernames_awx.sh +# Patch an existing AWX Machine credential (id 5) with candidate usernames +# and run an ad-hoc ping against the three static hosts until one succeeds. + +AWX_HOST="${AWX_HOST:-http://192.168.49.2:30081}" +AWX_USER="${AWX_USER:-admin}" +AWX_PASS="${AWX_PASS:-Aase#1234!}" +CRED_ID="${CRED_ID:-5}" +INV_ID="${INV_ID:-2}" + +USERS=(admin alex alexpolo ubuntu core ec2-user root ansible) + +for u in "${USERS[@]}"; do + echo + echo "===> Trying username=$u" + TMPPATCH=$(mktemp) + # build JSON with a literal "$encrypted$" for ssh_key_data so AWX keeps the stored key + printf '{"inputs":{"username":"%s","ssh_key_data":"$encrypted$"}}' "$u" > "$TMPPATCH" + curl -sS -u "$AWX_USER:$AWX_PASS" -H "Content-Type: application/json" -X PATCH "$AWX_HOST/api/v2/credentials/$CRED_ID/" -d "$TMPPATCH" -o /tmp/patch_resp.json || true + jq -r '.name, .inputs.username' /tmp/patch_resp.json || cat /tmp/patch_resp.json + + TMPADHOC=$(mktemp) + cat > "$TMPADHOC" <