Proxmox VEXX · CLI & AutomationAPI automation
pvesh: scripting the PVE API like a pro
What you'll learn
- Use pvesh to query and modify cluster state from the CLI
- Build repeatable automation scripts using pvesh + jq
- Authenticate against the API safely with API tokens
- Handle rate limits and pagination in automation
Prerequisites
None — start here.
Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-07
pvesh: scripting the PVE API like a pro
pvesh is the PVE command-line client for the same HTTP API that
the GUI uses. Anything the GUI can do, pvesh can do from a script.
This lesson covers the practical patterns.
The basic shape of pvesh
# Get a resource (returns JSON)
pvesh get /cluster/resources --type node --output-format json
# Get a specific resource
pvesh get /nodes/pve-01/status
# Create a resource
pvesh create /nodes/pve-01/qemu --vmid 100 --memory 1024 ...
# Set / update a resource
pvesh set /nodes/pve-01/qemu/100 --memory 2048
# Delete a resource
pvesh delete /nodes/pve-01/qemu/100
The URL paths follow the PVE resource tree:
/cluster— cluster-wide/nodes/{node}— per-node/nodes/{node}/qemu/{vmid}— VMs/nodes/{node}/lxc/{ctid}— containers/storage— storage backends/access/users— users/access/roles— roles
Authentication: API tokens
For automation, API tokens are safer than password auth:
# Create a token for a user
pvesh create /access/users/admin/token/automation \
--comment "Automation token" \
--privsep 0
# Returns: "Your new access token: ..." — save the secret!
# Format: USER!TOKENID=UUID
# Example: admin@pam!automation=12345678-1234-1234-1234-123456789abc
# Set up environment
export PVE_API_TOKEN="admin@pam!automation=12345678-1234-1234-1234-123456789abc"
export PVE_HOST="https://pve-01.cluster.example.com:8006/api2/json"
# Now use pvesh
pvesh get /cluster/resources --output-format json
The token has the same privileges as the user that owns it. For least-privilege automation:
# Create a dedicated automation user with restricted privileges
pvesh create /access/users/automation@pve --comment "Service account"
pvesh create /access/roles/AutomationRole \
--privs "VM.Audit,Datastore.Audit,Pool.Audit"
pvesh create /access/acl --path / --roles AutomationRole --users automation@pve
Now the automation user can read but not modify the cluster.
Common queries
# All nodes
pvesh get /nodes --output-format json | jq '.[] | {node, status, cpu, mem, maxmem}'
# All VMs across the cluster
pvesh get /cluster/resources --type vm --output-format json | \
jq '.[] | {vmid, name, node, status, cpu, mem}'
# All stopped VMs
pvesh get /cluster/resources --type vm --output-format json | \
jq '.[] | select(.status == "stopped") | .name'
# All VMs that don\'t have a backup in the last 7 days
pvesh get /cluster/resources --type vm --output-format json | \
jq -r '.[] | select(.backup == null or (.backup | . < (now - 604800))) | .name'
# Adjust "backup" check based on your schema
# Storage usage across the cluster
pvesh get /storage --output-format json | \
jq '.[] | {storage, type, total, used, avail, usage_percent: ((.used / .total) * 100 | round)}'
# Find the host of a specific VM
pvesh get /cluster/resources --type vm --output-format json | \
jq '.[] | select(.vmid == 100) | .node'
Automation patterns
Bulk start / stop
#!/bin/bash
# Start all VMs tagged "batch-worker"
pvesh get /cluster/resources --type vm --output-format json | \
jq -r '.[] | select(.tags | test("batch-worker")) | .vmid' | \
while read vmid; do
echo "Starting $vmid"
pvesh create /nodes/$(pvesh get /cluster/resources --type vm --vmid $vmid --output-format json | jq -r '.[0].node')/qemu/$vmid/status/start
done
Right-sizing from monitoring data
#!/bin/bash
# Adjust VM memory based on actual usage from Prometheus
for vmid in $(pvesh get /cluster/resources --type vm --output-format json | jq -r '.[].vmid'); do
node=$(pvesh get /cluster/resources --type vm --vmid $vmid --output-format json | jq -r '.[0].node')
# Get 99th percentile memory usage from Prometheus
used_mb=$(curl -sG http://prometheus:9090/api/v1/query \
--data-urlencode 'query=quantile_over_time(0.99, pve_memory_usage_bytes{vmid="'$vmid'"}[7d]) / 1024 / 1024' | \
jq -r '.data.result[0].value[1] | floor')
# Set memory to 1.5x peak usage, rounded up to nearest 256 MB
new_mem=$(( (used_mb * 3 / 2 / 256 + 1) * 256 ))
echo "VM $vmid: peak ${used_mb}MB -> set memory ${new_mem}MB"
pvesh set /nodes/$node/qemu/$vmid --memory $new_mem
done
Run this weekly. VMs that are over-provisioned get smaller; VMs that are at peak get more headroom.
Bulk snapshot before risky operation
#!/bin/bash
# Snapshot all production VMs before a maintenance window
TAG="pre-maintenance-$(date +%Y%m%d)"
for vmid in $(pvesh get /cluster/resources --type vm --output-format json | jq -r '.[] | select(.tags | test("production")) | .vmid'); do
pvesh create /nodes/$(pvesh get /cluster/resources --type vm --vmid $vmid --output-format json | jq -r '.[0].node')/qemu/$vmid/snapshot \
--snapname "$TAG" --description "Pre-maintenance snapshot" \
--vmstate 1
done
Rate limits and pagination
The PVE API enforces rate limits. For bulk operations:
# Add a small delay between requests
for vmid in 100 101 102 103 104 105; do
pvesh set /nodes/pve-01/qemu/$vmid --tag "migrated" 2>/dev/null
sleep 0.1
done
For listing large result sets, pagination is automatic but you may want to control it:
# pvesh automatically paginates; the full result is returned
# For very large clusters, use the --limit flag
pvesh get /cluster/resources --type vm --limit 500 --output-format json
Idempotent automation
For automation that runs repeatedly, make it idempotent:
#!/bin/bash
# Ensure all VMs in the production-pool are tagged
existing=$(pvesh get /pools/production-pool --output-format json | jq -r '.members[].vmid')
for vmid in $(echo "$existing" | xargs); do
current=$(pvesh get /cluster/resources --type vm --vmid $vmid --output-format json | jq -r '.[0].tags // ""')
if ! echo "$current" | grep -q "production"; then
echo "Adding tag to VM $vmid"
new_tags="$current production"
pvesh set /nodes/$(pvesh get /cluster/resources --type vm --vmid $vmid --output-format json | jq -r '.[0].node')/qemu/$vmid --tags "$new_tags"
fi
done
Idempotent scripts are safe to run via cron — they don’t error if the state is already correct.
Production considerations
- API tokens never expire by default. Set an expiry for tokens
used by people (
--expire 365). Tokens for service accounts can be long-lived but document them. - Audit log of API access. Every API call is logged with the user and token. Review periodically for unexpected access.
- Read-only tokens. A read-only role prevents accidental destructive operations during automation.
- API version compatibility. pvesh is tied to the PVE version. Upgrade pvesh and the cluster together.
Common mistakes
- Using the root user for automation. Use a dedicated automation user with the minimum required privileges.
- Long-running API calls without timeout. If an API call hangs,
the script hangs. Add
curl --max-timeortimeoutwrappers. - No idempotency. Scripts that fail on re-run are fragile.
- Logging API tokens. Tokens in logs are tokens in the wrong hands. Mask them.
Key takeaways
- pvesh gives scriptable access to everything the GUI can do.
- Use API tokens with least privilege.
- Pipe through jq for filtering and transformation.
- Make automation idempotent and safe to re-run.
Knowledge check
Knowledge check · 4 questions
Q1. Which command lists all VMs in a cluster via the API?
Q2. API tokens are safer than password auth for automation scripts.
Q3. Which of these are good practices for automation scripts? (Select all that apply)
Q4. Name the pvesh command to set a VM's memory to 4096 MB.
Passing score: 75%. Answers are checked in this browser.