From 4930f5c75c99fa54f29daf07888d4320f3d209c6 Mon Sep 17 00:00:00 2001 From: meteo Date: Sun, 17 May 2026 19:02:44 +0200 Subject: [PATCH] initial commit --- .env-sample | 3 + .gitignore | 2 + LICENSE | 21 ++++ README.md | 84 ++++++++++++++ proxmox-to-sentri.sh | 269 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 379 insertions(+) create mode 100644 .env-sample create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 proxmox-to-sentri.sh diff --git a/.env-sample b/.env-sample new file mode 100644 index 0000000..04012ec --- /dev/null +++ b/.env-sample @@ -0,0 +1,3 @@ +PM_HOST="https://proxmox.example.com:8006" +PM_USER="root@pam" +PM_PASS="super-secret-password" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a51f31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +/output/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ab57f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Marco Mooij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5d81e8 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Proxmox to Sentri +Proxmox to Sentri is a utility script that retrieves all virtual machines from a Proxmox cluster and imports them into Sentri as servers. + +The script collects information such as: + +- VM name +- Power state +- CPU and memory allocation +- Disk configuration +- IPv4 and IPv6 addresses +- Snapshot count +- Backup availability +- Operating system information +- VM descriptions/notes + +--- +# Requirements +## System Requirements +The machine running this script must: + +- Have network access to the Proxmox web interface/API +- Have the following packages installed: + - `curl` + - `jq` + - `git` (required for installation) + +## Proxmox Requirements +A Proxmox user account with permission to view VMs and storage information is required. + +Recommended role: +- `PVEAuditor` +## Optional: Guest IP Address Collection +To collect IPv4, IPv6, and operating system information from virtual machines, install the QEMU Guest Agent inside each VM. + +Example for AlmaLinux/RHEL: +```bash +sudo dnf install -y qemu-guest-agent +sudo systemctl enable --now qemu-guest-agent +``` +Then enable the QEMU Guest Agent in Proxmox: +``` +qm set --agent enabled=1 +``` + +--- +# Installation +Clone the repository and configure the environment file: +``` +cd /optgit clone https://gitea.mooij.me/meteo/proxmox-to-sentri.gitcd proxmox-to-sentricp .env-sample .envchmod 600 .envchmod 740 proxmox-to-sentri.sh +``` +Optional: Create a global command: +``` +ln -sf /opt/proxmox-to-sentri/proxmox-to-sentri.sh /usr/bin/proxmox-to-sentri +``` + +--- +# Configuration +Edit the `.env` file and configure the Proxmox connection settings: +``` +PM_HOST="https://proxmox.example.com:8006" +PM_USER="root@pam" +PM_PASS="your-password" +``` + +--- +# Running the Script + +Run the script directly: +``` +./proxmox-to-sentri.sh +``` +Or, if you created the symlink: +``` +proxmox-to-sentri +``` + +--- +# Output + +Generated JSON files are written to: +``` +/opt/proxmox-to-sentri/output/ +``` +One JSON file is generated per VM. \ No newline at end of file diff --git a/proxmox-to-sentri.sh b/proxmox-to-sentri.sh new file mode 100755 index 0000000..f7c76c3 --- /dev/null +++ b/proxmox-to-sentri.sh @@ -0,0 +1,269 @@ +#!/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/.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}" + +# 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") + +echo "$VMS" | jq -c '.data[]' | 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') + 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') + + 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 + jq -n \ + --arg vmid "$MAC" \ + --arg vmhost "$NAME" \ + --arg power "$STATUS" \ + --arg hostname "$NAME" \ + --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", + "server_hostname": $hostname + } + + + ( + 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 + } + ' > "${OUTPUT_DIR}/${VMID}.json" + +done + +echo +echo "Done." +echo "JSON files written to: ${OUTPUT_DIR}/" \ No newline at end of file