340 lines
7.5 KiB
Bash
Executable File
340 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Proxmox VM Inventory Exporter
|
|
#
|
|
# Exports one JSON object per VM with:
|
|
# - CPU / RAM
|
|
# - Network IPs
|
|
# - Snapshots
|
|
# - Backup detection
|
|
# - VM notes
|
|
# - Disk information
|
|
#
|
|
# Requirements:
|
|
# - curl
|
|
# - jq
|
|
# - for ipv4 and ipv6 collection install the proxmox agent on the VM
|
|
#
|
|
# Files:
|
|
# .env
|
|
#
|
|
# Example .env:
|
|
# PM_HOST="https://proxmox.example.com:8006"
|
|
# PM_USER="root@pam"
|
|
# PM_PASS="password"
|
|
#
|
|
# Output:
|
|
# ./output/<vmid>.json
|
|
# -----------------------------------------------------------------------------
|
|
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
|
|
[]
|
|
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."
|