scripts: add --bootstrap-proxmox flag to onboard_client.sh

This commit is contained in:
Semaphore
2026-03-13 08:48:42 -07:00
parent d9d250021d
commit e147b15a89

View File

@@ -14,9 +14,13 @@ usage() {
echo " -e, --estimate Human time estimate seconds (default: 2700)" echo " -e, --estimate Human time estimate seconds (default: 2700)"
echo " -v, --vpn VPN type (ipsec|openvpn) default: ipsec" echo " -v, --vpn VPN type (ipsec|openvpn) default: ipsec"
echo " -H, --hypervisor Hypervisor type (proxmox|xcpng|baremetal|mixed) default: proxmox" 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-id Proxmox API token ID (e.g. ansible@pve!token-name)"
echo " --proxmox-token-secret Proxmox API token secret" 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-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 " --xo-token XCP-NG/XO API token (default: \$XO_TOKEN from env)"
echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)" echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)"
@@ -26,8 +30,8 @@ usage() {
echo " --dry-run Print actions without executing" echo " --dry-run Print actions without executing"
echo "" echo ""
echo "Example:" echo "Example:"
echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp -w https://n8n.voice1.me/webhook/xxx \\" echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp \\"
echo " -H xcpng --xo-url https://xoa.voice1.me --xo-token <token>" echo " -H proxmox --proxmox-host 192.168.1.10 --bootstrap-proxmox"
exit 1 exit 1
} }
@@ -43,6 +47,10 @@ GITEA_REPO_URL="ssh://git@172.31.10.8:2222/VOICE1/ansible-msp-automations.git"
PROXMOX_HOST="" PROXMOX_HOST=""
PROXMOX_TOKEN_ID="" PROXMOX_TOKEN_ID=""
PROXMOX_TOKEN_SECRET="" PROXMOX_TOKEN_SECRET=""
BOOTSTRAP_PROXMOX=false
PROXMOX_ROLE="AnsibleMSP"
PROXMOX_USER="ansible@pve"
PROXMOX_TOKEN_NAME="ansible-token"
XO_URL_ARG="" XO_URL_ARG=""
XO_TOKEN_ARG="" XO_TOKEN_ARG=""
PROJECT_NAME_OVERRIDE="" PROJECT_NAME_OVERRIDE=""
@@ -62,6 +70,10 @@ while [[ $# -gt 0 ]]; do
--proxmox-host) PROXMOX_HOST="$2"; shift 2 ;; --proxmox-host) PROXMOX_HOST="$2"; shift 2 ;;
--proxmox-token-id) PROXMOX_TOKEN_ID="$2"; shift 2 ;; --proxmox-token-id) PROXMOX_TOKEN_ID="$2"; shift 2 ;;
--proxmox-token-secret) PROXMOX_TOKEN_SECRET="$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-url) XO_URL_ARG="$2"; shift 2 ;;
--xo-token) XO_TOKEN_ARG="$2"; shift 2 ;; --xo-token) XO_TOKEN_ARG="$2"; shift 2 ;;
--semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;; --semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;;
@@ -104,6 +116,14 @@ if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
fi fi
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}}" PROJECT_NAME="${PROJECT_NAME_OVERRIDE:-Client - ${CLIENT_NAME}}"
KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}" KEY_FILE="/root/.ssh/client_${CLIENT_SLUG}"
INVENTORY_DIR="$REPO_DIR/inventories/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" [[ "$DRY_RUN" == true ]] && echo " ⚠ DRY RUN — no changes will be made"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 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 ───────────────────────────────────────────────── # ─── Step 1: Generate SSH key ─────────────────────────────────────────────────
echo "" echo ""
echo "[ 1/6 ] Generating SSH key..." echo "[ 1/6 ] Generating SSH key..."
@@ -148,7 +305,6 @@ else
cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR" cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR"
mkdir -p "$INVENTORY_DIR/group_vars" mkdir -p "$INVENTORY_DIR/group_vars"
# Build xcpng_hosts group if applicable
XCPNG_HOSTS_BLOCK="" XCPNG_HOSTS_BLOCK=""
if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
XCPNG_HOSTS_BLOCK=" 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') REPO_ID=$(echo "$REPO_RESPONSE" | jq -r '.id')
echo " ✓ Repository linked (ID: $REPO_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 if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
VARS_JSON=$(jq -n \ VARS_JSON=$(jq -n \
--arg webhook "$WEBHOOK_URL" \ --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 "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" 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 if [[ "$HYPERVISOR" == "proxmox" || "$HYPERVISOR" == "mixed" ]]; then
create_template "Snapshot (Proxmox)" "playbooks/snapshot_pre.yml" "Pre-patch snapshot via Proxmox API" create_template "Snapshot (Proxmox)" "playbooks/snapshot_pre.yml" "Pre-patch snapshot via Proxmox API"
fi fi
@@ -433,6 +588,13 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
echo "" echo ""
echo " Semaphore project ID: $PROJECT_ID" echo " Semaphore project ID: $PROJECT_ID"
echo " Inventory file: $INVENTORY_DIR/hosts.yml" 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 ""
echo " Next steps:" echo " Next steps:"
echo " 1. Add hosts to: $INVENTORY_DIR/hosts.yml" echo " 1. Add hosts to: $INVENTORY_DIR/hosts.yml"
@@ -443,8 +605,9 @@ echo " 5. Semaphore → $PROJECT_NAME → Full Maintenance → ▶ Run"
echo "" echo ""
echo " To reboot hosts after a deferred patch run:" echo " To reboot hosts after a deferred patch run:"
echo " Semaphore → $PROJECT_NAME → Scheduled Reboot → ▶ 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 ""
echo " Public key to deploy to client hosts:" echo " Public key to deploy to client hosts:"
sed 's/^/ /' "$KEY_FILE.pub" sed 's/^/ /' "$KEY_FILE.pub"
echo "" echo ""