#!/bin/bash set -e # ─── Usage ─────────────────────────────────────────────────────────────────── usage() { echo "Usage: $0 [options]" echo "" echo "Options:" echo " -i, --id Client ID (e.g. DFA-001)" echo " -n, --name Client name (e.g. 'DFA Tech Colo')" echo " -s, --slug Short slug for filenames (e.g. dfa_tech)" echo " -w, --webhook n8n webhook URL" echo " -b, --billing Billing model (hybrid|per-host|time-saved|tiered) default: hybrid" echo " -e, --estimate Human time estimate seconds (default: 2700)" echo " -v, --vpn VPN type (ipsec|openvpn) default: ipsec" echo " -h, --hypervisor Hypervisor type (proxmox|xcpng) default: proxmox" echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)" echo " --semaphore-token Semaphore API token" echo "" echo "Example:" echo " $0 -i DFA-001 -n 'DFA Tech Colo' -s dfa_tech -w https://n8n.voice1.me/webhook/xxx" exit 1 } # ─── Defaults ──────────────────────────────────────────────────────────────── BILLING="hybrid" ESTIMATE="2700" VPN="ipsec" HYPERVISOR="proxmox" SEMAPHORE_URL="http://localhost:3000" REPO_DIR="/opt/ansible-msp-automations" # ─── Parse args ────────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case $1 in -i|--id) CLIENT_ID="$2"; shift 2 ;; -n|--name) CLIENT_NAME="$2"; shift 2 ;; -s|--slug) CLIENT_SLUG="$2"; shift 2 ;; -w|--webhook) WEBHOOK_URL="$2"; shift 2 ;; -b|--billing) BILLING="$2"; shift 2 ;; -e|--estimate) ESTIMATE="$2"; shift 2 ;; -v|--vpn) VPN="$2"; shift 2 ;; -h|--hypervisor) HYPERVISOR="$2"; shift 2 ;; --semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;; --semaphore-token) SEMAPHORE_TOKEN="$2"; shift 2 ;; *) usage ;; esac done # ─── Validate required args ────────────────────────────────────────────────── if [[ -z "$CLIENT_ID" || -z "$CLIENT_NAME" || -z "$CLIENT_SLUG" || -z "$WEBHOOK_URL" ]]; then echo "ERROR: --id, --name, --slug, and --webhook are required" usage fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Onboarding client: $CLIENT_NAME ($CLIENT_ID)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # ─── Step 1: Generate SSH key ───────────────────────────────────────────────── echo "" echo "[ 1/5 ] Generating SSH key..." KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}" if [[ -f "$KEY_FILE" ]]; then echo " ⚠ Key already exists at $KEY_FILE — skipping generation" else ssh-keygen -t ed25519 -C "ansible-${CLIENT_SLUG}" -f "$KEY_FILE" -N "" echo " ✓ Key generated: $KEY_FILE" fi echo "" echo " ┌─ Deploy this public key to all client hosts ───────────────────────" echo " │" cat "$KEY_FILE.pub" | sed 's/^/ │ /' echo " │" echo " └────────────────────────────────────────────────────────────────────" # ─── Step 2: Create inventory from template ─────────────────────────────────── echo "" echo "[ 2/5 ] Creating inventory from template..." INVENTORY_DIR="$REPO_DIR/inventories/client_${CLIENT_SLUG}" if [[ -d "$INVENTORY_DIR" ]]; then echo " ⚠ Inventory directory already exists — skipping" else cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR" # Write hosts.yml cat > "$INVENTORY_DIR/hosts.yml" << HOSTSEOF --- all: vars: client_id: "${CLIENT_ID}" client_name: "${CLIENT_NAME}" billing_model: "${BILLING}" maintenance_window_start: "02:00" maintenance_window_end: "05:00" maintenance_window_tz: "UTC" change_freeze: false hypervisor_type: "${HYPERVISOR}" vpn_type: "${VPN}" auto_reboot: false human_estimate_seconds: ${ESTIMATE} children: linux_hosts: hosts: {} vars: ansible_user: root os_family: "debian" windows_hosts: hosts: {} vars: ansible_user: Administrator ansible_connection: winrm ansible_winrm_transport: ntlm ansible_winrm_server_cert_validation: validate ansible_port: 5986 HOSTSEOF # Write group_vars/all.yml cat > "$INVENTORY_DIR/group_vars/all.yml" << VARSEOF --- # Client: ${CLIENT_NAME} (${CLIENT_ID}) # Onboarded: $(date +%Y-%m-%d) # VPN: ${VPN} # Hypervisor: ${HYPERVISOR} # Billing: ${BILLING} # Add client-specific overrides below VARSEOF echo " ✓ Inventory created at $INVENTORY_DIR" fi # ─── Step 3: Commit and push to Gitea ──────────────────────────────────────── echo "" echo "[ 3/5 ] Committing to Gitea..." cd "$REPO_DIR" git add . git commit -m "Onboard client: ${CLIENT_NAME} (${CLIENT_ID}) — inventory scaffold" \ || echo " ⚠ Nothing to commit" git push origin main echo " ✓ Pushed to Gitea" # ─── Step 4: Create Semaphore project via API ───────────────────────────────── echo "" echo "[ 4/5 ] Creating Semaphore project via API..." if [[ -z "$SEMAPHORE_TOKEN" ]]; then echo " ⚠ No --semaphore-token provided — skipping Semaphore API setup" echo " Create the project manually in Semaphore UI" else # Create project PROJECT_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/projects" \ -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"Client - ${CLIENT_NAME}\", \"alert\": false, \"max_parallel_tasks\": 0}") PROJECT_ID=$(echo "$PROJECT_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null) if [[ -z "$PROJECT_ID" ]]; then echo " ✗ Failed to create project — check token and Semaphore URL" echo " Response: $PROJECT_RESPONSE" else echo " ✓ Project created (ID: $PROJECT_ID)" # Add gitea-deploy key to project key store GITEA_KEY=$(cat /root/.ssh/gitea_ansible) curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \ -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"gitea-deploy\", \"type\": \"ssh\", \"ssh\": {\"private_key\": $(echo "$GITEA_KEY" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")}}" > /dev/null echo " ✓ Gitea deploy key added" # Add client SSH key CLIENT_KEY=$(cat "$KEY_FILE") curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \ -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"client-${CLIENT_SLUG}-ssh\", \"type\": \"ssh\", \"ssh\": {\"private_key\": $(echo "$CLIENT_KEY" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")}}" > /dev/null echo " ✓ Client SSH key added to Key Store" # Add repository REPO_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/repositories" \ -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"ansible-msp-automations\", \"project_id\": $PROJECT_ID, \"git_url\": \"ssh://git@172.31.10.8:2222/VOICE1/ansible-msp-automations.git\", \"git_branch\": \"main\", \"ssh_key_id\": 1}") REPO_ID=$(echo "$REPO_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null) echo " ✓ Repository linked (ID: $REPO_ID)" # Add variable group curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/environment" \ -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"${CLIENT_SLUG}-vars\", \"project_id\": $PROJECT_ID, \"json\": \"{\\\"N8N_WEBHOOK_URL\\\": \\\"${WEBHOOK_URL}\\\", \\\"CLIENT_ID\\\": \\\"${CLIENT_ID}\\\", \\\"CLIENT_NAME\\\": \\\"${CLIENT_NAME}\\\", \\\"BILLING_MODEL\\\": \\\"${BILLING}\\\", \\\"HUMAN_ESTIMATE_SECONDS\\\": \\\"${ESTIMATE}\\\"}\"}" > /dev/null echo " ✓ Variable group created" echo "" echo " Semaphore project ID: $PROJECT_ID" echo " Still needed manually:" echo " - Add inventory (File type → inventories/client_${CLIENT_SLUG}/hosts.yml)" echo " - Create task templates" echo " - Add hosts to inventory file in Gitea" fi fi # ─── Step 5: Summary ────────────────────────────────────────────────────────── echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " ✓ Client onboarding complete: $CLIENT_NAME" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo " SSH public key to deploy to client hosts:" echo " $KEY_FILE.pub" echo "" echo " Inventory file to populate with hosts:" echo " $INVENTORY_DIR/hosts.yml" echo "" echo " Next steps:" echo " 1. Deploy SSH public key to all client hosts" echo " 2. Add hosts to inventory file in Gitea" echo " 3. Add inventory entry in Semaphore (File type)" echo " 4. Create task templates in Semaphore" echo " 5. Run preflight check" echo ""