#!/usr/bin/env bash set -euo pipefail SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" OUTPUT_DIR="${SCRIPT_DIR}/output" mkdir -p "$OUTPUT_DIR" ENV_FILE="${SCRIPT_DIR}/.env" # Load environment variables if [[ -f "$ENV_FILE" ]]; then set -a source "$ENV_FILE" set +a else echo ".env file missing: $ENV_FILE" exit 1 fi # Validate vars : "${PM_HOST:?Missing PM_HOST}" : "${PM_USER:?Missing PM_USER}" : "${PM_PASS:?Missing PM_PASS}" # Sentri API configuration : "${SENTRI_URL:?Missing SENTRI_URL}" : "${SENTRI_TOKEN:?Missing SENTRI_TOKEN}" SERVERS_JSON="[]" # Authenticate AUTH_RESPONSE=$(curl -sk \ --data-urlencode "username=${PM_USER}" \ --data-urlencode "password=${PM_PASS}" \ "${PM_HOST}/api2/json/access/ticket") TICKET=$(echo "$AUTH_RESPONSE" | jq -r '.data.ticket // empty') CSRF=$(echo "$AUTH_RESPONSE" | jq -r '.data.CSRFPreventionToken // empty') if [[ "$TICKET" == "null" || -z "$TICKET" ]]; then echo "Authentication failed" exit 1 fi api_get() { local endpoint="$1" curl -sk \ -H "Cookie: PVEAuthCookie=${TICKET}" \ -H "CSRFPreventionToken: ${CSRF}" \ "${PM_HOST}${endpoint}" } echo "Collecting VMs..." VMS=$(api_get "/api2/json/cluster/resources?type=vm") while read -r VM; do VMID=$(echo "$VM" | jq -r '.vmid') NODE=$(echo "$VM" | jq -r '.node') NAME=$(echo "$VM" | jq -r '.name') STATUS=$(echo "$VM" | jq -r ' .status | if . == "running" then "Running" elif . == "stopped" then "Off" else "Running" end ') CPU=$(echo "$VM" | jq -r '.maxcpu') MEMORY_MB=$(echo "$VM" | jq -r '.maxmem / 1024 / 1024 | floor') echo "Processing VM ${VMID} (${NAME})" # VM Config CONFIG=$(api_get "/api2/json/nodes/${NODE}/qemu/${VMID}/config") DESCRIPTION=$(echo "$CONFIG" | jq -r '.data.description // empty') # Try to detect OS OS_INFO=$(api_get "/api2/json/nodes/${NODE}/qemu/${VMID}/agent/get-osinfo" || true) OS=$(echo "$OS_INFO" | jq -r ' .data["pretty-name"] // empty ') # Snapshot count SNAPSHOTS=$(api_get "/api2/json/nodes/${NODE}/qemu/${VMID}/snapshot") SNAPSHOT_COUNT=$(echo "$SNAPSHOTS" | jq '.data | length - 1') # Backup detection BACKUP_ENABLED="no" STORAGES=$(api_get "/api2/json/storage" | jq -r '.data[].storage') for STORAGE in $STORAGES; do CONTENT=$(api_get "/api2/json/nodes/${NODE}/storage/${STORAGE}/content" || true) if echo "$CONTENT" | jq -e --arg VMID "$VMID" ' .data[]? | select( .volid | test("vzdump-qemu-" + $VMID + "-") ) ' >/dev/null; then BACKUP_ENABLED="yes" break fi done # Network interfaces IPV4=() IPV6=() AGENT=$(api_get "/api2/json/nodes/${NODE}/qemu/${VMID}/agent/network-get-interfaces" || true) if echo "$AGENT" | jq -e '.data.result' >/dev/null 2>&1; then while read -r IP; do IPV4+=("$IP") done < <( echo "$AGENT" | jq -r ' .data.result[]."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address" ' \ | grep -v '^127\.' \ | grep -v '^172\.17\.' \ | grep -v '^172\.18\.' \ | grep -v '^172\.19\.' \ | grep -v '^172\.20\.' \ | grep -v '^172\.21\.' \ | grep -v '^172\.22\.' \ | grep -v '^172\.30\.' ) while read -r IP; do IPV6+=("$IP") done < <( echo "$AGENT" | jq -r ' .data.result[]."ip-addresses"[]? | select(."ip-address-type" == "ipv6") | ."ip-address" ' \ | grep -v '^::1' \ | grep -v '^fe80:' ) fi # MAC Address MAC=$(echo "$CONFIG" | jq -r ' .data.net0 // "" ' | sed -n 's/.*virtio=\([^,]*\).*/\1/p' \ | tr '[:upper:]' '[:lower:]' \ | tr -d ':') # Disks DISKS_JSON="[]" while read -r DISKKEY; do DISKCONF=$(echo "$CONFIG" | jq -r ".data.${DISKKEY}") STORAGE=$(echo "$DISKCONF" | cut -d':' -f1) DISK_LOCATION=$(echo "$DISKCONF" | cut -d',' -f1) SIZE=$(echo "$DISKCONF" | sed -n 's/.*size=\([^,]*\).*/\1/p' \ | awk ' function ceil(x) { return (x == int(x)) ? x : int(x)+1 } { if ($0 ~ /G$/) { gsub(/G/, "", $0) printf "%d", ceil($0) } else if ($0 ~ /M$/) { gsub(/M/, "", $0) printf "%d", ceil($0 / 1024) } else if ($0 ~ /T$/) { gsub(/T/, "", $0) printf "%d", ceil($0 * 1024) } else { printf "%d", $0 } } ') DISKS_JSON=$(echo "$DISKS_JSON" | jq \ --arg name "$DISKKEY" \ --arg size "$SIZE" \ --arg used "$SIZE" \ --arg location "$DISK_LOCATION" \ '. += [{ "disk_name": $name, "disk_space": $size, "disk_used": $used, "disk_location": $location }]' ) done < <( echo "$CONFIG" | jq -r ' .data | keys[] | select(test("^(virtio|scsi|sata|ide)[0-9]+$")) ' ) # Build JSON SERVER_JSON=$( jq -n \ --arg vmid "$MAC" \ --arg vmhost "$NAME" \ --arg power "$STATUS" \ --arg os "$OS" \ --argjson cpu "$CPU" \ --argjson memory "$MEMORY_MB" \ --argjson snapshot "$SNAPSHOT_COUNT" \ --arg description "$DESCRIPTION" \ --argjson disks "$DISKS_JSON" \ --argjson ipv4 "$(printf '%s\n' "${IPV4[@]:-}" | jq -R . | jq -s .)" \ --argjson ipv6 "$(printf '%s\n' "${IPV6[@]:-}" | jq -R . | jq -s .)" \ --arg backup "$BACKUP_ENABLED" ' { "server_vm_id": $vmid, "server_vm_host_name": $vmhost, "server_power_state": $power, "server_state": "new", } + ( if $os != "" then { "server_os": $os } else {} end ) + { "server_cpu": $cpu, "server_memory": $memory, "server_memory_demand": $memory, "server_disks": $disks, "server_ipv4": $ipv4, "server_ipv6": $ipv6, "server_vm_snapshot": $snapshot, "server_backup": ( if $backup == "yes" then [{"+proxmox":"yes"}] else [{"-proxmox":"no"}] end ), "server_description": $description } ') # Save per-VM JSON echo "$SERVER_JSON" > "${OUTPUT_DIR}/${VMID}.json" # Add VM to combined payload SERVERS_JSON=$(jq -n \ --argjson old "$SERVERS_JSON" \ --argjson server "$SERVER_JSON" \ '$old + [$server]') done < <( echo "$VMS" | jq -c '.data[]? // empty' ) FINAL_PAYLOAD=$(jq -n \ --argjson servers "$SERVERS_JSON" ' { "servers": $servers } ') # Save combined payload echo "$FINAL_PAYLOAD" | jq . > "${OUTPUT_DIR}/servers.json" echo echo "Uploading payload to Sentri..." RESPONSE=$(curl -sk \ -X POST \ -H "Authorization: Bearer ${SENTRI_TOKEN}" \ -H "Content-Type: application/json" \ --data "$FINAL_PAYLOAD" \ "${SENTRI_URL}" ) echo echo "Sentri API response:" echo "$RESPONSE" | jq . || echo "$RESPONSE" echo "Combined payload written to:" echo "${OUTPUT_DIR}/servers.json" echo "Individual server json files written to: ${OUTPUT_DIR}/" echo echo "Done."