Proxmox Host Utilities
Scripts for the Proxmox side of the fleet. They split by where they run:
| Script | Runs on | What it does |
|---|---|---|
pve-list-snapshots.sh | the PVE node | Lists every VM and LXC snapshot, read from /etc/pve |
pve-snapshots.py | anywhere, via API | The same listing, over the HTTP API |
auto-extend-lxc.sh | the PVE node | Grows any container rootfs that crosses 75% usage |
pve-trim-storage.sh | the PVE node | Runs fstrim across every running container and VM |
pve-add-ssh-key-to-lxcs.sh | the PVE node | Installs a public SSH key into every running container |
The shell scripts are installed to /usr/local/bin/ on the node. pve-snapshots.py uses
the HTTP API instead and runs from a laptop, the Ansible controller or CI.
Handing a key to the whole fleet
Section titled “Handing a key to the whole fleet”pve-add-ssh-key-to-lxcs.sh writes a public key into ~/.ssh/authorized_keys inside
every running LXC, going through pct exec rather than SSH. That is what makes it
useful: it reaches containers whose sshd rejects every login, so it works on a fleet you
cannot SSH into yet — the bootstrap problem that direct key distribution cannot solve.
./pve-add-ssh-key-to-lxcs.sh --key-file ~/.ssh/id_ed25519.pub # dry run./pve-add-ssh-key-to-lxcs.sh --key-file ~/.ssh/id_ed25519.pub --apply # write it./pve-add-ssh-key-to-lxcs.sh --key 'ssh-ed25519 AAAA... me@host' --user deploy --applyOK 203 metube (key already present)WOULD 204 plex-us-24 (/root/.ssh/authorized_keys)SKIP 216 huntarr (not running)==========================================Already present: 11Would add: 54Skipped: 3 huntarr(216) radarr-anime(229) nx-tempo(263)Failed: 0==========================================Dry run is the default — it reports and changes nothing until --apply. It is
idempotent in both modes, so a re-run only fills gaps.
- Presence is tested on the key body, not the whole line. The trailing comment field differs between machines; comparing whole lines would reinstall the same key forever.
- Every write is read back with a second
pct execbefore it counts as added. The exit status of the write alone is not taken as proof. - An existing
authorized_keysis copied toauthorized_keys.bak-<timestamp>beside itself first. sshd reads onlyauthorized_keys, so the backup is inert where it lands. - A file with no trailing newline gets one before the append, so the new key cannot be glued onto the end of the previous one.
- Stopped containers are skipped, never started, and are named in the summary.
--usertargets any account; containers lacking it are reported asNOUSERrather than having it created. Onlyrootis guaranteed to exist across this fleet.
Exit status is 0 clean, 1 bad usage, 2 not a Proxmox node, 3 at least one
container failed — a failure is recorded and the run continues rather than stopping half
way through the fleet.
Listing snapshots
Section titled “Listing snapshots”Snapshots are cheap to take and easy to forget, and a forgotten one with a saved RAM state can pin far more storage than its name suggests. Two scripts answer “what exists and how stale is it”, with the same columns, filters and summary — pick by where you are:
- In the node shell →
pve-list-snapshots.sh. No token, no network, no dependencies. - From a workstation →
pve-snapshots.py. Needs an API token.
Both are read-only. Neither creates, rolls back or deletes a snapshot.
From the node shell
Section titled “From the node shell”./pve-list-snapshots.sh # everything./pve-list-snapshots.sh --older-than 30 # stale enough to review./pve-list-snapshots.sh --type lxc --sort age./pve-list-snapshots.sh --format csv > snapshots.csvIt reads the guest configs under /etc/pve/nodes/*/ rather than shelling out to
pct/qm per guest. That is the cluster filesystem, so one node sees every node’s
guests — and it is a handful of file reads rather than a process per guest.
NODE TYPE VMID GUEST SNAPSHOT CREATED (UTC) AGE RAM PARENT DESCRIPTIONpve-dell qemu 217 prod-vm-us pre-traefik-v3 2026-05-19 23:28 91d 0h yes - before the provider plugin bumppve-dell qemu 217 prod-vm-us clean 2026-08-18 22:51 1h 2m - pre-traefik-v3 -A STATE column appears only when a snapshot has a leftover snapstate, which means an
interrupted create or delete that needs cleaning up; the summary warns about those
separately. PARENT is hidden when no snapshot in the output has one.
Options are --type, --node, --vmid, --name, --older-than, --sort, --format
and --quiet — the same meanings as the table under
Options below. Exit status is 0 fine, 1 bad usage, 2 not a Proxmox node.
To install it on the node alongside the other two:
install -m 0755 pve-list-snapshots.sh /usr/local/bin/Nothing to install beyond that: it uses only awk, sort and date. Dates are
converted to UTC in integer arithmetic rather than with strftime, because Debian’s
awk is mawk and mawk has no strftime — verified against date(1) across 413
timestamps including leap days and the 2100 non-leap century.
From a workstation
Section titled “From a workstation”pve-snapshots.py reaches the same data over the API.
cd utils/pveexport $(cat ../../ansible/.env | grep -v '^#' | xargs)
./pve-snapshots.py --insecure # everything./pve-snapshots.py --insecure --older-than 30 # stale enough to review./pve-snapshots.py --insecure --type lxc --sort age./pve-snapshots.py --insecure --format json | jq .NODE TYPE VMID GUEST SNAPSHOT CREATED (UTC) AGE RAM DESCRIPTIONpve-dell qemu 217 prod-vm-us pre-traefik-v3 2026-05-19 23:28 91d 0h yes before the provider plugin bumpRAM marks a snapshot that saved the guest’s memory (vmstate) — QEMU only, and the
usual reason a snapshot is unexpectedly large. The summary line, guest count and any
per-guest warnings go to stderr, so --format json and --format csv pipe cleanly.
Credentials
Section titled “Credentials”The same variables the Ansible dynamic inventory reads, so one export covers both:
| Variable | Default | Notes |
|---|---|---|
PROXMOX_URL | https://192.168.1.131:8006 | The node’s only address; pve-dell does not resolve in DNS |
PROXMOX_TOKEN_SECRET | — | Preferred |
PROXMOX_USER | root@pam | Realm stays on the user |
PROXMOX_TOKEN_ID | ansible | Sent as PVEAPIToken=<user>!<id>=<secret> |
PROXMOX_PASSWORD | — | Fallback; used only when no token secret is set |
PROXMOX_VERIFY_SSL | true | false is the same as --insecure |
--insecure is required against this cluster. The node presents a certificate that
is not valid for 192.168.1.131, so verification fails by default — deliberately, rather
than silently accepting any certificate. The script says exactly that when it happens:
Error: TLS verification failed for https://192.168.1.131:8006: [SSL: CERTIFICATE_VERIFY_FAILED]certificate verify failed: self-signed certificate. Proxmox ships a self-signedcertificate - re-run with --insecure (or PROXMOX_VERIFY_SSL=false) if that is expected.A read-only API token is enough. PVEAuditor on / covers both calls the script makes.
Options
Section titled “Options”Shared by both scripts, with the same meanings:
| Flag | Default | Meaning |
|---|---|---|
--node NAME | all | One node |
--type {qemu,lxc,all} | all | VMs, containers, or both |
--vmid ID | all | One VMID |
--name SUBSTR | all | Guests whose name contains SUBSTR, case-insensitive |
--older-than DAYS | off | Only snapshots older than DAYS |
--sort {node,age,guest} | node | age is oldest first |
--format {table,csv} | table | csv carries the full description and parent |
--quiet | off | No summary on stderr |
The API client adds --format json, --insecure, --timeout, --workers, and accepts
--node and --vmid more than once.
Exit status differs, because the two fail in different ways:
| Code | pve-list-snapshots.sh | pve-snapshots.py |
|---|---|---|
0 | clean run | clean run |
1 | bad usage | bad usage |
2 | not a Proxmox node | API unusable — unreachable, rejected, or bad TLS |
3 | — | some guest could not be read; the list is short |
3 matters in automation: the listing is real but incomplete, and the guest is named in
a warning on stderr.
Implementation notes
Section titled “Implementation notes”Proxmox details both scripts have to work around, and that are easy to trip over again:
/cluster/resources?type=lxcis not valid — it returns400 Parameter verification failed.type=vmreturns QEMU guests and containers, which are then split on thetypefield.- The API reports a snapshot named
currenton every guest. It is the live state rather than a snapshot, has nosnaptime, and cannot be rolled back to or deleted. It is dropped. The config files have no such entry, so the shell script never sees it. - A guest config repeats the guest’s name inside every
[snapshot]section, as it was when that snapshot was taken. The shell parser only trustsname:/hostname:from the live section above the first[, so a rename does not produce two names for one guest.nameserver:in an LXC config is not a name. - A snapshot can have no
snaptime. An interrupted one carriessnapstate: prepareinstead; it shows as-for created and age rather than as 1970.
pve-snapshots.py is stdlib only — no requirements.txt, nothing beyond python3.
pve-list-snapshots.sh needs only awk, sort and date.