From e147b15a89599e4460be6905094443464891c998 Mon Sep 17 00:00:00 2001 From: Semaphore Date: Fri, 13 Mar 2026 08:48:42 -0700 Subject: [PATCH] scripts: add --bootstrap-proxmox flag to onboard_client.sh --- scripts/onboard_client.sh | 177 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 7 deletions(-) diff --git a/scripts/onboard_client.sh b/scripts/onboard_client.sh index df1066f..7ee7779 100755 --- a/scripts/onboard_client.sh +++ b/scripts/onboard_client.sh @@ -14,9 +14,13 @@ usage() { 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|baremetal|mixed) default: proxmox" - echo " --proxmox-host Proxmox host IP (optional)" + echo " --proxmox-host Proxmox host IP or hostname (required for proxmox/mixed)" echo " --proxmox-token-id Proxmox API token ID (e.g. ansible@pve!token-name)" echo " --proxmox-token-secret Proxmox API token secret" + echo " --bootstrap-proxmox Create Proxmox role/user/token via API (prompts for root password)" + echo " --proxmox-role Proxmox role name to create (default: AnsibleMSP)" + echo " --proxmox-user Proxmox user to create (default: ansible@pve)" + echo " --proxmox-token-name Token name to create (default: ansible-token)" echo " --xo-url XCP-NG/XO API URL (default: \$XO_URL from env)" echo " --xo-token XCP-NG/XO API token (default: \$XO_TOKEN from env)" echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)" @@ -26,8 +30,8 @@ usage() { echo " --dry-run Print actions without executing" echo "" echo "Example:" - echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp -w https://n8n.voice1.me/webhook/xxx \\" - echo " -H xcpng --xo-url https://xoa.voice1.me --xo-token " + echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp \\" + echo " -H proxmox --proxmox-host 192.168.1.10 --bootstrap-proxmox" exit 1 } @@ -43,6 +47,10 @@ GITEA_REPO_URL="ssh://git@172.31.10.8:2222/VOICE1/ansible-msp-automations.git" PROXMOX_HOST="" PROXMOX_TOKEN_ID="" PROXMOX_TOKEN_SECRET="" +BOOTSTRAP_PROXMOX=false +PROXMOX_ROLE="AnsibleMSP" +PROXMOX_USER="ansible@pve" +PROXMOX_TOKEN_NAME="ansible-token" XO_URL_ARG="" XO_TOKEN_ARG="" PROJECT_NAME_OVERRIDE="" @@ -62,6 +70,10 @@ while [[ $# -gt 0 ]]; do --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 ;; + --bootstrap-proxmox) BOOTSTRAP_PROXMOX=true; shift ;; + --proxmox-role) PROXMOX_ROLE="$2"; shift 2 ;; + --proxmox-user) PROXMOX_USER="$2"; shift 2 ;; + --proxmox-token-name) PROXMOX_TOKEN_NAME="$2"; shift 2 ;; --xo-url) XO_URL_ARG="$2"; shift 2 ;; --xo-token) XO_TOKEN_ARG="$2"; shift 2 ;; --semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;; @@ -104,6 +116,14 @@ if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then fi fi +# Validate proxmox host for bootstrap or proxmox hypervisor +if [[ "$BOOTSTRAP_PROXMOX" == true || "$HYPERVISOR" == "proxmox" || "$HYPERVISOR" == "mixed" ]]; then + if [[ -z "$PROXMOX_HOST" ]]; then + echo "ERROR: --proxmox-host is required for hypervisor type '$HYPERVISOR'" + exit 1 + fi +fi + PROJECT_NAME="${PROJECT_NAME_OVERRIDE:-Client - ${CLIENT_NAME}}" KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}" INVENTORY_DIR="$REPO_DIR/inventories/client_${CLIENT_SLUG}" @@ -115,6 +135,143 @@ echo " Onboarding client: $CLIENT_NAME ($CLIENT_ID)" [[ "$DRY_RUN" == true ]] && echo " ⚠ DRY RUN — no changes will be made" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +# ─── Step 0: Bootstrap Proxmox (optional) ──────────────────────────────────── +if [[ "$BOOTSTRAP_PROXMOX" == true ]]; then + echo "" + echo "[ 0 ] Bootstrapping Proxmox user/role/token on $PROXMOX_HOST..." + echo "" + + if [[ "$DRY_RUN" == true ]]; then + echo " [dry-run] Would authenticate as root@pam and create:" + echo " Role: $PROXMOX_ROLE" + echo " User: $PROXMOX_USER" + echo " Token: $PROXMOX_USER!$PROXMOX_TOKEN_NAME" + echo " Permission: / (propagate=1)" + else + # Prompt for root password + echo -n " Proxmox root password for $PROXMOX_HOST: " + read -rs PVE_ROOT_PASS + echo "" + + PVE_API="https://${PROXMOX_HOST}:8006/api2/json" + + # Get auth ticket + echo " Authenticating as root@pam..." + TICKET_RESPONSE=$(curl -sk -X POST "$PVE_API/access/ticket" \ + -d "username=root@pam" \ + --data-urlencode "password=$PVE_ROOT_PASS") + + PVE_TICKET=$(echo "$TICKET_RESPONSE" | jq -r '.data.ticket // empty') + PVE_CSRF=$(echo "$TICKET_RESPONSE" | jq -r '.data.CSRFPreventionToken // empty') + + if [[ -z "$PVE_TICKET" ]]; then + echo " ✗ Authentication failed — check root password" + echo " Response: $TICKET_RESPONSE" + exit 1 + fi + echo " ✓ Authenticated" + + # Helper functions — use ticket auth for all subsequent calls + pve_post() { + local endpoint="$1"; shift + curl -sk -X POST "$PVE_API/$endpoint" \ + -H "CSRFPreventionToken: $PVE_CSRF" \ + -b "PVEAuthCookie=$PVE_TICKET" \ + "$@" + } + pve_put() { + local endpoint="$1"; shift + curl -sk -X PUT "$PVE_API/$endpoint" \ + -H "CSRFPreventionToken: $PVE_CSRF" \ + -b "PVEAuthCookie=$PVE_TICKET" \ + "$@" + } + + # Create role + echo " Creating role: $PROXMOX_ROLE..." + pve_post "access/roles" \ + -d "roleid=$PROXMOX_ROLE" \ + -d "privs=VM.Snapshot,VM.Snapshot.Rollback,VM.Audit,VM.PowerMgmt,Datastore.AllocateSpace" \ + > /dev/null + echo " ✓ Role created (or already exists): $PROXMOX_ROLE" + + # Create user + PVE_USERID_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PROXMOX_USER'))") + echo " Creating user: $PROXMOX_USER..." + USER_RESPONSE=$(pve_post "access/users" \ + -d "userid=$PROXMOX_USER" \ + -d "password=ChangeMe123!" \ + -d "comment=Ansible MSP automation user — managed by onboard_client.sh") + USER_DATA=$(echo "$USER_RESPONSE" | jq -r '.data // empty') + USER_ERR=$(echo "$USER_RESPONSE" | jq -r '.errors // empty') + + if [[ -n "$USER_ERR" ]]; then + if echo "$USER_RESPONSE" | jq -r '.errors | to_entries[].value' 2>/dev/null | grep -qi "already exist"; then + echo " ⚠ User already exists — continuing" + else + echo " ✗ Failed to create user" + echo " Response: $USER_RESPONSE" + exit 1 + fi + else + echo " ✓ User created: $PROXMOX_USER" + echo " ⚠ Temporary password set: ChangeMe123! — rotate after token creation" + fi + + # Create API token + echo " Creating API token: $PROXMOX_TOKEN_NAME..." + TOKEN_RESPONSE=$(pve_post "access/users/${PVE_USERID_ENCODED}/token/$PROXMOX_TOKEN_NAME" \ + -d "privsep=0" \ + -d "comment=Ansible MSP snapshot token — managed by onboard_client.sh") + TOKEN_VALUE=$(echo "$TOKEN_RESPONSE" | jq -r '.data.value // empty') + + if [[ -z "$TOKEN_VALUE" ]]; then + echo " ✗ Failed to create token (may already exist — token secrets are shown only once)" + echo " Response: $TOKEN_RESPONSE" + echo "" + echo " If the token already exists, delete it in Proxmox UI and re-run --bootstrap-proxmox" + echo " Or pass --proxmox-token-id and --proxmox-token-secret directly to skip bootstrap" + exit 1 + fi + + PROXMOX_TOKEN_ID="${PROXMOX_USER}!${PROXMOX_TOKEN_NAME}" + PROXMOX_TOKEN_SECRET="$TOKEN_VALUE" + + echo " ✓ Token created" + echo "" + echo " ┌─ SAVE THIS TOKEN SECRET — shown only once ─────────────────────" + echo " │" + echo " │ Token ID: $PROXMOX_TOKEN_ID" + echo " │ Token Secret: $PROXMOX_TOKEN_SECRET" + echo " │" + echo " └────────────────────────────────────────────────────────────────" + echo "" + + # Assign permission: user + role to path / + echo " Assigning permission: $PROXMOX_USER + $PROXMOX_ROLE → / (propagate)..." + pve_put "access/acl" \ + -d "path=/" \ + -d "users=$PROXMOX_USER" \ + -d "roles=$PROXMOX_ROLE" \ + -d "propagate=1" \ + > /dev/null + echo " ✓ Permission assigned" + echo "" + echo " ✓ Proxmox bootstrap complete" + fi +fi + +# ─── Validate token args (post-bootstrap) ──────────────────────────────────── +if [[ "$BOOTSTRAP_PROXMOX" != true ]]; then + if [[ "$HYPERVISOR" == "proxmox" || "$HYPERVISOR" == "mixed" ]]; then + if [[ -z "$PROXMOX_TOKEN_ID" || -z "$PROXMOX_TOKEN_SECRET" ]]; then + echo "ERROR: --proxmox-token-id and --proxmox-token-secret are required" + echo " (or use --bootstrap-proxmox to create them automatically)" + exit 1 + fi + fi +fi + # ─── Step 1: Generate SSH key ───────────────────────────────────────────────── echo "" echo "[ 1/6 ] Generating SSH key..." @@ -148,7 +305,6 @@ else cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR" mkdir -p "$INVENTORY_DIR/group_vars" - # Build xcpng_hosts group if applicable XCPNG_HOSTS_BLOCK="" if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then XCPNG_HOSTS_BLOCK=" @@ -315,7 +471,7 @@ REPO_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/reposito REPO_ID=$(echo "$REPO_RESPONSE" | jq -r '.id') echo " ✓ Repository linked (ID: $REPO_ID)" -# 4f. Build variable group JSON — include XO vars for xcpng/mixed +# 4f. Build variable group JSON if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then VARS_JSON=$(jq -n \ --arg webhook "$WEBHOOK_URL" \ @@ -416,7 +572,6 @@ create_template "Linux Patch" "playbooks/linux_patch.yml" "Full Linu create_template "Full Maintenance" "playbooks/site_maintenance.yml" "Full maintenance: snapshot, preflight, patch" create_template "Scheduled Reboot" "playbooks/linux_reboot.yml" "Reboot hosts that require it — pass -e force_reboot=true to reboot all" -# Hypervisor-specific templates if [[ "$HYPERVISOR" == "proxmox" || "$HYPERVISOR" == "mixed" ]]; then create_template "Snapshot (Proxmox)" "playbooks/snapshot_pre.yml" "Pre-patch snapshot via Proxmox API" fi @@ -433,6 +588,13 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━ echo "" echo " Semaphore project ID: $PROJECT_ID" echo " Inventory file: $INVENTORY_DIR/hosts.yml" +if [[ "$BOOTSTRAP_PROXMOX" == true && "$DRY_RUN" != true ]]; then + echo "" + echo " Proxmox credentials (also stored in Semaphore variable group):" + echo " Token ID: $PROXMOX_TOKEN_ID" + echo " Token Secret: $PROXMOX_TOKEN_SECRET" + echo " ⚠ Rotate the $PROXMOX_USER password in Proxmox UI when convenient" +fi echo "" echo " Next steps:" echo " 1. Add hosts to: $INVENTORY_DIR/hosts.yml" @@ -443,8 +605,9 @@ echo " 5. Semaphore → $PROJECT_NAME → Full Maintenance → ▶ Run" echo "" echo " To reboot hosts after a deferred patch run:" echo " Semaphore → $PROJECT_NAME → Scheduled Reboot → ▶ Run" -echo " (add extra var force_reboot=true to reboot all hosts regardless)" +echo " (add extra var -e force_reboot=true to reboot all hosts regardless)" echo "" echo " Public key to deploy to client hosts:" sed 's/^/ /' "$KEY_FILE.pub" echo "" +