gvm — VMware command line helper
A small command line tool for the VMware vCenters: list the virtual machines of all of them at once, take and remove snapshots, look at what the ESXi hosts are doing, and mail the vCenter event log.
gvm # interactive list of every machine, everywhere
gvm vm # the same thing, spelled out
gvm vm -l # the same as plain output
gvm vm -l -m web # only those whose name matches "web"
gvm -v v108 snap -l myvm # the snapshots of myvm on v108
gvm -v v108 snap -n myvm # take one
gvm -v v108 power -s myvm # ask its guest to shut down
gvm host # cpu, memory and machine counts per host
gvm log -l # the last hour of events
gvm config # what gvm made of ~/.gvmrc
Configuration
Everything gvm knows about the world is in ~/.gvmrc; there are no built-in
servers and no built-in credentials. On first run gvm writes an annotated
template there and says so — fill in the passwords and it works.
gvmrc.example is the same file with the comments spelled out.
default = v308
vcenter.v308.url = https://v308.fhi.mpg.de/
vcenter.v308.user = administrator@v308.fhi.mpg.de
vcenter.v308.password = ...
vcenter.v308.datacenter = PPB
vcenter.v308.insecure = true
-v <name> picks a server by its full name; an unknown name is an error rather
than a silent fallback to the first one in the list. Every setting has an
environment spelling that wins over the file — GVM_VCENTER_V308_PASSWORD,
GVM_MAILTO, GVM_DEFAULT and so on — which is how to run gvm from cron
without the password living in a file.
The file holds passwords, so gvm creates it mode 0600 and complains when it finds it readable by others.
Commands
| command | what it does |
|---|---|
| (nothing) | browse the machines interactively (see below) |
vm |
the same, spelled out |
vm -l [-m <re>] [--sort <order>] [--reverse] |
print them instead; all vCenters unless -v names one |
snap -l <vm> |
list a machine's snapshots |
snap -n <vm> |
take a snapshot, name printed |
snap -r <vm> -s <snap> |
remove one snapshot |
snap --revert <vm> -s <snap> |
put the machine back to that snapshot |
snap --removeall <vm> |
remove all of them |
power -o <vm> |
power on |
power -s <vm> |
ask the guest to shut down (needs VMware Tools) |
power -b <vm> |
ask the guest to reboot (needs VMware Tools) |
power --off <vm> |
power off at the hypervisor — hard |
power --reset <vm> |
reset at the hypervisor — hard |
host [-t] |
per-host cpu, memory, machine counts; -t also posts them |
host -c |
just the machine counts |
log -l [-m] [-t <min>] |
the event log, optionally by mail, default 60 minutes |
config |
the effective configuration, passwords not shown |
The interactive list
gvm on its own — or gvm vm, or gvm vm -i — asks every configured vCenter
at once and puts the machines of all of them into one full-screen list, sorted
by name. -v <name> narrows it to one server, and gvm -h still prints the
help, as does anything gvm does not recognise:
type narrow the list — the text is matched against the whole
line, so a name, an address, a host or "off" all work, and
the hit is picked out in the row
↑ ↓ PgUp PgDn move, Home/End for the ends
enter the machine's parameters: power, host, guest and tools,
cpu and memory in use, uptime, storage, guest filesystems,
network adapters, snapshots, uuid and moref
↑ ↓ in there scroll the sheet, esc/enter back to the list
The sheet is one line per thing worth knowing, values that belong together joined with a middle dot and no section headings — an ordinary machine fits a 24-row terminal whole, and the identity numbers and the annotation at the bottom are the only part anyone scrolls for. A value too long for the width is carried onto a continuation line under its own label rather than cut off at the edge, including one long word such as a datastore path, so a narrow terminal loses nothing. The machine's name, vCenter, datacenter and host are the title. ^o sort the table (see below) ^r ask the servers again esc clear the filter, or leave when there is none ^c leave
The columns are name, vCenter, power, address, host, vCPUs, CPU load, memory and memory in use, and the guest's operating system. The two load figures are percentages of what the machine is allowed to use and of what it has configured; a machine that is not running has no load rather than a load of zero and shows a dash. Past 75 % they turn yellow, past 90 % red.
A terminal too narrow for all of that gives columns up, least useful first: the guest's operating system, then the host, then the address, then the vCPU count — so what survives longest is what a glance is for. Each step only ever takes a column away, never brings one back, so dragging a window narrower does not rearrange the table. An eighty-column terminal keeps everything but the operating system and the host.
Sorting
^o puts a legend on the status line and the next key picks the order, so the
list stays on screen while it rearranges itself:
sort: n·name p·power c·cpu% m·mem% s·size u·cpus v·vc h·host a·ip r·reverse
Each order comes with its own direction, because that is what asking for it
means: by name is a to z, by processor load is the busiest first. r reverses
whatever is current. Anything that is not a choice — Esc, a stray letter — leaves
the table as it was.
The title says which order the table is in (↓ cpu load) and the heading of that
column is lit in the same colour, so the state is visible without asking. A
missing value is never a small one: a stopped machine has no load and a machine
whose guest is silent has no address, and both sort to the bottom whichever
direction the order runs. Machines that compare equal stay in name order, so
flipping the direction on a screen full of identical figures does not reshuffle
them.
The order survives ^r, and the selection follows the machine it was on. gvm vm -l --sort cpu% --reverse takes the same orders by letter or by name.
A vCenter that does not answer is named in the title in red — on v308, v108 v38 unreachable — for as long as the list is open, and the reason is on the
status line when it opens. Only the servers whose machines are actually there are
named as holding them.
Nothing acts on a machine from the table. Everything that changes one lives in
the machine's own sheet, which ⏎ opens — a row of a table of two hundred
machines is something the eye runs past, not something anyone has read. In the
sheet:
^a the action menu (see below)
^s take a snapshot: a name, then a confirmation
Pressing either in the table says so rather than doing nothing visible.
^s takes two steps.
First the name. The field starts empty and its hint says which random name Enter
alone would use — the same kind snap -n gives — so ^s enter y is the quick
path and typing something that will still mean something in three weeks is the
deliberate one. ← → Home End Backspace Delete edit it, Esc abandons the whole
thing, and 80 characters is where vSphere stops.
Then the question, naming the snapshot, the machine and the vCenter. It takes
nothing but y: Enter finishes a name, it never takes a snapshot. Afterwards the
name is on the status line, and a sheet that is open jumps to its snapshot
section so the new one is there to see.
The action menu
In a machine's sheet, ^a opens the menu for it. Above the choices it repeats
the few lines of the sheet the choice depends on — state, guest, hostname,
address — taken from the sheet itself, so the two cannot word the same fact
differently. On a terminal too short for both, those lines go and the choices
stay. Everything that changes a
machine lives there and nowhere else — the list is arrowed through and its filter
swallows every ordinary letter, so a hotkey that powered a machine off would sit
one fumbled control key away from an outage, and the sheet has to be opened first
anyway.
n take a snapshot o power on
r revert to a snapshot ... s shut down the guest
d remove a snapshot ... b reboot the guest
D remove ALL snapshots S power off (hard)
B reset (hard)
Lowercase asks the guest, uppercase acts at the hypervisor: the violent variant always needs the shift key. What cannot be done right now is greyed out with the reason next to it — "no VMware Tools", "already running" — rather than left out, and picking it anyway spells the reason out instead of running it.
Snapshots are drawn as the tree they are — which state descends from which is the whole point of a snapshot list — and the one the machine is running from says so:
snapshots base (01.09.2026 02:00)
├─ after-patch (03.09.2026 09:12)
│ └─ hotfix (03.09.2026 16:40)
└─ before-boot (05.09.2026 07:00) ← current
The same drawing appears in the picker below and in gvm snap -l; there is one
function that draws it, so the three cannot drift apart.
r and d open a list of the machine's snapshots. What is chosen there is
carried on by its vSphere reference, not by its name: two snapshots of one
machine may share a name, and a rollback point is not something to identify by a
string that is not unique.
Then comes a full page: the machine, the vCenter, the datacenter, the host, the
state, and what the operation costs — and it asks for YES, in capitals.
Not y, not yes, not Enter. A word that needs the shift key cannot be given by
a hand resting on Enter, and in the list the machine is whatever the cursor
happens to be on, so the page above the prompt is the part that matters.
Powering on is the single exception, and ends at one y: it destroys nothing.
Reverting asks vCenter not to start the machine again afterwards, so a machine whose snapshot was taken while it ran does not come back up with a rewound disk while you are still reading the message. Afterwards gvm reads the power state back and reports what it actually is rather than what it asked for.
After an operation the machine's row is read again, so the list shows what
happened rather than what the last sweep found. For a power on or off that means
waiting for vCenter's own view to catch up first: the task finishes a moment
before the property collector agrees, and a row re-read in between would still
show the old state. Opening the menu re-reads too, which is what picks up a guest
that has finished shutting down in its own time — ^r reloads everything.
Nothing else here writes: no key changes a setting, and there is no way to delete a machine.
Everything but the snapshot tree comes out of the one inventory sweep the list makes at the start; the snapshots of a machine are fetched when its sheet is opened.
It needs a terminal, and says so before it connects to anything — in a pipe or
under cron, use gvm vm -l.
The dangerous half
snap --revert, snap --removeall, power --off and power --reset stop or
rewind a running machine, and the first of them destroys data outright. Four
things hold for all of them:
- Graceful and hard are separate commands.
power -sasks the guest's operating system to shut down;power --offcuts the power at the hypervisor. gvm never turns the first into the second because VMware Tools did not answer — it refuses, and names the hard variant so you choose it deliberately. - The destructive options have no short letter.
--revert,--removeall,--offand--resethave to be spelled out; only the harmless ones (-o,-s,-b,-l,-n) are one keystroke. - Nothing impossible is sent. A machine that is already off is not shut down
again, and a graceful operation on a machine without Tools is refused before
anything reaches vCenter.
-ydoes not override this. - They ask first, and fail closed. Each prints what will happen, to which
machine, on which vCenter, and asks — defaulting to no.
-yanswers in advance, which is what cron needs. Without a terminal and without-ythe command refuses with exit status 1 rather than doing nothing quietly: a script that gets "nothing done" and exit 0 would believe the machine was stopped.
One power operation per command line; two is a mistake, not a sequence, and gvm says so instead of guessing.
The printed listing (vm -l) is the same table: the same columns, the same
cells, the same colours, fitted to the terminal when there is one and written out
in full into a pipe, where the colours are left off.
Colours
The palette is mwxcol, copied into
colors.go — that repo's mwxcol.go is the source of truth, so a change there
is a change here. Everything gvm paints goes through that one file: the P/PF
helpers in tools.go through the C* functions, the full-screen list through
the escape sequences at the bottom of it.
The frame follows the mapping mwxcol's own fzf theme uses, job for job: the
selected row on a darker surface with a violet pointer, the filter's hits in
pink, counts in green, questions in yellow, errors in red, headers and
the help line in dark.
Inside the table and the sheet every colour is a role, not a decoration:
white |
the machine's own name, and its guest |
violet |
which vCenter — a kind of thing |
green / dark / yellow / red |
powered on / off / suspended / anything else |
blue |
addresses, paths and dates |
orange |
sizes and counts |
pink |
names a person gave: snapshots |
grey |
present but seldom read: host, guest os, uuids |
A machine that is off is dark rather than red — being switched off is not a
fault. Two places lift that tone to grey: the selected row, where dark would
sit on the darker surface, and every value on the sheet, where the labels
beside it are dark themselves. Lifted, it is still visibly quieter than the
rest of the palette, so what was dimmed stays dimmed.
Colours are written as true colour (24 bit). In a pipe the C* functions leave
them out; the full-screen list needs a terminal anyway.
Updating
gvm updates itself from the releases of the Gitea instance named at the top of
selfupdate.go:
gvm --check-update # look, change nothing
gvm --update # fetch and replace the running binary
gvm --version
Once a day an ordinary run looks in the background and, when there is something
newer, prints one line about it. It costs nothing in the foreground, says
nothing when gvm is not on a terminal, and GVM_NO_UPDATE_CHECK=1 turns it off.
The downloaded binary is run once with --version before it replaces the
running one, so a truncated or wrong-platform download cannot install itself.
Building
./build.sh # all platforms into ./bin
PLATFORMS="linux/amd64" ./build.sh
VERSION=1.1.0 ./build.sh # set the version instead of bumping it
go test ./... # incl. tests against govmomi's simulator
Every run bumps the patch level in version.txt and injects it into the
binaries. The files in ./bin are named the way --update expects them in a
release: gvm-<goos>-<goarch> on a release tagged with the bare version number.
The automatic bump only ever touches the last number, so VERSION= is how a
major or minor step is made — no number of builds reaches 1.0.0 from 0.x. It is
checked to be MAJOR.MINOR.PATCH before anything is built: that number ends up
in the binary, in version.txt and on the release tag, and --update compares
versions number by number, so anything else would compare as older than
everything and quietly stop updates.
Tests
go test ./... runs without touching any real vCenter. The list, the filter,
the parameter sheet and the palette are checked on synthetic machines, and
everything that talks to a server — logging in, the inventory sweep, the
snapshot round trip, ^s with its name field and its confirmation, the whole
power and revert half with its confirmations declined and then given, the error
paths — runs
against govmomi's own simulator, started inside the test process (see
sim_test.go). The configured vCenters are production; no test goes near them,
and none of them reads ~/.gvmrc.
The checks that stand between a keystroke and a machine are tested on their own as well: the whole power matrix (state × VMware Tools × operation), that a refused operation sends nothing, that an unavailable menu entry does not run when it is picked anyway, that only the exact machine name passes the confirmation, and that removing one of two identically named snapshots removes the one that was picked.