Skip to main content
RunBook Academy

Proxmox VEXX · CLI & AutomationAPI automation

pvesh: scripting the PVE API like a pro

Foundation⏱ ~18 min🧪 Lab requiredpveshjq

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

Not yet marked complete on this device.

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 '.[] | &#123;storage, type, total, used, avail, usage_percent: ((.used / .total) * 100 | round)&#125;'

# 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&#123;vmid="'$vmid'"&#125;[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 $&#123;used_mb&#125;MB -> set memory $&#123;new_mem&#125;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-time or timeout wrappers.
  • 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

  1. Q1. Which command lists all VMs in a cluster via the API?

  2. Q2. API tokens are safer than password auth for automation scripts.

  3. Q3. Which of these are good practices for automation scripts? (Select all that apply)

  4. Q4. Name the pvesh command to set a VM's memory to 4096 MB.

Passing score: 75%. Answers are checked in this browser.