1134 lines
39 KiB
Bash
Executable File
1134 lines
39 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy-awx - Deploy and manage AWX on an Incus cluster
|
|
#
|
|
# Deploys a Debian 12 VM with K3s + AWX Operator, configures AWX for
|
|
# Aether integration (project, inventory, job templates), and provides
|
|
# ongoing health checks and healing.
|
|
#
|
|
# Usage: deploy-awx [OPTIONS]
|
|
# Run 'deploy-awx --help' for full usage information.
|
|
|
|
set -euo pipefail
|
|
|
|
readonly VERSION="0.1.0"
|
|
readonly SCRIPT_NAME="deploy-awx"
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly MANIFESTS_DIR="${SCRIPT_DIR}/awx-manifests"
|
|
readonly AWX_NAMESPACE="awx"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Defaults (overridden by config file or CLI flags)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
VM_NAME="awx"
|
|
TARGET_REMOTE="oc-node-02"
|
|
AWX_IP="192.168.102.161"
|
|
AWX_PREFIX="22"
|
|
AWX_GATEWAY="192.168.100.1"
|
|
AWX_DNS="192.168.100.1"
|
|
AWX_CPU="4"
|
|
AWX_MEMORY="8GiB"
|
|
AWX_DISK="40GiB"
|
|
GIT_REPO="ssh://git@192.168.1.200:2222/maarten/incus-contrib.git"
|
|
GIT_BRANCH="master"
|
|
PLAYBOOK_DIR="playbooks"
|
|
AETHER_URL="https://192.168.102.160:8443"
|
|
AETHER_CLUSTER_ID="52"
|
|
|
|
# Runtime flags
|
|
DRY_RUN=false
|
|
VERBOSE=false
|
|
QUIET=false
|
|
CONFIG_FILE=""
|
|
ACTION=""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Color and formatting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
setup_colors() {
|
|
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
|
|
fi
|
|
}
|
|
|
|
info() { [[ "$QUIET" == true ]] && return; echo -e "${BLUE}[info]${RESET} $*"; }
|
|
success() { [[ "$QUIET" == true ]] && return; echo -e "${GREEN}[ok]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
|
|
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
|
step() { [[ "$QUIET" == true ]] && return; echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
|
|
detail() { [[ "$VERBOSE" != true ]] && return; echo -e "${DIM} $*${RESET}"; }
|
|
|
|
dry_run_guard() {
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
info "(dry-run) $*"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Help
|
|
# ---------------------------------------------------------------------------
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Deploy and manage AWX on an Incus cluster
|
|
|
|
${BOLD}USAGE${RESET}
|
|
${SCRIPT_NAME} [OPTIONS]
|
|
|
|
${BOLD}ACTIONS${RESET}
|
|
--deploy Full deploy: create VM, install K3s, deploy AWX, configure
|
|
--status Check AWX health, K3s pods, Aether connection
|
|
--heal Restart failed pods, re-sync project, re-validate templates
|
|
--configure (Re-)configure AWX: project, inventory, templates
|
|
--join-aether Register AWX endpoint in Aether + bind to cluster
|
|
--cleanup Destroy the AWX VM
|
|
--doctor Check prerequisites (incus CLI, curl, python3, kubectl)
|
|
|
|
${BOLD}OPTIONS${RESET}
|
|
-c, --config FILE Configuration file (YAML with awx: section)
|
|
-n, --dry-run Preview actions without executing
|
|
-v, --verbose Show detailed output
|
|
-q, --quiet Suppress informational output
|
|
-h, --help Show this help message
|
|
|
|
${BOLD}CONFIGURATION${RESET}
|
|
The script uses built-in defaults for the lab environment. Override with
|
|
a YAML config file containing an 'awx:' section:
|
|
|
|
awx:
|
|
vm_name: awx
|
|
target_node: oc-node-02
|
|
ip: 192.168.102.161/22
|
|
gateway: 192.168.100.1
|
|
dns: 192.168.100.1
|
|
cpu: 4
|
|
memory: 8GiB
|
|
disk: 40GiB
|
|
git_repo: ssh://git@192.168.1.200:2222/maarten/incus-contrib.git
|
|
git_branch: master
|
|
aether_url: https://192.168.102.160:8443
|
|
aether_cluster_id: 52
|
|
|
|
${BOLD}EXAMPLES${RESET}
|
|
# Check prerequisites
|
|
${SCRIPT_NAME} --doctor
|
|
|
|
# Full deployment with defaults
|
|
${SCRIPT_NAME} --deploy
|
|
|
|
# Preview deployment without executing
|
|
${SCRIPT_NAME} --deploy --dry-run
|
|
|
|
# Check AWX health
|
|
${SCRIPT_NAME} --status
|
|
|
|
# Heal broken pods
|
|
${SCRIPT_NAME} --heal
|
|
|
|
# Reconfigure AWX (project, templates)
|
|
${SCRIPT_NAME} --configure
|
|
|
|
# Register with Aether
|
|
${SCRIPT_NAME} --join-aether
|
|
|
|
# Destroy AWX VM
|
|
${SCRIPT_NAME} --cleanup
|
|
EOF
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config parsing (simple YAML — same pattern as other scripts)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
parse_config() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
error "Config file not found: $file"
|
|
exit 1
|
|
fi
|
|
|
|
# Extract awx: section values using grep + sed (no yq dependency)
|
|
local val
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *vm_name:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && VM_NAME="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *target_node:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && TARGET_REMOTE="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *ip:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
if [[ -n "$val" ]]; then
|
|
AWX_IP="${val%%/*}"
|
|
[[ "$val" == */* ]] && AWX_PREFIX="${val#*/}"
|
|
fi
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *gateway:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AWX_GATEWAY="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *dns:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AWX_DNS="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *cpu:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AWX_CPU="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *memory:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AWX_MEMORY="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *disk:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AWX_DISK="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *git_repo:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && GIT_REPO="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *git_branch:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && GIT_BRANCH="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *aether_url:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AETHER_URL="$val"
|
|
val=$(grep -A50 '^awx:' "$file" | grep '^ *aether_cluster_id:' | head -1 | sed 's/.*: *//' | tr -d '"' || true)
|
|
[[ -n "$val" ]] && AETHER_CLUSTER_ID="$val"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
vm_ref() {
|
|
echo "${TARGET_REMOTE}:${VM_NAME}"
|
|
}
|
|
|
|
vm_exists() {
|
|
incus info "$(vm_ref)" &>/dev/null 2>&1
|
|
}
|
|
|
|
vm_running() {
|
|
local state
|
|
state=$(incus info "$(vm_ref)" 2>/dev/null | grep -i '^status:' | awk '{print $2}')
|
|
[[ "$state" == "RUNNING" || "$state" == "Running" ]]
|
|
}
|
|
|
|
vm_exec() {
|
|
incus exec "$(vm_ref)" -- "$@"
|
|
}
|
|
|
|
awx_url() {
|
|
# AWX is exposed via K3s NodePort on port 30080 (HTTP)
|
|
# Traefik ingress returns 404 for IP-based access
|
|
echo "http://${AWX_IP}:30080"
|
|
}
|
|
|
|
awx_api() {
|
|
local method="$1" path="$2"
|
|
shift 2
|
|
curl -sk -X "$method" "$(awx_url)${path}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
awx_api_auth() {
|
|
local method="$1" path="$2"
|
|
shift 2
|
|
local token
|
|
token=$(get_awx_admin_password)
|
|
curl -sk -X "$method" "$(awx_url)${path}" \
|
|
--user "admin:${token}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
get_awx_admin_password() {
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" get secret awx-admin-password \
|
|
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d
|
|
}
|
|
|
|
wait_for_vm_agent() {
|
|
local timeout="${1:-120}"
|
|
local elapsed=0
|
|
info "Waiting for VM agent (up to ${timeout}s)..."
|
|
while ! vm_exec true &>/dev/null 2>&1; do
|
|
sleep 5
|
|
elapsed=$((elapsed + 5))
|
|
if [[ $elapsed -ge $timeout ]]; then
|
|
error "VM agent not available after ${timeout}s"
|
|
return 1
|
|
fi
|
|
done
|
|
success "VM agent connected (${elapsed}s)"
|
|
}
|
|
|
|
wait_for_awx_web() {
|
|
local timeout="${1:-600}"
|
|
local elapsed=0
|
|
info "Waiting for AWX web interface (up to ${timeout}s)..."
|
|
while ! curl -sk -o /dev/null -w '%{http_code}' "$(awx_url)/api/v2/ping/" 2>/dev/null | grep -q '200'; do
|
|
sleep 10
|
|
elapsed=$((elapsed + 10))
|
|
if [[ $elapsed -ge $timeout ]]; then
|
|
error "AWX web not responding after ${timeout}s"
|
|
return 1
|
|
fi
|
|
[[ "$VERBOSE" == true ]] && detail "Still waiting... (${elapsed}s)"
|
|
done
|
|
success "AWX web interface ready (${elapsed}s)"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Doctor — prerequisite checks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_doctor() {
|
|
step "Checking prerequisites"
|
|
local ok=true
|
|
|
|
# incus CLI
|
|
if command -v incus &>/dev/null; then
|
|
success "incus CLI: $(incus version 2>/dev/null || echo 'installed')"
|
|
else
|
|
error "incus CLI not found — install the Incus client"
|
|
ok=false
|
|
fi
|
|
|
|
# curl
|
|
if command -v curl &>/dev/null; then
|
|
success "curl: $(curl --version | head -1 | awk '{print $2}')"
|
|
else
|
|
error "curl not found"
|
|
ok=false
|
|
fi
|
|
|
|
# python3
|
|
if command -v python3 &>/dev/null; then
|
|
success "python3: $(python3 --version 2>&1 | awk '{print $2}')"
|
|
else
|
|
error "python3 not found"
|
|
ok=false
|
|
fi
|
|
|
|
# base64
|
|
if command -v base64 &>/dev/null; then
|
|
success "base64: available"
|
|
else
|
|
error "base64 not found"
|
|
ok=false
|
|
fi
|
|
|
|
# Manifest files
|
|
if [[ -d "$MANIFESTS_DIR" ]]; then
|
|
success "AWX manifests: $MANIFESTS_DIR"
|
|
else
|
|
error "AWX manifests not found at $MANIFESTS_DIR"
|
|
ok=false
|
|
fi
|
|
|
|
# Target remote
|
|
if incus remote list -f csv 2>/dev/null | grep -q "^${TARGET_REMOTE},"; then
|
|
success "Incus remote '${TARGET_REMOTE}': reachable"
|
|
else
|
|
warn "Incus remote '${TARGET_REMOTE}' not found (needed for deployment)"
|
|
fi
|
|
|
|
# Existing VM
|
|
if vm_exists; then
|
|
info "AWX VM '$(vm_ref)' exists"
|
|
if vm_running; then
|
|
success " VM is running"
|
|
# Check K3s
|
|
if vm_exec kubectl get nodes &>/dev/null 2>&1; then
|
|
success " K3s is responding"
|
|
else
|
|
warn " K3s not responding"
|
|
fi
|
|
# Check AWX pods
|
|
local pod_count
|
|
pod_count=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods --no-headers 2>/dev/null | wc -l || echo 0)
|
|
info " AWX pods: ${pod_count}"
|
|
else
|
|
warn " VM is not running"
|
|
fi
|
|
else
|
|
info "AWX VM '$(vm_ref)' does not exist (--deploy to create)"
|
|
fi
|
|
|
|
echo
|
|
if [[ "$ok" == true ]]; then
|
|
success "All prerequisites satisfied"
|
|
else
|
|
error "Some prerequisites missing — fix the issues above"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deploy — full deployment pipeline
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_deploy() {
|
|
step "Deploying AWX on $(vm_ref)"
|
|
echo
|
|
info " Target: ${TARGET_REMOTE}"
|
|
info " VM: ${VM_NAME}"
|
|
info " IP: ${AWX_IP}/${AWX_PREFIX}"
|
|
info " CPU: ${AWX_CPU}"
|
|
info " Memory: ${AWX_MEMORY}"
|
|
info " Disk: ${AWX_DISK}"
|
|
echo
|
|
|
|
if vm_exists; then
|
|
if vm_running; then
|
|
error "AWX VM already exists and is running. Use --status, --heal, or --cleanup first."
|
|
return 1
|
|
else
|
|
warn "AWX VM exists but is not running — starting it"
|
|
if dry_run_guard "incus start $(vm_ref)"; then
|
|
incus start "$(vm_ref)"
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
phase_create_vm
|
|
phase_configure_network
|
|
phase_install_k3s
|
|
phase_deploy_awx_operator
|
|
phase_deploy_awx_instance
|
|
phase_verify
|
|
|
|
echo
|
|
success "AWX deployment complete!"
|
|
info " Web UI: $(awx_url)/"
|
|
info " API: $(awx_url)/api/v2/ping/"
|
|
local pw
|
|
pw=$(get_awx_admin_password 2>/dev/null || echo "<retrieve with --status>")
|
|
info " Admin: admin / ${pw}"
|
|
echo
|
|
info "Next steps:"
|
|
info " ${SCRIPT_NAME} --configure # set up project, inventory, templates"
|
|
info " ${SCRIPT_NAME} --join-aether # register with Aether"
|
|
}
|
|
|
|
phase_create_vm() {
|
|
step "Phase 1: Create VM"
|
|
|
|
if ! dry_run_guard "incus launch images:debian/12 $(vm_ref) --vm --target ${TARGET_REMOTE} ..."; then
|
|
return 0
|
|
fi
|
|
|
|
incus launch images:debian/12 "$(vm_ref)" --vm \
|
|
--target "$TARGET_REMOTE" \
|
|
-c "limits.cpu=${AWX_CPU}" \
|
|
-c "limits.memory=${AWX_MEMORY}" \
|
|
-d "root,size=${AWX_DISK}"
|
|
|
|
success "VM created"
|
|
detail "Waiting 10s for initial boot..."
|
|
sleep 10
|
|
|
|
# Switch to macvlan for direct VLAN access
|
|
info "Configuring macvlan NIC (parent=mgmt)"
|
|
incus config device remove "$(vm_ref)" eth0 2>/dev/null || true
|
|
incus config device add "$(vm_ref)" eth0 nic \
|
|
nictype=macvlan parent=mgmt
|
|
|
|
success "NIC switched to macvlan — VM will restart networking"
|
|
}
|
|
|
|
phase_configure_network() {
|
|
step "Phase 2: Configure static IP + hostname"
|
|
|
|
wait_for_vm_agent 120
|
|
|
|
if ! dry_run_guard "Configure networking on $(vm_ref)"; then
|
|
return 0
|
|
fi
|
|
|
|
# Debian 12 images use systemd-networkd (not netplan)
|
|
# The macvlan NIC appears as enp5s0 in Incus VMs
|
|
vm_exec bash -c "
|
|
hostnamectl set-hostname '${VM_NAME}'
|
|
|
|
# Disable any existing DHCP config
|
|
rm -f /etc/network/interfaces.d/* 2>/dev/null || true
|
|
|
|
# Configure static IP via systemd-networkd
|
|
mkdir -p /etc/systemd/network
|
|
cat > /etc/systemd/network/10-static.network << 'NETWORKD'
|
|
[Match]
|
|
Name=enp5s0
|
|
|
|
[Network]
|
|
Address=${AWX_IP}/${AWX_PREFIX}
|
|
Gateway=${AWX_GATEWAY}
|
|
DNS=${AWX_DNS}
|
|
NETWORKD
|
|
|
|
systemctl enable systemd-networkd
|
|
systemctl restart systemd-networkd
|
|
"
|
|
|
|
success "Static IP ${AWX_IP}/${AWX_PREFIX} configured"
|
|
|
|
# Verify connectivity
|
|
detail "Verifying network connectivity..."
|
|
sleep 3
|
|
if vm_exec ping -c 1 -W 3 "${AWX_GATEWAY}" &>/dev/null; then
|
|
success "Gateway reachable from VM"
|
|
else
|
|
warn "Cannot reach gateway — check VLAN/network config"
|
|
fi
|
|
}
|
|
|
|
phase_install_k3s() {
|
|
step "Phase 3: Install K3s"
|
|
|
|
if ! dry_run_guard "Install K3s on $(vm_ref)"; then
|
|
return 0
|
|
fi
|
|
|
|
# Ensure curl is available (minimal Debian 12 may not have it)
|
|
info "Installing prerequisites..."
|
|
vm_exec bash -c "
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq curl python3 2>&1 | tail -1
|
|
"
|
|
|
|
vm_exec bash -c "
|
|
curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644
|
|
"
|
|
success "K3s installed"
|
|
|
|
info "Waiting for K3s node to be ready..."
|
|
vm_exec kubectl wait --for=condition=Ready node --all --timeout=120s
|
|
success "K3s node ready"
|
|
}
|
|
|
|
phase_deploy_awx_operator() {
|
|
step "Phase 4: Deploy AWX Operator"
|
|
|
|
if ! dry_run_guard "Deploy AWX Operator on $(vm_ref)"; then
|
|
return 0
|
|
fi
|
|
|
|
# Create namespace
|
|
vm_exec kubectl create namespace "$AWX_NAMESPACE" 2>/dev/null || true
|
|
|
|
# Push kustomization files
|
|
info "Pushing operator manifests..."
|
|
incus file push "${MANIFESTS_DIR}/operator/kustomization.yaml" \
|
|
"$(vm_ref)/tmp/awx-operator-kustomization.yaml"
|
|
|
|
vm_exec bash -c "
|
|
mkdir -p /opt/awx/operator
|
|
cp /tmp/awx-operator-kustomization.yaml /opt/awx/operator/kustomization.yaml
|
|
kubectl apply -k /opt/awx/operator/
|
|
"
|
|
|
|
info "Waiting for AWX Operator (up to 5 min)..."
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" wait \
|
|
--for=condition=Available \
|
|
deployment/awx-operator-controller-manager \
|
|
--timeout=300s
|
|
|
|
success "AWX Operator deployed"
|
|
}
|
|
|
|
phase_deploy_awx_instance() {
|
|
step "Phase 5: Deploy AWX instance"
|
|
|
|
if ! dry_run_guard "Deploy AWX instance on $(vm_ref)"; then
|
|
return 0
|
|
fi
|
|
|
|
# Push AWX CR manifests
|
|
info "Pushing AWX instance manifests..."
|
|
incus file push "${MANIFESTS_DIR}/base/awx.yaml" \
|
|
"$(vm_ref)/tmp/awx-cr.yaml"
|
|
incus file push "${MANIFESTS_DIR}/base/kustomization.yaml" \
|
|
"$(vm_ref)/tmp/awx-kustomization.yaml"
|
|
|
|
vm_exec bash -c "
|
|
mkdir -p /opt/awx/base
|
|
cp /tmp/awx-cr.yaml /opt/awx/base/awx.yaml
|
|
cp /tmp/awx-kustomization.yaml /opt/awx/base/kustomization.yaml
|
|
kubectl apply -k /opt/awx/base/
|
|
"
|
|
|
|
info "AWX instance deploying — this takes 5-10 minutes..."
|
|
info "Waiting for AWX pods to start..."
|
|
|
|
# Wait for AWX deployment to exist and become available
|
|
local elapsed=0
|
|
local timeout=600
|
|
while ! vm_exec kubectl -n "$AWX_NAMESPACE" get deployment awx-web &>/dev/null 2>&1; do
|
|
sleep 15
|
|
elapsed=$((elapsed + 15))
|
|
if [[ $elapsed -ge $timeout ]]; then
|
|
error "AWX web deployment not created after ${timeout}s"
|
|
warn "Check pod status: incus exec $(vm_ref) -- kubectl -n awx get pods"
|
|
return 1
|
|
fi
|
|
[[ "$VERBOSE" == true ]] && detail "Waiting for AWX web deployment... (${elapsed}s)"
|
|
done
|
|
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" wait \
|
|
--for=condition=Available \
|
|
deployment/awx-web \
|
|
--timeout=300s || {
|
|
warn "AWX web deployment not fully available yet — checking pods"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" get pods
|
|
}
|
|
|
|
success "AWX instance deployed"
|
|
}
|
|
|
|
phase_verify() {
|
|
step "Phase 6: Verify AWX"
|
|
|
|
wait_for_awx_web 300
|
|
|
|
local admin_pw
|
|
admin_pw=$(get_awx_admin_password)
|
|
success "Admin password retrieved"
|
|
|
|
# Test API ping (NodePort 30080)
|
|
local ping_result
|
|
ping_result=$(curl -sk "$(awx_url)/api/v2/ping/" 2>/dev/null)
|
|
if echo "$ping_result" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then
|
|
success "AWX API responding"
|
|
else
|
|
warn "AWX API returned unexpected response"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Status — health checks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_status() {
|
|
step "AWX Status for $(vm_ref)"
|
|
echo
|
|
|
|
# VM existence and state
|
|
if ! vm_exists; then
|
|
error "AWX VM does not exist — use --deploy to create"
|
|
return 1
|
|
fi
|
|
|
|
if ! vm_running; then
|
|
error "AWX VM exists but is not running"
|
|
return 1
|
|
fi
|
|
success "VM: running"
|
|
|
|
# K3s
|
|
if vm_exec kubectl get nodes &>/dev/null 2>&1; then
|
|
success "K3s: healthy"
|
|
if [[ "$VERBOSE" == true ]]; then
|
|
vm_exec kubectl get nodes -o wide
|
|
fi
|
|
else
|
|
error "K3s: not responding"
|
|
return 1
|
|
fi
|
|
|
|
# AWX pods
|
|
echo
|
|
info "AWX pods:"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" get pods -o wide 2>/dev/null || warn "Cannot list pods"
|
|
|
|
# Pod health summary
|
|
local total running
|
|
total=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods --no-headers 2>/dev/null | wc -l || echo 0)
|
|
running=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods --no-headers 2>/dev/null | grep -c 'Running' || echo 0)
|
|
echo
|
|
if [[ "$total" -gt 0 && "$running" -eq "$total" ]]; then
|
|
success "All ${total} pods running"
|
|
else
|
|
warn "Pods: ${running}/${total} running"
|
|
fi
|
|
|
|
# AWX web (NodePort 30080)
|
|
local http_code
|
|
http_code=$(curl -sk -o /dev/null -w '%{http_code}' "$(awx_url)/api/v2/ping/" 2>/dev/null || echo "000")
|
|
if [[ "$http_code" == "200" ]]; then
|
|
success "AWX API: responding at $(awx_url) (HTTP ${http_code})"
|
|
else
|
|
error "AWX API: not responding (HTTP ${http_code})"
|
|
fi
|
|
|
|
# Admin password
|
|
local admin_pw
|
|
admin_pw=$(get_awx_admin_password 2>/dev/null || echo "")
|
|
if [[ -n "$admin_pw" ]]; then
|
|
success "Admin password: ${admin_pw}"
|
|
else
|
|
warn "Cannot retrieve admin password"
|
|
fi
|
|
|
|
# AWX version
|
|
local version
|
|
version=$(curl -sk "$(awx_url)/api/v2/ping/" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','unknown'))" 2>/dev/null || echo "unknown")
|
|
info "AWX version: ${version}"
|
|
|
|
# Aether connectivity
|
|
echo
|
|
local aether_code
|
|
aether_code=$(curl -sk -o /dev/null -w '%{http_code}' "${AETHER_URL}/api/health" 2>/dev/null || echo "000")
|
|
if [[ "$aether_code" == "200" || "$aether_code" == "401" || "$aether_code" == "302" ]]; then
|
|
success "Aether: reachable at ${AETHER_URL}"
|
|
else
|
|
warn "Aether: not reachable (HTTP ${aether_code})"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Heal — fix common issues
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_heal() {
|
|
step "Healing AWX on $(vm_ref)"
|
|
echo
|
|
|
|
if ! vm_exists || ! vm_running; then
|
|
error "AWX VM is not running"
|
|
return 1
|
|
fi
|
|
|
|
# 1. K3s service
|
|
info "Checking K3s service..."
|
|
if vm_exec systemctl is-active k3s &>/dev/null; then
|
|
success "K3s service: active"
|
|
else
|
|
warn "K3s service not active — restarting"
|
|
if dry_run_guard "systemctl restart k3s"; then
|
|
vm_exec systemctl restart k3s
|
|
sleep 10
|
|
fi
|
|
fi
|
|
|
|
# 2. Unhealthy pods
|
|
info "Checking for unhealthy pods..."
|
|
local unhealthy
|
|
unhealthy=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods --no-headers 2>/dev/null \
|
|
| grep -vE 'Running|Completed' | awk '{print $1}' || true)
|
|
if [[ -n "$unhealthy" ]]; then
|
|
warn "Unhealthy pods found — deleting for operator to recreate"
|
|
for pod in $unhealthy; do
|
|
info " Deleting pod: $pod"
|
|
if dry_run_guard "kubectl delete pod $pod"; then
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" delete pod "$pod" --grace-period=30
|
|
fi
|
|
done
|
|
info "Waiting 30s for pods to restart..."
|
|
sleep 30
|
|
else
|
|
success "All pods healthy"
|
|
fi
|
|
|
|
# 3. AWX web responsiveness (NodePort 30080)
|
|
info "Checking AWX web..."
|
|
local http_code
|
|
http_code=$(curl -sk -o /dev/null -w '%{http_code}' "$(awx_url)/api/v2/ping/" 2>/dev/null || echo "000")
|
|
if [[ "$http_code" == "200" ]]; then
|
|
success "AWX web: responding"
|
|
else
|
|
warn "AWX web not responding — restarting deployment"
|
|
if dry_run_guard "kubectl rollout restart deployment/awx-web"; then
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" rollout restart deployment/awx-web 2>/dev/null || true
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" rollout restart deployment/awx-task 2>/dev/null || true
|
|
wait_for_awx_web 300
|
|
fi
|
|
fi
|
|
|
|
# 4. Project sync
|
|
info "Triggering project sync..."
|
|
local project_id
|
|
project_id=$(awx_api_auth GET "/api/v2/projects/?name=incus-contrib" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
if [[ -n "$project_id" ]]; then
|
|
if dry_run_guard "POST /api/v2/projects/${project_id}/update/"; then
|
|
awx_api_auth POST "/api/v2/projects/${project_id}/update/" 2>/dev/null || true
|
|
success "Project sync triggered (ID: ${project_id})"
|
|
fi
|
|
else
|
|
info "No project configured yet — use --configure"
|
|
fi
|
|
|
|
echo
|
|
success "Heal complete"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configure — set up AWX project, inventory, templates
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_configure() {
|
|
step "Configuring AWX"
|
|
echo
|
|
|
|
if ! vm_running; then
|
|
error "AWX VM is not running"
|
|
return 1
|
|
fi
|
|
|
|
wait_for_awx_web 60
|
|
|
|
local admin_pw
|
|
admin_pw=$(get_awx_admin_password)
|
|
detail "Admin password: ${admin_pw}"
|
|
|
|
# Helper for authenticated API calls with password
|
|
awx_auth() {
|
|
local method="$1" path="$2"
|
|
shift 2
|
|
curl -sk -X "$method" "$(awx_url)${path}" \
|
|
--user "admin:${admin_pw}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
# 1. Push Incus client cert/key to AWX project directory
|
|
# Playbooks use Incus REST API (not SSH) — cert is required.
|
|
# AWX EE mounts project dir at /runner/project/ during job execution.
|
|
step "Pushing Incus client certificate to AWX"
|
|
local incus_cert="${HOME}/.config/incus/client.crt"
|
|
local incus_key="${HOME}/.config/incus/client.key"
|
|
if [[ -f "$incus_cert" && -f "$incus_key" ]]; then
|
|
local task_pod
|
|
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
|
-l app.kubernetes.io/component=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
|
if [[ -n "$task_pod" ]]; then
|
|
local project_dir="/var/lib/awx/projects/incus-contrib"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- mkdir -p "$project_dir"
|
|
# Push cert via base64 to avoid file push permission issues
|
|
local cert_b64 key_b64
|
|
cert_b64=$(base64 -w0 "$incus_cert")
|
|
key_b64=$(base64 -w0 "$incus_key")
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
|
bash -c "echo '${cert_b64}' | base64 -d > ${project_dir}/incus-client.crt"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
|
bash -c "echo '${key_b64}' | base64 -d > ${project_dir}/incus-client.key"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
|
chmod 600 "${project_dir}/incus-client.key"
|
|
success "Incus client cert/key pushed to AWX task pod"
|
|
else
|
|
warn "Cannot find AWX task pod — push cert manually later"
|
|
fi
|
|
else
|
|
warn "Incus client cert not found at ${incus_cert}"
|
|
warn "Playbooks need the cert to talk to Incus API — push it manually"
|
|
fi
|
|
|
|
# 2. Create manual project (local_path)
|
|
# AWX EE containers cannot reach the private git repo, so we use a
|
|
# manual project with files pushed directly to the task pod.
|
|
step "Creating project"
|
|
local project_id
|
|
project_id=$(awx_auth GET "/api/v2/projects/?name=incus-contrib" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
|
|
if [[ -z "$project_id" ]]; then
|
|
project_id=$(awx_auth POST "/api/v2/projects/" \
|
|
-d '{"name":"incus-contrib","organization":1,"scm_type":"","local_path":"incus-contrib"}' 2>/dev/null \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
[[ -n "$project_id" ]] && success "Project created (ID: ${project_id}, local_path)" || warn "Failed to create project"
|
|
else
|
|
success "Project exists (ID: ${project_id})"
|
|
fi
|
|
|
|
# Push playbooks to AWX task pod project directory
|
|
if [[ -n "$project_id" ]]; then
|
|
local task_pod
|
|
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
|
-l app.kubernetes.io/component=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
|
if [[ -n "$task_pod" ]]; then
|
|
local project_dir="/var/lib/awx/projects/incus-contrib"
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- mkdir -p "${project_dir}/playbooks"
|
|
for playbook in post-deploy.yml decommission.yml; do
|
|
local pb_path="${SCRIPT_DIR}/../ansible/playbooks/${playbook}"
|
|
if [[ -f "$pb_path" ]]; then
|
|
local pb_b64
|
|
pb_b64=$(base64 -w0 "$pb_path")
|
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
|
bash -c "echo '${pb_b64}' | base64 -d > ${project_dir}/playbooks/${playbook}"
|
|
detail "Pushed ${playbook}"
|
|
else
|
|
warn "Playbook not found: ${pb_path}"
|
|
fi
|
|
done
|
|
success "Playbooks pushed to AWX project directory"
|
|
else
|
|
warn "Cannot find AWX task pod — push playbooks manually"
|
|
fi
|
|
fi
|
|
|
|
# 3. Create inventory
|
|
step "Creating inventory"
|
|
local inventory_id
|
|
inventory_id=$(awx_auth GET "/api/v2/inventories/?name=incus-instances" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
|
|
if [[ -z "$inventory_id" ]]; then
|
|
inventory_id=$(awx_auth POST "/api/v2/inventories/" \
|
|
-d '{"name":"incus-instances","organization":1}' 2>/dev/null \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
[[ -n "$inventory_id" ]] && success "Inventory created (ID: ${inventory_id})" || warn "Failed to create inventory"
|
|
else
|
|
success "Inventory exists (ID: ${inventory_id})"
|
|
fi
|
|
|
|
# 4. Create machine credential (required by AWX templates, though
|
|
# playbooks use Incus API not SSH)
|
|
step "Creating machine credential"
|
|
local machine_cred_id
|
|
machine_cred_id=$(awx_auth GET "/api/v2/credentials/?name=incus-instances" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
|
|
if [[ -z "$machine_cred_id" ]]; then
|
|
machine_cred_id=$(awx_auth POST "/api/v2/credentials/" \
|
|
-d '{"name":"incus-instances","organization":1,"credential_type":1,"inputs":{"username":"root"}}' 2>/dev/null \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
[[ -n "$machine_cred_id" ]] && success "Machine credential created (ID: ${machine_cred_id})" || warn "Failed to create machine credential"
|
|
else
|
|
success "Machine credential exists (ID: ${machine_cred_id})"
|
|
fi
|
|
|
|
# 5. Create job templates
|
|
step "Creating job templates"
|
|
for tmpl_name in post-deploy decommission; do
|
|
local tmpl_id
|
|
tmpl_id=$(awx_auth GET "/api/v2/job_templates/?name=${tmpl_name}" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
|
|
if [[ -z "$tmpl_id" ]]; then
|
|
local tmpl_payload
|
|
tmpl_payload=$(python3 -c "
|
|
import json
|
|
d = {
|
|
'name': '${tmpl_name}',
|
|
'organization': 1,
|
|
'job_type': 'run',
|
|
'ask_variables_on_launch': True,
|
|
'playbook': '${PLAYBOOK_DIR}/${tmpl_name}.yml'
|
|
}
|
|
pid = '${project_id}'
|
|
iid = '${inventory_id}'
|
|
cid = '${machine_cred_id}'
|
|
if pid: d['project'] = int(pid)
|
|
if iid: d['inventory'] = int(iid)
|
|
print(json.dumps(d))")
|
|
|
|
tmpl_id=$(awx_auth POST "/api/v2/job_templates/" -d "$tmpl_payload" 2>/dev/null \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
|
|
# Associate credential separately (templates use credential list)
|
|
if [[ -n "$tmpl_id" && -n "$machine_cred_id" ]]; then
|
|
awx_auth POST "/api/v2/job_templates/${tmpl_id}/credentials/" \
|
|
-d "{\"id\":${machine_cred_id}}" 2>/dev/null || true
|
|
fi
|
|
|
|
[[ -n "$tmpl_id" ]] && success "Template '${tmpl_name}' created (ID: ${tmpl_id})" || warn "Failed to create template '${tmpl_name}'"
|
|
else
|
|
success "Template '${tmpl_name}' exists (ID: ${tmpl_id})"
|
|
fi
|
|
done
|
|
|
|
echo
|
|
success "AWX configuration complete"
|
|
echo
|
|
info "Template IDs (for Aether binding):"
|
|
for tmpl_name in post-deploy decommission; do
|
|
local tid
|
|
tid=$(awx_auth GET "/api/v2/job_templates/?name=${tmpl_name}" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
info " ${tmpl_name}: ${tid:-unknown}"
|
|
done
|
|
echo
|
|
info "Next: ${SCRIPT_NAME} --join-aether"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Join Aether — register AWX endpoint + bind to cluster
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_join_aether() {
|
|
step "Registering AWX with Aether"
|
|
echo
|
|
|
|
if ! vm_running; then
|
|
error "AWX VM is not running"
|
|
return 1
|
|
fi
|
|
|
|
# 1. Create Personal Access Token
|
|
step "Creating AWX Personal Access Token"
|
|
local admin_pw
|
|
admin_pw=$(get_awx_admin_password)
|
|
|
|
local pat_response
|
|
pat_response=$(curl -sk -X POST "$(awx_url)/api/v2/users/1/personal_tokens/" \
|
|
--user "admin:${admin_pw}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"description": "Aether integration", "scope": "write"}' 2>/dev/null)
|
|
|
|
local awx_token
|
|
awx_token=$(echo "$pat_response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || true)
|
|
|
|
if [[ -z "$awx_token" ]]; then
|
|
error "Failed to create PAT — check AWX admin credentials"
|
|
detail "Response: ${pat_response}"
|
|
return 1
|
|
fi
|
|
success "PAT created"
|
|
|
|
# 2. Get template IDs
|
|
local post_deploy_id decommission_id
|
|
post_deploy_id=$(curl -sk "$(awx_url)/api/v2/job_templates/?name=post-deploy" \
|
|
--user "admin:${admin_pw}" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
decommission_id=$(curl -sk "$(awx_url)/api/v2/job_templates/?name=decommission" \
|
|
--user "admin:${admin_pw}" 2>/dev/null \
|
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
|
|
|
info "Post-deploy template ID: ${post_deploy_id:-not found}"
|
|
info "Decommission template ID: ${decommission_id:-not found}"
|
|
|
|
echo
|
|
info "To complete Aether integration:"
|
|
echo
|
|
info "1. Log into Aether at ${AETHER_URL}"
|
|
info "2. Navigate to Settings → Ansible Automation (/awx-endpoints)"
|
|
info "3. Add Endpoint:"
|
|
info " Name: lab-awx"
|
|
info " URL: $(awx_url)"
|
|
info " Token: ${awx_token}"
|
|
info " Verify SSL: unchecked"
|
|
info "4. Configure cluster AWX binding:"
|
|
info " Post-deploy template ID: ${post_deploy_id:-<configure first>}"
|
|
info " Decommission template ID: ${decommission_id:-<configure first>}"
|
|
info " Job timeout: 600 seconds"
|
|
echo
|
|
|
|
# Print API commands for scripted registration
|
|
if [[ "$VERBOSE" == true ]]; then
|
|
detail "API commands for scripted registration:"
|
|
detail ""
|
|
detail "# Register endpoint:"
|
|
detail "curl -sSk -b cookies.txt -H 'X-CSRF-Token: \$CSRF' \\"
|
|
detail " -H 'Referer: ${AETHER_URL}/' \\"
|
|
detail " -X POST ${AETHER_URL}/api/awx/endpoints \\"
|
|
detail " -H 'Content-Type: application/json' \\"
|
|
detail " -d '{\"name\":\"lab-awx\",\"url\":\"http://${AWX_IP}:30080\",\"token\":\"${awx_token}\",\"verify_ssl\":false}'"
|
|
detail ""
|
|
detail "# Bind cluster (replace ENDPOINT_ID):"
|
|
detail "curl -sSk -b cookies.txt -H 'X-CSRF-Token: \$CSRF' \\"
|
|
detail " -H 'Referer: ${AETHER_URL}/' \\"
|
|
detail " -X PUT ${AETHER_URL}/api/clusters/${AETHER_CLUSTER_ID}/awx-config \\"
|
|
detail " -H 'Content-Type: application/json' \\"
|
|
detail " -d '{\"cluster_id\":${AETHER_CLUSTER_ID},\"awx_endpoint_id\":ENDPOINT_ID,\"awx_post_deploy_template_id\":${post_deploy_id:-0},\"awx_decommission_template_id\":${decommission_id:-0},\"awx_job_timeout_seconds\":600}'"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cleanup — destroy AWX VM
|
|
# ---------------------------------------------------------------------------
|
|
|
|
action_cleanup() {
|
|
step "Cleaning up AWX VM"
|
|
|
|
if ! vm_exists; then
|
|
info "AWX VM does not exist — nothing to clean up"
|
|
return 0
|
|
fi
|
|
|
|
echo
|
|
warn "This will permanently destroy the AWX VM '$(vm_ref)'"
|
|
warn "All AWX data (jobs, templates, credentials) will be lost."
|
|
echo
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
info "(dry-run) Would destroy $(vm_ref)"
|
|
return 0
|
|
fi
|
|
|
|
read -r -p "Type 'yes' to confirm: " confirm
|
|
if [[ "$confirm" != "yes" ]]; then
|
|
info "Aborted"
|
|
return 0
|
|
fi
|
|
|
|
if vm_running; then
|
|
info "Stopping VM..."
|
|
incus stop "$(vm_ref)" --force 2>/dev/null || true
|
|
sleep 3
|
|
fi
|
|
|
|
info "Deleting VM..."
|
|
incus delete "$(vm_ref)" --force 2>/dev/null || true
|
|
|
|
success "AWX VM destroyed"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Argument parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--deploy) ACTION="deploy";;
|
|
--status) ACTION="status";;
|
|
--heal) ACTION="heal";;
|
|
--configure) ACTION="configure";;
|
|
--join-aether) ACTION="join-aether";;
|
|
--cleanup) ACTION="cleanup";;
|
|
--doctor) ACTION="doctor";;
|
|
-c|--config) CONFIG_FILE="$2"; shift;;
|
|
-n|--dry-run) DRY_RUN=true;;
|
|
-v|--verbose) VERBOSE=true;;
|
|
-q|--quiet) QUIET=true;;
|
|
-h|--help) usage; exit 0;;
|
|
*)
|
|
error "Unknown option: $1"
|
|
echo "Run '${SCRIPT_NAME} --help' for usage."
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ -z "$ACTION" ]]; then
|
|
error "No action specified"
|
|
echo "Run '${SCRIPT_NAME} --help' for usage."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main() {
|
|
setup_colors
|
|
parse_args "$@"
|
|
|
|
# Load config file if provided
|
|
if [[ -n "$CONFIG_FILE" ]]; then
|
|
parse_config "$CONFIG_FILE"
|
|
fi
|
|
|
|
echo
|
|
case "$ACTION" in
|
|
deploy) action_deploy;;
|
|
status) action_status;;
|
|
heal) action_heal;;
|
|
configure) action_configure;;
|
|
join-aether) action_join_aether;;
|
|
cleanup) action_cleanup;;
|
|
doctor) action_doctor;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|