diff --git a/scripts/onboard_client.sh b/scripts/onboard_client.sh index a54bad8..dc2cc66 100755 --- a/scripts/onboard_client.sh +++ b/scripts/onboard_client.sh @@ -13,12 +13,19 @@ usage() { 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 " -H, --hypervisor Hypervisor type (proxmox|xcpng) default: proxmox" + echo " --proxmox-host Proxmox host IP (optional)" + echo " --proxmox-token-id Proxmox API token ID (e.g. ansible@pve!token-name)" + echo " --proxmox-token-secret Proxmox API token secret" echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)" echo " --semaphore-token Semaphore API token" + echo " --gitea-url Gitea repo SSH URL" + echo " --project-name Semaphore project name (default: 'Client - ')" echo "" echo "Example:" - echo " $0 -i DFA-001 -n 'DFA Tech Colo' -s dfa_tech -w https://n8n.voice1.me/webhook/xxx" + echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp -w https://n8n.voice1.me/webhook/xxx \\" + echo " --proxmox-host 10.1.2.3 --proxmox-token-id ansible@pve!ansible-token \\" + echo " --proxmox-token-secret abc123" exit 1 } @@ -29,30 +36,51 @@ VPN="ipsec" HYPERVISOR="proxmox" SEMAPHORE_URL="http://localhost:3000" REPO_DIR="/opt/ansible-msp-automations" +GITEA_DEPLOY_KEY="/root/.ssh/gitea_ansible" +GITEA_REPO_URL="ssh://git@172.31.10.8:2222/VOICE1/ansible-msp-automations.git" +PROXMOX_HOST="" +PROXMOX_TOKEN_ID="" +PROXMOX_TOKEN_SECRET="" +PROJECT_NAME_OVERRIDE="" # ─── 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 ;; + -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 ;; + --proxmox-host) PROXMOX_HOST="$2"; shift 2 ;; + --proxmox-token-id) PROXMOX_TOKEN_ID="$2"; shift 2 ;; + --proxmox-token-secret) PROXMOX_TOKEN_SECRET="$2"; shift 2 ;; + --semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;; + --semaphore-token) SEMAPHORE_TOKEN="$2"; shift 2 ;; + --gitea-url) GITEA_REPO_URL="$2"; shift 2 ;; + --project-name) PROJECT_NAME_OVERRIDE="$2"; shift 2 ;; *) usage ;; esac done # ─── Validate required args ────────────────────────────────────────────────── -if [[ -z "$CLIENT_ID" || -z "$CLIENT_NAME" || -z "$CLIENT_SLUG" || -z "$WEBHOOK_URL" ]]; then +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 +# Load SEMAPHORE_TOKEN from env file if not passed as arg +if [[ -z "${SEMAPHORE_TOKEN:-}" && -f "/root/.semaphore_env" ]]; then + source /root/.semaphore_env +fi + +PROJECT_NAME="${PROJECT_NAME_OVERRIDE:-Client - ${CLIENT_NAME}}" +KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}" +INVENTORY_DIR="$REPO_DIR/inventories/client_${CLIENT_SLUG}" +INVENTORY_REPO_PATH="inventories/client_${CLIENT_SLUG}/hosts.yml" + echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Onboarding client: $CLIENT_NAME ($CLIENT_ID)" @@ -60,8 +88,7 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━ # ─── Step 1: Generate SSH key ───────────────────────────────────────────────── echo "" -echo "[ 1/5 ] Generating SSH key..." -KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}" +echo "[ 1/6 ] Generating SSH key..." if [[ -f "$KEY_FILE" ]]; then echo " ⚠ Key already exists at $KEY_FILE — skipping generation" else @@ -71,21 +98,19 @@ fi echo "" echo " ┌─ Deploy this public key to all client hosts ───────────────────────" echo " │" -cat "$KEY_FILE.pub" | sed 's/^/ │ /' +sed 's/^/ │ /' "$KEY_FILE.pub" echo " │" echo " └────────────────────────────────────────────────────────────────────" # ─── Step 2: Create inventory from template ─────────────────────────────────── echo "" -echo "[ 2/5 ] Creating inventory from template..." -INVENTORY_DIR="$REPO_DIR/inventories/client_${CLIENT_SLUG}" +echo "[ 2/6 ] Creating inventory from template..." 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: @@ -119,7 +144,6 @@ all: ansible_port: 5986 HOSTSEOF - # Write group_vars/all.yml cat > "$INVENTORY_DIR/group_vars/all.yml" << VARSEOF --- # Client: ${CLIENT_NAME} (${CLIENT_ID}) @@ -136,7 +160,7 @@ fi # ─── Step 3: Commit and push to Gitea ──────────────────────────────────────── echo "" -echo "[ 3/5 ] Committing to Gitea..." +echo "[ 3/6 ] Committing to Gitea..." cd "$REPO_DIR" git add . git commit -m "Onboard client: ${CLIENT_NAME} (${CLIENT_ID}) — inventory scaffold" \ @@ -146,82 +170,184 @@ echo " ✓ Pushed to Gitea" # ─── Step 4: Create Semaphore project via API ───────────────────────────────── echo "" -echo "[ 4/5 ] Creating Semaphore project via API..." +echo "[ 4/6 ] 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 +if [[ -z "${SEMAPHORE_TOKEN:-}" ]]; then + echo " ✗ No semaphore token available." + echo " Set SEMAPHORE_TOKEN in /root/.semaphore_env or pass --semaphore-token" + exit 1 fi -# ─── Step 5: Summary ────────────────────────────────────────────────────────── +# 4a. Create project +PROJECT_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/projects" \ + -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"$PROJECT_NAME\", \"alert\": false, \"max_parallel_tasks\": 0}") +PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r '.id') + +if [[ "$PROJECT_ID" == "null" || -z "$PROJECT_ID" ]]; then + echo " ✗ Failed to create project" + echo " Response: $PROJECT_RESPONSE" + exit 1 +fi +echo " ✓ Project created: $PROJECT_NAME (ID: $PROJECT_ID)" + +# 4b. Add gitea-deploy key — dynamically read key file, jq for safe JSON encoding +GITEA_KEY_RESPONSE=$(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\", + \"project_id\": $PROJECT_ID, + \"ssh\": { + \"login\": \"\", + \"passphrase\": \"\", + \"private_key\": $(jq -Rs . < "$GITEA_DEPLOY_KEY") + } + }") +GITEA_KEY_ID=$(echo "$GITEA_KEY_RESPONSE" | jq -r '.id') +echo " ✓ gitea-deploy key added (ID: $GITEA_KEY_ID)" + +# 4c. Add client SSH key +CLIENT_KEY_RESPONSE=$(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\", + \"project_id\": $PROJECT_ID, + \"ssh\": { + \"login\": \"\", + \"passphrase\": \"\", + \"private_key\": $(jq -Rs . < "$KEY_FILE") + } + }") +CLIENT_KEY_ID=$(echo "$CLIENT_KEY_RESPONSE" | jq -r '.id') +echo " ✓ Client SSH key added (ID: $CLIENT_KEY_ID)" + +# 4d. Add None key (required for inventory become_key_id) +NONE_KEY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \ + -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"None\", + \"type\": \"none\", + \"project_id\": $PROJECT_ID + }") +NONE_KEY_ID=$(echo "$NONE_KEY_RESPONSE" | jq -r '.id') +echo " ✓ None key added (ID: $NONE_KEY_ID)" + +# 4e. Add repository — uses dynamically obtained GITEA_KEY_ID (not hardcoded) +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\": \"$GITEA_REPO_URL\", + \"git_branch\": \"main\", + \"ssh_key_id\": $GITEA_KEY_ID + }") +REPO_ID=$(echo "$REPO_RESPONSE" | jq -r '.id') +echo " ✓ Repository linked (ID: $REPO_ID)" + +# 4f. Add variable group with all vars including Proxmox +VARS_JSON=$(jq -n \ + --arg webhook "$WEBHOOK_URL" \ + --arg cid "$CLIENT_ID" \ + --arg cname "$CLIENT_NAME" \ + --arg billing "$BILLING" \ + --arg estimate "$ESTIMATE" \ + --arg phost "$PROXMOX_HOST" \ + --arg ptid "$PROXMOX_TOKEN_ID" \ + --arg ptsecret "$PROXMOX_TOKEN_SECRET" \ + '{ + N8N_WEBHOOK_URL: $webhook, + CLIENT_ID: $cid, + CLIENT_NAME: $cname, + BILLING_MODEL: $billing, + HUMAN_ESTIMATE_SECONDS: $estimate, + PROXMOX_HOST: $phost, + PROXMOX_TOKEN_ID: $ptid, + PROXMOX_TOKEN_SECRET: $ptsecret + }') + +ENV_RESPONSE=$(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\": $(echo "$VARS_JSON" | jq -Rs .), + \"env\": \"{}\" + }") +ENV_ID=$(echo "$ENV_RESPONSE" | jq -r '.id') +echo " ✓ Variable group created (ID: $ENV_ID)" + +# 4g. Add inventory (file type, pointing to Git repo path) +INVENTORY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/inventory" \ + -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"client-${CLIENT_SLUG}\", + \"project_id\": $PROJECT_ID, + \"inventory\": \"$INVENTORY_REPO_PATH\", + \"ssh_key_id\": $CLIENT_KEY_ID, + \"become_key_id\": $NONE_KEY_ID, + \"type\": \"file\", + \"repository_id\": $REPO_ID + }") +INVENTORY_ID=$(echo "$INVENTORY_RESPONSE" | jq -r '.id') +echo " ✓ Inventory created (ID: $INVENTORY_ID)" + +# ─── Step 5: Create task templates ─────────────────────────────────────────── +echo "" +echo "[ 5/6 ] Creating task templates..." + +create_template() { + local TNAME="$1" + local PLAYBOOK="$2" + local DESC="$3" + RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/templates" \ + -H "Authorization: Bearer $SEMAPHORE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"project_id\": $PROJECT_ID, + \"inventory_id\": $INVENTORY_ID, + \"repository_id\": $REPO_ID, + \"environment_id\": $ENV_ID, + \"name\": \"$TNAME\", + \"playbook\": \"$PLAYBOOK\", + \"arguments\": \"[]\", + \"allow_override_args_in_task\": false, + \"description\": \"$DESC\", + \"app\": \"ansible\" + }") + echo " ✓ $(echo "$RESPONSE" | jq -r '"Template: \(.name) (ID: \(.id))"')" +} + +create_template "Preflight Check" "playbooks/site_preflight.yml" "Run safety checks on all hosts before maintenance" +create_template "Snapshot Test" "playbooks/snapshot_pre.yml" "Test pre-patch snapshot creation via Proxmox API" +create_template "Linux Patch" "playbooks/linux_patch.yml" "Full Linux patch run with version tracking" +create_template "Full Maintenance" "playbooks/site_maintenance.yml" "Full maintenance: snapshot, preflight, patch" + +# ─── Step 6: 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 " Semaphore project ID: $PROJECT_ID" +echo " Inventory file: $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 " 1. Add hosts to: $INVENTORY_DIR/hosts.yml" +echo " 2. git add . && git commit -m 'Add hosts for ${CLIENT_NAME}' && git push origin main" +echo " 3. Semaphore → $PROJECT_NAME → Preflight Check → ▶ Run" +echo " 4. Semaphore → $PROJECT_NAME → Snapshot Test → ▶ Run" +echo " 5. Semaphore → $PROJECT_NAME → Linux Patch → ▶ Run" +echo "" +echo " Public key to deploy to client hosts:" +sed 's/^/ /' "$KEY_FILE.pub" echo ""