Add linux_reboot playbook, update onboard_client.sh with reboot template + xcpng support
This commit is contained in:
63
playbooks/linux_reboot.yml
Normal file
63
playbooks/linux_reboot.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
- name: Bootstrap — ensure Python is available
|
||||||
|
hosts: linux_hosts
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- role: preflight
|
||||||
|
tasks_from: bootstrap
|
||||||
|
|
||||||
|
- name: Reboot Linux hosts if required (or forced)
|
||||||
|
hosts: linux_hosts
|
||||||
|
gather_facts: true
|
||||||
|
vars:
|
||||||
|
force_reboot: false # set to true in Semaphore extra vars to reboot regardless
|
||||||
|
tasks:
|
||||||
|
- name: Check if reboot is required (Debian/Ubuntu)
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /var/run/reboot-required
|
||||||
|
register: reboot_required_file
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Set reboot_needed fact (Debian/Ubuntu)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
reboot_needed: "{{ reboot_required_file.stat.exists | default(false) }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Check if reboot is required (Alpine)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
apk version -l = 2>/dev/null | grep -q kernel && echo "yes" || echo "no"
|
||||||
|
register: alpine_reboot_check
|
||||||
|
changed_when: false
|
||||||
|
when: ansible_os_family == "Alpine"
|
||||||
|
|
||||||
|
- name: Set reboot_needed fact (Alpine)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
reboot_needed: "{{ alpine_reboot_check.stdout | trim == 'yes' }}"
|
||||||
|
when: ansible_os_family == "Alpine"
|
||||||
|
|
||||||
|
- name: Set reboot_needed fallback (RHEL or unknown)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
reboot_needed: false
|
||||||
|
when: reboot_needed is not defined
|
||||||
|
|
||||||
|
- name: Report reboot status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ inventory_hostname }}:
|
||||||
|
reboot_needed={{ reboot_needed }},
|
||||||
|
force_reboot={{ force_reboot }}
|
||||||
|
— {{ 'WILL reboot' if (reboot_needed or force_reboot) else 'Skipping reboot' }}
|
||||||
|
|
||||||
|
- name: Reboot host
|
||||||
|
ansible.builtin.reboot:
|
||||||
|
reboot_timeout: 300
|
||||||
|
pre_reboot_delay: 10
|
||||||
|
post_reboot_delay: 30
|
||||||
|
msg: "Scheduled reboot — initiated by Ansible"
|
||||||
|
when: reboot_needed | bool or force_reboot | bool
|
||||||
|
|
||||||
|
- name: Reboot complete
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ inventory_hostname }} is back online and responding"
|
||||||
|
when: reboot_needed | bool or force_reboot | bool
|
||||||
|
|
||||||
@@ -1,42 +1,41 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# =============================================================================
|
# ─── Usage ───────────────────────────────────────────────────────────────────
|
||||||
# scripts/onboard_client.sh — MSP Client Onboarding
|
usage() {
|
||||||
# =============================================================================
|
echo "Usage: $0 [options]"
|
||||||
# Creates a new client project in Semaphore, generates SSH keys, scaffolds
|
echo ""
|
||||||
# the inventory, and creates all task templates.
|
echo "Options:"
|
||||||
#
|
echo " -i, --id Client ID (e.g. DFA-001)"
|
||||||
# Usage:
|
echo " -n, --name Client name (e.g. 'DFA Tech Colo')"
|
||||||
# ./onboard_client.sh -i CLIENT-001 -n "Client Name" -s client_slug [options]
|
echo " -s, --slug Short slug for filenames (e.g. dfa_tech)"
|
||||||
#
|
echo " -w, --webhook n8n webhook URL"
|
||||||
# Options:
|
echo " -b, --billing Billing model (hybrid|per-host|time-saved|tiered) default: hybrid"
|
||||||
# -i, --id Client ID (e.g. SRH-001) [required]
|
echo " -e, --estimate Human time estimate seconds (default: 2700)"
|
||||||
# -n, --name Client name (e.g. 'Sanrufo Homes') [required]
|
echo " -v, --vpn VPN type (ipsec|openvpn) default: ipsec"
|
||||||
# -s, --slug Inventory slug (e.g. sanrufo_homes) [required]
|
echo " -H, --hypervisor Hypervisor type (proxmox|xcpng|baremetal|mixed) default: proxmox"
|
||||||
# -b, --billing Billing model (default: hybrid)
|
echo " --proxmox-host Proxmox host IP (optional)"
|
||||||
# -e, --estimate Human time estimate seconds (default: 2700)
|
echo " --proxmox-token-id Proxmox API token ID (e.g. ansible@pve!token-name)"
|
||||||
# -H, --hypervisor Hypervisor type: proxmox|xcpng|baremetal|mixed
|
echo " --proxmox-token-secret Proxmox API token secret"
|
||||||
# (default: xcpng)
|
echo " --xo-url XCP-NG/XO API URL (default: \$XO_URL from env)"
|
||||||
# Use 'mixed' when a client has multiple hypervisor types
|
echo " --xo-token XCP-NG/XO API token (default: \$XO_TOKEN from env)"
|
||||||
# Use 'baremetal' when no snapshots are possible
|
echo " --semaphore-url Semaphore base URL (default: http://localhost:3000)"
|
||||||
# -w, --webhook n8n webhook URL override (default: global from env)
|
echo " --semaphore-token Semaphore API token"
|
||||||
# --proxmox-host Proxmox host IP
|
echo " --gitea-url Gitea repo SSH URL"
|
||||||
# --proxmox-token-id Proxmox API token ID
|
echo " --project-name Semaphore project name (default: 'Client - <n>')"
|
||||||
# --proxmox-token-secret Proxmox API token secret
|
echo " --dry-run Print actions without executing"
|
||||||
# --xo-url XO URL override (default: global XO_URL from env)
|
echo ""
|
||||||
# --xo-token XO token override (default: global XO_TOKEN from env)
|
echo "Example:"
|
||||||
# --semaphore-url Semaphore base URL (default: http://localhost:3000)
|
echo " $0 -i ACME-001 -n 'Acme Corp' -s acme_corp -w https://n8n.voice1.me/webhook/xxx \\"
|
||||||
# --semaphore-token Semaphore API token (default: from /root/.semaphore_env)
|
echo " -H xcpng --xo-url https://xoa.voice1.me --xo-token <token>"
|
||||||
# --gitea-url Gitea repo SSH URL
|
exit 1
|
||||||
# --project-name Override Semaphore project name
|
}
|
||||||
# --dry-run Show what would be done without making changes
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# ─── Defaults ────────────────────────────────────────────────────────────────
|
# ─── Defaults ────────────────────────────────────────────────────────────────
|
||||||
BILLING="hybrid"
|
BILLING="hybrid"
|
||||||
ESTIMATE="2700"
|
ESTIMATE="2700"
|
||||||
HYPERVISOR="xcpng"
|
VPN="ipsec"
|
||||||
|
HYPERVISOR="proxmox"
|
||||||
SEMAPHORE_URL="http://localhost:3000"
|
SEMAPHORE_URL="http://localhost:3000"
|
||||||
REPO_DIR="/opt/ansible-msp-automations"
|
REPO_DIR="/opt/ansible-msp-automations"
|
||||||
GITEA_DEPLOY_KEY="/root/.ssh/gitea_ansible"
|
GITEA_DEPLOY_KEY="/root/.ssh/gitea_ansible"
|
||||||
@@ -44,87 +43,65 @@ 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=""
|
||||||
XO_URL_OVERRIDE=""
|
XO_URL_ARG=""
|
||||||
XO_TOKEN_OVERRIDE=""
|
XO_TOKEN_ARG=""
|
||||||
WEBHOOK_URL_OVERRIDE=""
|
|
||||||
PROJECT_NAME_OVERRIDE=""
|
PROJECT_NAME_OVERRIDE=""
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
|
|
||||||
# Load global defaults from env file
|
|
||||||
if [[ -f /root/.semaphore_env ]]; then
|
|
||||||
source /root/.semaphore_env
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Colors / logging ────────────────────────────────────────────────────────
|
|
||||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'; NC='\033[0m'
|
|
||||||
log_info() { echo -e "${BLUE} ℹ $*${NC}"; }
|
|
||||||
log_ok() { echo -e "${GREEN} ✓ $*${NC}"; }
|
|
||||||
log_warn() { echo -e "${YELLOW} ⚠ $*${NC}"; }
|
|
||||||
log_error() { echo -e "${RED} ✗ $*${NC}"; }
|
|
||||||
log_section() { echo -e "\n${BLUE}[ $* ]${NC}"; }
|
|
||||||
dry() { [[ "$DRY_RUN" == "true" ]] && echo -e "${YELLOW} DRY-RUN: $*${NC}" && return 0 || return 1; }
|
|
||||||
|
|
||||||
# ─── Usage ───────────────────────────────────────────────────────────────────
|
|
||||||
usage() {
|
|
||||||
grep "^#" "$0" | head -40 | sed 's/^# \?//'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ─── Parse args ──────────────────────────────────────────────────────────────
|
# ─── Parse args ──────────────────────────────────────────────────────────────
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-i|--id) CLIENT_ID="$2"; shift 2 ;;
|
-i|--id) CLIENT_ID="$2"; shift 2 ;;
|
||||||
-n|--name) CLIENT_NAME="$2"; shift 2 ;;
|
-n|--name) CLIENT_NAME="$2"; shift 2 ;;
|
||||||
-s|--slug) CLIENT_SLUG="$2"; shift 2 ;;
|
-s|--slug) CLIENT_SLUG="$2"; shift 2 ;;
|
||||||
-b|--billing) BILLING="$2"; shift 2 ;;
|
-w|--webhook) WEBHOOK_URL="$2"; shift 2 ;;
|
||||||
-e|--estimate) ESTIMATE="$2"; shift 2 ;;
|
-b|--billing) BILLING="$2"; shift 2 ;;
|
||||||
-H|--hypervisor) HYPERVISOR="$2"; shift 2 ;;
|
-e|--estimate) ESTIMATE="$2"; shift 2 ;;
|
||||||
-w|--webhook) WEBHOOK_URL_OVERRIDE="$2"; shift 2 ;;
|
-v|--vpn) VPN="$2"; shift 2 ;;
|
||||||
--proxmox-host) PROXMOX_HOST="$2"; shift 2 ;;
|
-H|--hypervisor) HYPERVISOR="$2"; shift 2 ;;
|
||||||
--proxmox-token-id) PROXMOX_TOKEN_ID="$2"; shift 2 ;;
|
--proxmox-host) PROXMOX_HOST="$2"; shift 2 ;;
|
||||||
--proxmox-token-secret) PROXMOX_TOKEN_SECRET="$2"; shift 2 ;;
|
--proxmox-token-id) PROXMOX_TOKEN_ID="$2"; shift 2 ;;
|
||||||
--xo-url) XO_URL_OVERRIDE="$2"; shift 2 ;;
|
--proxmox-token-secret) PROXMOX_TOKEN_SECRET="$2"; shift 2 ;;
|
||||||
--xo-token) XO_TOKEN_OVERRIDE="$2"; shift 2 ;;
|
--xo-url) XO_URL_ARG="$2"; shift 2 ;;
|
||||||
--semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;;
|
--xo-token) XO_TOKEN_ARG="$2"; shift 2 ;;
|
||||||
--semaphore-token) SEMAPHORE_TOKEN="$2"; shift 2 ;;
|
--semaphore-url) SEMAPHORE_URL="$2"; shift 2 ;;
|
||||||
--gitea-url) GITEA_REPO_URL="$2"; shift 2 ;;
|
--semaphore-token) SEMAPHORE_TOKEN="$2"; shift 2 ;;
|
||||||
--project-name) PROJECT_NAME_OVERRIDE="$2";shift 2 ;;
|
--gitea-url) GITEA_REPO_URL="$2"; shift 2 ;;
|
||||||
--dry-run) DRY_RUN=true; shift ;;
|
--project-name) PROJECT_NAME_OVERRIDE="$2"; shift 2 ;;
|
||||||
-h|--help) usage ;;
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
*) log_error "Unknown option: $1"; usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# ─── Validate required args ──────────────────────────────────────────────────
|
# ─── Validate required args ──────────────────────────────────────────────────
|
||||||
MISSING=()
|
if [[ -z "${CLIENT_ID:-}" || -z "${CLIENT_NAME:-}" || -z "${CLIENT_SLUG:-}" ]]; then
|
||||||
[[ -z "${CLIENT_ID:-}" ]] && MISSING+=("--id")
|
echo "ERROR: --id, --name, and --slug are required"
|
||||||
[[ -z "${CLIENT_NAME:-}" ]] && MISSING+=("--name")
|
|
||||||
[[ -z "${CLIENT_SLUG:-}" ]] && MISSING+=("--slug")
|
|
||||||
if [[ ${#MISSING[@]} -gt 0 ]]; then
|
|
||||||
log_error "Missing required arguments: ${MISSING[*]}"
|
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$HYPERVISOR" in
|
# Load env file if present
|
||||||
proxmox|xcpng|baremetal|mixed) ;;
|
if [[ -f "/root/.semaphore_env" ]]; then
|
||||||
*) log_error "Invalid hypervisor type: $HYPERVISOR (use: proxmox|xcpng|baremetal|mixed)"; exit 1 ;;
|
source /root/.semaphore_env
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ -z "${SEMAPHORE_TOKEN:-}" ]]; then
|
|
||||||
log_error "No SEMAPHORE_TOKEN available. Set in /root/.semaphore_env or pass --semaphore-token"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve XO vars — per-client override takes priority, then global env
|
# Resolve webhook — arg > env
|
||||||
EFFECTIVE_XO_URL="${XO_URL_OVERRIDE:-${XO_URL:-}}"
|
WEBHOOK_URL="${WEBHOOK_URL:-${N8N_WEBHOOK_URL:-}}"
|
||||||
EFFECTIVE_XO_TOKEN="${XO_TOKEN_OVERRIDE:-${XO_TOKEN:-}}"
|
if [[ -z "${WEBHOOK_URL:-}" ]]; then
|
||||||
|
echo "ERROR: --webhook is required (or set N8N_WEBHOOK_URL in /root/.semaphore_env)"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
# Resolve webhook — per-client override takes priority, then global env
|
# Resolve XO vars — arg > env
|
||||||
EFFECTIVE_WEBHOOK="${WEBHOOK_URL_OVERRIDE:-${N8N_WEBHOOK_URL:-}}"
|
RESOLVED_XO_URL="${XO_URL_ARG:-${XO_URL:-}}"
|
||||||
if [[ -z "$EFFECTIVE_WEBHOOK" ]]; then
|
RESOLVED_XO_TOKEN="${XO_TOKEN_ARG:-${XO_TOKEN:-}}"
|
||||||
log_error "No webhook URL. Set N8N_WEBHOOK_URL in /root/.semaphore_env or pass --webhook"
|
|
||||||
exit 1
|
# Validate XO vars for xcpng/mixed
|
||||||
|
if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
|
||||||
|
if [[ -z "$RESOLVED_XO_URL" || -z "$RESOLVED_XO_TOKEN" ]]; then
|
||||||
|
echo "ERROR: --xo-url and --xo-token are required for hypervisor type '$HYPERVISOR'"
|
||||||
|
echo " (or set XO_URL and XO_TOKEN in /root/.semaphore_env)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PROJECT_NAME="${PROJECT_NAME_OVERRIDE:-Client - ${CLIENT_NAME}}"
|
PROJECT_NAME="${PROJECT_NAME_OVERRIDE:-Client - ${CLIENT_NAME}}"
|
||||||
@@ -133,78 +110,56 @@ INVENTORY_DIR="$REPO_DIR/inventories/client_${CLIENT_SLUG}"
|
|||||||
INVENTORY_REPO_PATH="inventories/client_${CLIENT_SLUG}/hosts.yml"
|
INVENTORY_REPO_PATH="inventories/client_${CLIENT_SLUG}/hosts.yml"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo " Onboarding: $CLIENT_NAME ($CLIENT_ID)"
|
echo " Onboarding client: $CLIENT_NAME ($CLIENT_ID)"
|
||||||
echo " Slug: client_${CLIENT_SLUG}"
|
[[ "$DRY_RUN" == true ]] && echo " ⚠ DRY RUN — no changes will be made"
|
||||||
echo " Hypervisor: $HYPERVISOR"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo " Billing: $BILLING"
|
|
||||||
[[ "$DRY_RUN" == "true" ]] && echo -e " ${YELLOW}DRY RUN MODE — no changes will be made${NC}"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
|
|
||||||
# ─── Step 1: Generate SSH key ─────────────────────────────────────────────────
|
# ─── Step 1: Generate SSH key ─────────────────────────────────────────────────
|
||||||
log_section "1/6 — SSH key"
|
echo ""
|
||||||
|
echo "[ 1/6 ] Generating SSH key..."
|
||||||
if [[ -f "$KEY_FILE" ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
log_warn "Key already exists at $KEY_FILE — skipping generation"
|
echo " [dry-run] Would generate SSH key at $KEY_FILE"
|
||||||
|
elif [[ -f "$KEY_FILE" ]]; then
|
||||||
|
echo " ⚠ Key already exists at $KEY_FILE — skipping generation"
|
||||||
else
|
else
|
||||||
if ! dry "ssh-keygen -t ed25519 -C ansible-${CLIENT_SLUG} -f $KEY_FILE -N ''"; then
|
ssh-keygen -t ed25519 -C "ansible-${CLIENT_SLUG}" -f "$KEY_FILE" -N ""
|
||||||
ssh-keygen -t ed25519 -C "ansible-${CLIENT_SLUG}" -f "$KEY_FILE" -N ""
|
echo " ✓ Key generated: $KEY_FILE"
|
||||||
log_ok "Key generated: $KEY_FILE"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
if [[ "$DRY_RUN" != true ]]; then
|
||||||
echo " ┌─ Public key to deploy to all client hosts ──────────────────────────"
|
echo ""
|
||||||
echo " │"
|
echo " ┌─ Deploy this public key to all client hosts ───────────────────────"
|
||||||
sed 's/^/ │ /' "$KEY_FILE.pub" 2>/dev/null || echo " │ (dry run — key not yet generated)"
|
echo " │"
|
||||||
echo " │"
|
sed 's/^/ │ /' "$KEY_FILE.pub"
|
||||||
echo " └─────────────────────────────────────────────────────────────────────"
|
echo " │"
|
||||||
|
echo " └────────────────────────────────────────────────────────────────────"
|
||||||
|
fi
|
||||||
|
|
||||||
# ─── Step 2: Create inventory from template ───────────────────────────────────
|
# ─── Step 2: Create inventory from template ───────────────────────────────────
|
||||||
log_section "2/6 — Inventory scaffold"
|
echo ""
|
||||||
|
echo "[ 2/6 ] Creating inventory..."
|
||||||
|
|
||||||
if [[ -d "$INVENTORY_DIR" ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
log_warn "Inventory already exists at $INVENTORY_DIR — skipping"
|
echo " [dry-run] Would create inventory at $INVENTORY_DIR"
|
||||||
|
elif [[ -d "$INVENTORY_DIR" ]]; then
|
||||||
|
echo " ⚠ Inventory directory already exists — skipping"
|
||||||
else
|
else
|
||||||
if ! dry "cp -r $REPO_DIR/inventories/client_template $INVENTORY_DIR"; then
|
cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR"
|
||||||
cp -r "$REPO_DIR/inventories/client_template" "$INVENTORY_DIR"
|
mkdir -p "$INVENTORY_DIR/group_vars"
|
||||||
|
|
||||||
# Determine which hypervisor groups to include
|
# Build xcpng_hosts group if applicable
|
||||||
INCLUDE_XCPNG=false
|
XCPNG_HOSTS_BLOCK=""
|
||||||
INCLUDE_PROXMOX=false
|
if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
|
||||||
case "$HYPERVISOR" in
|
XCPNG_HOSTS_BLOCK="
|
||||||
xcpng) INCLUDE_XCPNG=true ;;
|
|
||||||
proxmox) INCLUDE_PROXMOX=true ;;
|
|
||||||
mixed) INCLUDE_XCPNG=true; INCLUDE_PROXMOX=true ;;
|
|
||||||
baremetal) ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
XCPNG_BLOCK=""
|
|
||||||
if [[ "$INCLUDE_XCPNG" == "true" ]]; then
|
|
||||||
XCPNG_BLOCK=$(cat << 'XCPNGEOF'
|
|
||||||
|
|
||||||
# XCP-NG pool entries — one entry per pool (not per hypervisor host)
|
|
||||||
# Each entry triggers xcpng_pool_update.yml against that pool via XO REST API
|
|
||||||
# Required per host: xo_pool_uuid
|
|
||||||
# XO_URL and XO_TOKEN come from Semaphore variable group (override here if needed)
|
|
||||||
xcpng_hosts:
|
xcpng_hosts:
|
||||||
hosts: {}
|
hosts: {}
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local"
|
||||||
XCPNGEOF
|
fi
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$INVENTORY_DIR/hosts.yml" << HOSTSEOF
|
cat > "$INVENTORY_DIR/hosts.yml" << HOSTSEOF
|
||||||
---
|
---
|
||||||
# Client: ${CLIENT_NAME} (${CLIENT_ID})
|
|
||||||
# Onboarded: $(date +%Y-%m-%d)
|
|
||||||
# Hypervisor: ${HYPERVISOR}
|
|
||||||
# Billing: ${BILLING}
|
|
||||||
#
|
|
||||||
# ansible_user: ansible-msp-agent (deployed by scripts/deploy_agent.sh)
|
|
||||||
# Do NOT use root as ansible_user for day-to-day operations.
|
|
||||||
|
|
||||||
all:
|
all:
|
||||||
vars:
|
vars:
|
||||||
client_id: "${CLIENT_ID}"
|
client_id: "${CLIENT_ID}"
|
||||||
@@ -215,6 +170,7 @@ all:
|
|||||||
maintenance_window_tz: "UTC"
|
maintenance_window_tz: "UTC"
|
||||||
change_freeze: false
|
change_freeze: false
|
||||||
hypervisor_type: "${HYPERVISOR}"
|
hypervisor_type: "${HYPERVISOR}"
|
||||||
|
vpn_type: "${VPN}"
|
||||||
auto_reboot: false
|
auto_reboot: false
|
||||||
human_estimate_seconds: ${ESTIMATE}
|
human_estimate_seconds: ${ESTIMATE}
|
||||||
|
|
||||||
@@ -225,10 +181,6 @@ all:
|
|||||||
ansible_user: ansible-msp-agent
|
ansible_user: ansible-msp-agent
|
||||||
ansible_become: true
|
ansible_become: true
|
||||||
ansible_become_method: sudo
|
ansible_become_method: sudo
|
||||||
# Per-host vars to set:
|
|
||||||
# ansible_host: <ip>
|
|
||||||
# xcpng_vm_uuid: <uuid> (if hypervisor is xcpng or mixed)
|
|
||||||
# proxmox_vmid: <id> (if hypervisor is proxmox or mixed)
|
|
||||||
|
|
||||||
windows_hosts:
|
windows_hosts:
|
||||||
hosts: {}
|
hosts: {}
|
||||||
@@ -238,149 +190,210 @@ all:
|
|||||||
ansible_winrm_transport: ntlm
|
ansible_winrm_transport: ntlm
|
||||||
ansible_winrm_server_cert_validation: validate
|
ansible_winrm_server_cert_validation: validate
|
||||||
ansible_port: 5986
|
ansible_port: 5986
|
||||||
# Windows patching not yet implemented — hosts listed for inventory completeness
|
${XCPNG_HOSTS_BLOCK}
|
||||||
# Per-host vars to set:
|
|
||||||
# ansible_host: <ip>
|
|
||||||
# xcpng_vm_uuid: <uuid> (if hypervisor is xcpng or mixed)
|
|
||||||
${XCPNG_BLOCK}
|
|
||||||
HOSTSEOF
|
HOSTSEOF
|
||||||
|
|
||||||
cat > "$INVENTORY_DIR/group_vars/all.yml" << VARSEOF
|
cat > "$INVENTORY_DIR/group_vars/all.yml" << VARSEOF
|
||||||
---
|
---
|
||||||
# Client: ${CLIENT_NAME} (${CLIENT_ID})
|
# Client: ${CLIENT_NAME} (${CLIENT_ID})
|
||||||
# Onboarded: $(date +%Y-%m-%d)
|
# Onboarded: $(date +%Y-%m-%d)
|
||||||
|
# VPN: ${VPN}
|
||||||
|
# Hypervisor: ${HYPERVISOR}
|
||||||
|
# Billing: ${BILLING}
|
||||||
|
|
||||||
# Client-specific variable overrides go here.
|
# Add client-specific overrides below
|
||||||
# Global vars (XO_URL, XO_TOKEN, N8N_WEBHOOK_URL) come from Semaphore variable group.
|
|
||||||
# Override here only if this client uses a different XO instance or webhook.
|
|
||||||
VARSEOF
|
VARSEOF
|
||||||
|
|
||||||
log_ok "Inventory created at $INVENTORY_DIR"
|
echo " ✓ Inventory created at $INVENTORY_DIR"
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── Step 3: Commit and push ──────────────────────────────────────────────────
|
# ─── Step 3: Commit and push to Gitea ────────────────────────────────────────
|
||||||
log_section "3/6 — Git commit"
|
echo ""
|
||||||
|
echo "[ 3/6 ] Committing to Gitea..."
|
||||||
if ! dry "git add . && git commit && git push"; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo " [dry-run] Would commit and push inventory to Gitea"
|
||||||
|
else
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
git add .
|
git add .
|
||||||
git diff --cached --quiet && log_warn "Nothing to commit" || \
|
git commit -m "Onboard client: ${CLIENT_NAME} (${CLIENT_ID}) — inventory scaffold" \
|
||||||
git commit -m "Onboard client: ${CLIENT_NAME} (${CLIENT_ID}) — inventory scaffold"
|
|| echo " ⚠ Nothing to commit"
|
||||||
git push origin main
|
git push origin main
|
||||||
log_ok "Pushed to Gitea"
|
echo " ✓ Pushed to Gitea"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── Step 4: Semaphore project ────────────────────────────────────────────────
|
# ─── Step 4: Create Semaphore project via API ─────────────────────────────────
|
||||||
log_section "4/6 — Semaphore project"
|
echo ""
|
||||||
|
echo "[ 4/6 ] Creating Semaphore project via API..."
|
||||||
|
|
||||||
if dry "Create Semaphore project + keys + repo + env + inventory"; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
PROJECT_ID=0; GITEA_KEY_ID=0; CLIENT_KEY_ID=0
|
echo " [dry-run] Would create Semaphore project: $PROJECT_NAME"
|
||||||
NONE_KEY_ID=0; REPO_ID=0; ENV_ID=0; INVENTORY_ID=0
|
echo " [dry-run] Would create keys, repo, environment, inventory, and templates"
|
||||||
else
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " [dry-run] Onboarding simulation complete: $CLIENT_NAME"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# 4a. Project
|
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
|
||||||
|
|
||||||
|
# 4a. Create project
|
||||||
PROJECT_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/projects" \
|
PROJECT_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/projects" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"$PROJECT_NAME\",\"alert\":false,\"max_parallel_tasks\":0}")
|
-d "{\"name\": \"$PROJECT_NAME\", \"alert\": false, \"max_parallel_tasks\": 0}")
|
||||||
PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r '.id')
|
PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r '.id')
|
||||||
[[ "$PROJECT_ID" == "null" || -z "$PROJECT_ID" ]] && {
|
|
||||||
log_error "Failed to create project: $PROJECT_RESPONSE"; exit 1; }
|
|
||||||
log_ok "Project: $PROJECT_NAME (ID: $PROJECT_ID)"
|
|
||||||
|
|
||||||
# 4b. Gitea deploy key
|
if [[ "$PROJECT_ID" == "null" || -z "$PROJECT_ID" ]]; then
|
||||||
GITEA_KEY_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
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
|
||||||
|
GITEA_KEY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"gitea-deploy\",\"type\":\"ssh\",\"project_id\":$PROJECT_ID,
|
-d "{
|
||||||
\"ssh\":{\"login\":\"\",\"passphrase\":\"\",\"private_key\":$(jq -Rs . < "$GITEA_DEPLOY_KEY")}}" \
|
\"name\": \"gitea-deploy\",
|
||||||
| jq -r '.id')
|
\"type\": \"ssh\",
|
||||||
log_ok "gitea-deploy key (ID: $GITEA_KEY_ID)"
|
\"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. Client SSH key
|
# 4c. Add client SSH key
|
||||||
CLIENT_KEY_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
CLIENT_KEY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"client-${CLIENT_SLUG}-ssh\",\"type\":\"ssh\",\"project_id\":$PROJECT_ID,
|
-d "{
|
||||||
\"ssh\":{\"login\":\"\",\"passphrase\":\"\",\"private_key\":$(jq -Rs . < "$KEY_FILE")}}" \
|
\"name\": \"client-${CLIENT_SLUG}-ssh\",
|
||||||
| jq -r '.id')
|
\"type\": \"ssh\",
|
||||||
log_ok "Client SSH key (ID: $CLIENT_KEY_ID)"
|
\"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. None key
|
# 4d. Add None key
|
||||||
NONE_KEY_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
NONE_KEY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/keys" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"None\",\"type\":\"none\",\"project_id\":$PROJECT_ID}" \
|
-d "{
|
||||||
| jq -r '.id')
|
\"name\": \"None\",
|
||||||
log_ok "None key (ID: $NONE_KEY_ID)"
|
\"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. Repository
|
# 4e. Add repository
|
||||||
REPO_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/repositories" \
|
REPO_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/repositories" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"ansible-msp-automations\",\"project_id\":$PROJECT_ID,
|
-d "{
|
||||||
\"git_url\":\"$GITEA_REPO_URL\",\"git_branch\":\"main\",
|
\"name\": \"ansible-msp-automations\",
|
||||||
\"ssh_key_id\":$GITEA_KEY_ID}" \
|
\"project_id\": $PROJECT_ID,
|
||||||
| jq -r '.id')
|
\"git_url\": \"$GITEA_REPO_URL\",
|
||||||
log_ok "Repository (ID: $REPO_ID)"
|
\"git_branch\": \"main\",
|
||||||
|
\"ssh_key_id\": $GITEA_KEY_ID
|
||||||
|
}")
|
||||||
|
REPO_ID=$(echo "$REPO_RESPONSE" | jq -r '.id')
|
||||||
|
echo " ✓ Repository linked (ID: $REPO_ID)"
|
||||||
|
|
||||||
# 4f. Variable group — only include hypervisor vars that are set
|
# 4f. Build variable group JSON — include XO vars for xcpng/mixed
|
||||||
VARS_JSON=$(jq -n \
|
if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
|
||||||
--arg webhook "$EFFECTIVE_WEBHOOK" \
|
VARS_JSON=$(jq -n \
|
||||||
--arg cid "$CLIENT_ID" \
|
--arg webhook "$WEBHOOK_URL" \
|
||||||
--arg cname "$CLIENT_NAME" \
|
--arg cid "$CLIENT_ID" \
|
||||||
--arg billing "$BILLING" \
|
--arg cname "$CLIENT_NAME" \
|
||||||
--arg estimate "$ESTIMATE" \
|
--arg billing "$BILLING" \
|
||||||
--arg phost "$PROXMOX_HOST" \
|
--arg estimate "$ESTIMATE" \
|
||||||
--arg ptid "$PROXMOX_TOKEN_ID" \
|
--arg xo_url "$RESOLVED_XO_URL" \
|
||||||
--arg ptsecret "$PROXMOX_TOKEN_SECRET" \
|
--arg xo_token "$RESOLVED_XO_TOKEN" \
|
||||||
--arg xourl "$EFFECTIVE_XO_URL" \
|
'{
|
||||||
--arg xotoken "$EFFECTIVE_XO_TOKEN" \
|
N8N_WEBHOOK_URL: $webhook,
|
||||||
'{
|
CLIENT_ID: $cid,
|
||||||
N8N_WEBHOOK_URL: $webhook,
|
CLIENT_NAME: $cname,
|
||||||
CLIENT_ID: $cid,
|
BILLING_MODEL: $billing,
|
||||||
CLIENT_NAME: $cname,
|
HUMAN_ESTIMATE_SECONDS: $estimate,
|
||||||
BILLING_MODEL: $billing,
|
XO_URL: $xo_url,
|
||||||
HUMAN_ESTIMATE_SECONDS: $estimate
|
XO_TOKEN: $xo_token
|
||||||
}
|
}')
|
||||||
| if $phost != "" then . + {PROXMOX_HOST: $phost} else . end
|
else
|
||||||
| if $ptid != "" then . + {PROXMOX_TOKEN_ID: $ptid} else . end
|
VARS_JSON=$(jq -n \
|
||||||
| if $ptsecret != "" then . + {PROXMOX_TOKEN_SECRET: $ptsecret} else . end
|
--arg webhook "$WEBHOOK_URL" \
|
||||||
| if $xourl != "" then . + {XO_URL: $xourl} else . end
|
--arg cid "$CLIENT_ID" \
|
||||||
| if $xotoken != "" then . + {XO_TOKEN: $xotoken} else . end
|
--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
|
||||||
|
}')
|
||||||
|
fi
|
||||||
|
|
||||||
ENV_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/environment" \
|
ENV_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/environment" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"${CLIENT_SLUG}-vars\",\"project_id\":$PROJECT_ID,
|
-d "{
|
||||||
\"json\":$(echo "$VARS_JSON" | jq -Rs .),\"env\":\"{}\"}" \
|
\"name\": \"${CLIENT_SLUG}-vars\",
|
||||||
| jq -r '.id')
|
\"project_id\": $PROJECT_ID,
|
||||||
log_ok "Variable group (ID: $ENV_ID)"
|
\"json\": $(echo "$VARS_JSON" | jq -Rs .),
|
||||||
|
\"env\": \"{}\"
|
||||||
|
}")
|
||||||
|
ENV_ID=$(echo "$ENV_RESPONSE" | jq -r '.id')
|
||||||
|
echo " ✓ Variable group created (ID: $ENV_ID)"
|
||||||
|
|
||||||
# 4g. Inventory
|
# 4g. Add inventory
|
||||||
INVENTORY_ID=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/inventory" \
|
INVENTORY_RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/inventory" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"client-${CLIENT_SLUG}\",\"project_id\":$PROJECT_ID,
|
-d "{
|
||||||
\"inventory\":\"$INVENTORY_REPO_PATH\",\"ssh_key_id\":$CLIENT_KEY_ID,
|
\"name\": \"client-${CLIENT_SLUG}\",
|
||||||
\"become_key_id\":$NONE_KEY_ID,\"type\":\"file\",
|
\"project_id\": $PROJECT_ID,
|
||||||
\"repository_id\":$REPO_ID}" \
|
\"inventory\": \"$INVENTORY_REPO_PATH\",
|
||||||
| jq -r '.id')
|
\"ssh_key_id\": $CLIENT_KEY_ID,
|
||||||
log_ok "Inventory (ID: $INVENTORY_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)"
|
||||||
|
|
||||||
fi # end dry run block
|
# ─── Step 5: Create task templates ───────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
# ─── Step 5: Task templates ───────────────────────────────────────────────────
|
echo "[ 5/6 ] Creating task templates..."
|
||||||
log_section "5/6 — Task templates"
|
|
||||||
|
|
||||||
create_template() {
|
create_template() {
|
||||||
local TNAME="$1"
|
local TNAME="$1"
|
||||||
local PLAYBOOK="$2"
|
local PLAYBOOK="$2"
|
||||||
local DESC="$3"
|
local DESC="$3"
|
||||||
if dry "Template: $TNAME → $PLAYBOOK"; then return; fi
|
local EXTRA_ARGS="${4:-[]}"
|
||||||
RESULT=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/templates" \
|
RESPONSE=$(curl -s -X POST "$SEMAPHORE_URL/api/project/$PROJECT_ID/templates" \
|
||||||
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
-H "Authorization: Bearer $SEMAPHORE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
@@ -390,56 +403,48 @@ create_template() {
|
|||||||
\"environment_id\": $ENV_ID,
|
\"environment_id\": $ENV_ID,
|
||||||
\"name\": \"$TNAME\",
|
\"name\": \"$TNAME\",
|
||||||
\"playbook\": \"$PLAYBOOK\",
|
\"playbook\": \"$PLAYBOOK\",
|
||||||
\"arguments\": \"[]\",
|
\"arguments\": \"$EXTRA_ARGS\",
|
||||||
\"allow_override_args_in_task\": false,
|
\"allow_override_args_in_task\": true,
|
||||||
\"description\": \"$DESC\",
|
\"description\": \"$DESC\",
|
||||||
\"app\": \"ansible\"
|
\"app\": \"ansible\"
|
||||||
}")
|
}")
|
||||||
log_ok "$(echo "$RESULT" | jq -r '"Template: \(.name) (ID: \(.id))"')"
|
echo " ✓ $(echo "$RESPONSE" | jq -r '"Template: \(.name) (ID: \(.id))"')"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Always created
|
create_template "Preflight Check" "playbooks/site_preflight.yml" "Run safety checks on all hosts before maintenance"
|
||||||
create_template "Preflight Check" "playbooks/site_preflight.yml" "Safety checks on all hosts before maintenance"
|
create_template "Linux Patch" "playbooks/linux_patch.yml" "Full Linux patch run with version tracking"
|
||||||
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"
|
||||||
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"
|
||||||
|
|
||||||
# Proxmox
|
# Hypervisor-specific templates
|
||||||
case "$HYPERVISOR" in proxmox|mixed)
|
if [[ "$HYPERVISOR" == "proxmox" || "$HYPERVISOR" == "mixed" ]]; then
|
||||||
create_template "Snapshot (Proxmox)" "playbooks/snapshot_pre.yml" "Pre-patch VM snapshots via Proxmox API"
|
create_template "Snapshot (Proxmox)" "playbooks/snapshot_pre.yml" "Pre-patch snapshot via Proxmox API"
|
||||||
;; esac
|
fi
|
||||||
|
if [[ "$HYPERVISOR" == "xcpng" || "$HYPERVISOR" == "mixed" ]]; then
|
||||||
|
create_template "Snapshot (XCP-NG)" "playbooks/snapshot_pre.yml" "Pre-patch snapshot via XO REST API"
|
||||||
|
create_template "XCP-NG Pool Update" "playbooks/xcpng_pool_update.yml" "Update XCP-NG host pool patches"
|
||||||
|
fi
|
||||||
|
|
||||||
# XCP-NG
|
# ─── Step 6: Summary ─────────────────────────────────────────────────────────
|
||||||
case "$HYPERVISOR" in xcpng|mixed)
|
|
||||||
create_template "XCP-NG Pool Update" "playbooks/xcpng_pool_update.yml" "Patch XCP-NG hypervisor pools via XO REST API"
|
|
||||||
create_template "Snapshot (XCP-NG)" "playbooks/snapshot_pre.yml" "Pre-patch VM snapshots via XO REST API"
|
|
||||||
;; esac
|
|
||||||
|
|
||||||
[[ "$HYPERVISOR" == "baremetal" ]] && \
|
|
||||||
log_warn "Baremetal — no snapshot templates created. Ensure change approval before patching."
|
|
||||||
|
|
||||||
# ─── Step 6: Summary ──────────────────────────────────────────────────────────
|
|
||||||
log_section "6/6 — Done"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo " ✓ ${CLIENT_NAME} (${CLIENT_ID}) onboarded"
|
echo " ✓ Client onboarding complete: $CLIENT_NAME"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo ""
|
echo ""
|
||||||
[[ "$DRY_RUN" != "true" ]] && echo " Semaphore project ID : $PROJECT_ID"
|
echo " Semaphore project ID: $PROJECT_ID"
|
||||||
echo " Inventory : $INVENTORY_DIR/hosts.yml"
|
echo " Inventory file: $INVENTORY_DIR/hosts.yml"
|
||||||
echo " Hypervisor : $HYPERVISOR"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Next steps:"
|
echo " Next steps:"
|
||||||
echo " 1. Add hosts to inventory then git push"
|
echo " 1. Add hosts to: $INVENTORY_DIR/hosts.yml"
|
||||||
echo " 2. bash scripts/deploy_agent.sh --inventory $INVENTORY_REPO_PATH"
|
echo " 2. Run deploy_agent.sh to bootstrap ansible-msp-agent on Linux hosts"
|
||||||
echo " 3. Semaphore → $PROJECT_NAME → Preflight Check → ▶ Run"
|
echo " 3. git add . && git commit -m 'Add hosts for ${CLIENT_NAME}' && git push origin main"
|
||||||
[[ "$HYPERVISOR" != "baremetal" ]] && \
|
echo " 4. Semaphore → $PROJECT_NAME → Preflight Check → ▶ Run"
|
||||||
echo " 4. Semaphore → $PROJECT_NAME → XCP-NG Pool Update / Snapshot → ▶ Run"
|
echo " 5. Semaphore → $PROJECT_NAME → Full Maintenance → ▶ Run"
|
||||||
[[ "$HYPERVISOR" == "baremetal" ]] && \
|
|
||||||
echo " NOTE: Baremetal — no snapshots. Get explicit change approval before patching."
|
|
||||||
echo ""
|
echo ""
|
||||||
if [[ "$DRY_RUN" != "true" && -f "$KEY_FILE.pub" ]]; then
|
echo " To reboot hosts after a deferred patch run:"
|
||||||
echo " Client public key:"
|
echo " Semaphore → $PROJECT_NAME → Scheduled Reboot → ▶ Run"
|
||||||
sed 's/^/ /' "$KEY_FILE.pub"
|
echo " (add extra var force_reboot=true to reboot all hosts regardless)"
|
||||||
fi
|
echo ""
|
||||||
|
echo " Public key to deploy to client hosts:"
|
||||||
|
sed 's/^/ /' "$KEY_FILE.pub"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user