Files
gvm/README.md
Michael WesemannandClaude Opus 5 c22a66c470 [mike@mwxm4]
The customisation picker opens on the site's own road.

A domain, a netmask and a gateway in ~/.gvmrc are written down so that machines
get them — that is what writing them down was for — so the line that gives them
to the guest is where the cursor starts, rather than the one that leaves the
guest as the template made it:

      leave the guest as the template made it
    ▸ this site — fhi-berlin.mpg.de, gateway 10.0.0.1
      linux-static
      windows-domain

Where no site is set up it starts at the top as before, on the answer that
cannot be wrong. Nothing is decided by where the cursor sits: the picker picks
on Enter, the address is asked for after it, and the confirmation still says in
full what will happen before anything is made.

What the picker offers is a function of its own now (customChoices), which is
what made it testable: the lines, what each of them means, and where to open
are one answer to one question and were three statements spread through a
method that also asks four others.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:57:33 +02:00

65 KiB
Raw Permalink Blame History

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 and the datastores are doing, report the snapshots nobody came back for, 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 vm -l --issues            # only the machines with something wrong
gvm vm -l --json              # the same listing as a document
gvm -v v108 snap -l myvm      # the snapshots of myvm on v108
gvm -v v108 snap -n myvm      # take one
gvm snap --old                # every snapshot older than 30 days, everywhere
gvm -v v108 power -s myvm     # ask its guest to shut down
gvm host                      # cpu, memory and machine counts per host
gvm ds                        # capacity, free space and over-commitment
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. The commands that sweep every server — gvm, gvm vm -l, gvm snap --old — also take a list, -v v308,v108, in the order given and with a repeat counted once; the commands that act on one machine refuse a list rather than taking the first of it. 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.

Besides the servers it holds the site's own answers, each written once: where mail goes, where telemetry is posted, how h logs in to a guest, and what a machine made from a template is told about the network (domain, dns, netmask, gateway, timezone — see Making a machine from a template). gvm config prints the lot, passwords as set or not set, and names what is missing for anything that is half configured.

The file holds passwords, so gvm creates it mode 0600 and complains when it finds it readable by others.

Passwords in the file

A password written into ~/.gvmrc in the clear is sealed on the next run of gvm and replaced in place by a gvmenc1:... word:

vcenter.v308.password   = gvmenc1:otspj7CLz1/8vEUFHpfrKH/zKiVLqlOvVwQ…

Nothing else about the file changes — the keys, the spacing, the order, the blank lines and the comments beside a setting are all left exactly as they were — and gvm says which password it sealed. Only the value is sealed, never the file, so ~/.gvmrc stays readable and editable by hand.

gvm config -p v308 asks for a password instead of taking it from the file and writes it sealed straight away. That is the way to set one: a password typed into the file stands there in the clear until the next run of gvm, and by then it has been through the editor's swap file and whatever backs the home directory up.

gvm config says whether each password is sealed, still in the clear, or sealed but no longer openable — it opens each one and throws it away, because "it is sealed" is worth nothing if it does not open.

What this is, and is not. The key is compiled into gvm and is the same in every copy of it, so whoever holds ~/.gvmrc and a gvm binary can open the value; prising the key out is an afternoon's work, not a cluster's. This is not a vault. What it buys is that the password no longer stands in the clear in a backup, in a home directory that syncs somewhere, in an editor's swap file, or on a screen someone else is looking at. The 0600 is what keeps other local users out. A value given in GVM_VCENTER_*_PASSWORD is taken as it stands, sealed or not.

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
vm -l --issues only the machines with something wrong with them
vm -l --json the same listing as a JSON document
snap -l <vm> list a machine's snapshots
snap -n <vm> take a snapshot, name printed
snap --old [-d <days>] [-m] every snapshot older than that, on every vCenter, optionally by mail
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
size --vm <vm> what it has: vCPUs, memory, and what may be changed while it runs
size --vm <vm> -c 8 -m 16 give it 8 vCPUs and 16 GB — -m 512m for megabytes
new --from <template> --name <vm> make a new machine from a template
new ... --host <h> --datastore <ds> --on where it lands, and whether it starts
new ... --spec <spec> --ip <addr> customise the guest: hostname and address
new ... --ip auto have the site's dns tool hand an address out
new --specs the customisation specifications this vCenter holds
config also shows what a new guest would be told about the network
host [-t] per-host cpu, memory, machine counts; -t also posts them
host -c just the machine counts
ds [-t] per-datastore capacity, free space, over-commitment; -t also posts them
log -l [-m] [-t <min>] the event log, optionally by mail, default 60 minutes
config the effective configuration, passwords not shown
config -p <vcenter> ask for a password and store it sealed
completion zsh|bash print the shell completion script

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: what is wrong with it and what is
                being done to it, 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
e h y w         in the sheet: recent events, ssh to the guest, copy what
                ssh would connect to, open it in the vSphere client

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) ^w only the machines with something wrong with them (see below) ^e the whole estate on one screen (see below) ^l live: the list re-reads itself (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, snapshots, 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.

The snapshot column is how many the machine is dragging along, aged by colour: past a week the count turns yellow, past a month red — a month being also what snap --old reports on, so a red count means "this machine is in that report". A machine with none shows a dash. The count is what the column says and the age is only how it is said, which is why the sheet spells the date out: a colour cannot be read in a pipe.

Two columns are not always there, because they hold an exception rather than a property, and thirteen characters of blank down two hundred rows is thirteen characters spent on nothing:

  • TASK appears while vCenter is doing something to any machine in the list — a clone, a migration, a consolidation, with its progress — and is gone again when nothing is going on. A column that turns up because somebody started a clone is not the layout shifting about: it is the news.
  • WHY takes the guest operating system's place in the issues list (^w, --issues), where every row has a reason to be there — and the four figures go with it, so that the reason has the width. It is the last column that listing gives up rather than the first: in a list whose every row is there because of it, dropping the reason first leaves a list of machines with no reason showing on any of them.

A terminal too narrow for all of that gives columns up, least useful first: the guest's operating system, then the host, then the snapshot count, 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, the host and the snapshot count: the name column's minimum is one notch narrower than it reads in order to buy the address its place there, and the count is the one column here that has somewhere else to be said — ^w, --issues and snap --old all name it and date it.

The estate

^e answers the question the machine list cannot: where is there still room. Every host of every server that answered, grouped under its cluster, with what it carries set against what it has:

Estate   v308, v309   14 hosts · 212 machines

  CLUSTER / HOST        ON/VM      vCPU      x         MEM ALLOC      x  CPU LOAD      %  MEM USED      %
  v308 · prod           58/61    196/96   2.0x     1.1TB/768.0GB   1.5x
▸ esx01                 21/22     72/32   2.2x    384.0GB/256.0GB  1.5x  ████······   41  ███████···   72
  esx02                 19/20     68/32   2.1x    360.0GB/256.0GB  1.4x  ███·······   32  ██████····   64
  esx03                 18/19     56/32   1.7x    376.0GB/256.0GB  1.4x  █████·····   53  ████████··   81
  v308 · standalone      4/4       8/16          32.0GB/128.0GB         ██········   12  ██········   21

Two kinds of number, and the difference between them is the whole point — which is also what decides what a narrow terminal gives up. Three layouts, each the widest that fits whole: everything with the load drawn as bars; the same figures with the bars down to their percentages; and, narrower still, only the two ratios and the two percentages. Both kinds of number survive all three, because one of them alone says nothing.

What is allocated — every vCPU and every megabyte the machines on a host have been promised, added up. It routinely exceeds the host and is meant to: the ratio is the figure worth having, because 1.5x of memory is a decision somebody made and 8.0x is one somebody forgot. It is left blank where a host has room to spare, since a column of "0.4x" down a screen of healthy hosts is noise where the point is to spot the one that is over.

What is in use — what the host itself reports it is doing, as a bar and a percentage. A host at 2.2x allocation and 41 % load is fine; the same host at 90 % is not, and no allocation figure can tell those two apart. A load that is not known draws nothing rather than an empty trough: a host at one per cent fills none of the bar either, and "almost idle" must not look like "I cannot see this host".

A host vCenter has lost touch with keeps its hardware figures — they do not depend on reaching it — but reports no load at all rather than the nought its cached statistics would give: an empty bar is what an idle host looks like.

Only running machines are charged to a host. A machine that is switched off has been promised nothing it is using, and counting it would make a host of parked machines look full when the whole point of parking them there was that it is not — they are still in the ON/VM count, which is where that belongs.

⏎ on a host is the other half: it goes back to the machine list with the filter set to that host, so the answer to "what is on this one" is the table everybody already knows how to read, and Esc undoes it. The count it reports is the host's own, not the filtered list's: the filter is a text match over the whole row and carries no server, so it can also catch another vCenter's host of the same name or a machine named after a host, and a count taken from it would then contradict the screen it came from. ^r reads the screen again — live mode deliberately does not tick here, because this screen reads the hosts itself and a timer doing that every ten seconds would be paying for a screen somebody is reading rather than watching.

The allocations are added up from the rows the list already holds, so no machine is read twice for this, and they are matched to hosts by reference rather than by name — the same lesson gvm host carries a comment about, where a host added by address and renamed later reported zero machines while running dozens.

Live

^l and the list stops being a snapshot. It re-reads itself every ten seconds — every two while vCenter is doing anything at all, so a clone's progress is something one watches finish rather than a figure one waits for — and the title says so, because a screen that moves on its own with nothing to explain it reads as a fault.

It refreshes over the connections that are already open. ^r logs in again, which is how a session that has died is recovered; doing that every ten seconds would be three logins a minute for nothing.

^l is not the only way in: making a machine from a template turns it on, since a clone that has just been started is exactly the screen this is for, and says so in the same line that reports the machine.

Two things come with it.

The line under the table says what just changed. A table shows what is; this is the only thing on the screen that says what became:

db01 off · web01 +1 snapshot · app07 clone · esx03: disks need consolidating

Power states, snapshots appearing and going, tasks starting and finishing, machines arriving and leaving, and vCenter starting to complain. Four of them and a count of the rest, because a line that has to be read carefully is a line nobody reads. It is the reason to leave the thing open.

Where two vCenters hold a machine of the same name the server goes in front of it — "v309 web01 off" — and only there: a name is not what makes a machine that machine, and the width is worth spending on the rare case rather than on every line. A quiet refresh clears that line and nothing else: a message somebody produced by pressing a key is theirs to keep.

The CPU~ column is where each machine has just been. Six sweeps of its processor load, one character each:

NAME     PWR  CPU  CPU%  CPU~    MEM
app07    on   4    18    ▃▅█▆▄▂  8.0GB
web01    on   4    82    ▄▂▁▃▅█  8.0GB

The scale is fixed at 0 to 100 and never fitted to the samples: a line that scales itself would make a machine idling between 1 and 2 per cent look exactly like one swinging between 40 and 80. The column is there only once there is something in it — a machine has a history after its second sweep — and it is the first thing after the guest's operating system that a narrow terminal gives up: everything else in the table is a fact about a machine, and this is a shape.

A machine that is not running has no load rather than a load of zero, so nothing is sampled from it and it draws nothing — and what it had drawn before goes with it, rather than leaving a busy shape beside a CPU% of "-".

The refresh holds still for every screen that asks something: a menu decides what it offers from the state it was drawn with, the picker holds a list being chosen from, and a half-typed name or a confirmation is an answer in progress. Being ten seconds out of date is better than any of those moving under a hand. A machine's sheet is refreshed, at the line it was being read at — watching one machine's memory is a reason to have it open.

Nothing here acts on a machine. A refresh that could start or stop something would be a timer with the power to do it, and that is the one thing a screen left open unattended must not have.

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·pwr c·cpu% m·mem% z·snaps o·old w·why 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.

Thirteen choices are ninety-three columns, so a terminal narrower than that gets them on two lines instead of one that runs off the right-hand edge, hiding the very choices the legend exists to offer. They break where the meaning breaks — what the machine is doing and what it wants doing to it, then what it is made of and where it lives — and the second line takes the help line's row, which describes keys that do nothing while a menu is waiting for one. Decided at render time, so a window dragged wider gets the one line back:

sort: n·name p·pwr c·cpu% m·mem% z·snaps o·old w·why
      s·size u·cpus v·vc h·host a·ip r·reverse

Three of them are about the two columns that are new:

  • z is by how many snapshots the machine is carrying, most first. Nought is a figure here and not a missing one — nothing to clean up is a fact about the machine — so a machine with none sorts where nought belongs: at the bottom going down, at the top coming back up. The letter carries no mnemonic because every letter that does was taken; --sort snaps spells it out.
  • o is by the age of the machine's oldest snapshot, oldest first, which is the order the housekeeping is done in — a different question from #, and the more useful one: one snapshot from March wants attention before six from this morning. A machine with no snapshots has no age and sorts to the bottom either way round, the same as a stopped machine's load does.
  • w is by what is wrong with the machine: broken above wants-a-look above nothing to report, and within each the machine with the most to answer for first. Sorting the reasons as text would put "alarm" above "disks need consolidating" and mean nothing at all. Run the other way up it is the machines that are fine, by name — a listing worth having too.

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.

The machines that want looking at

A list of two hundred machines is read by running the eye down it, which is exactly the wrong way to find the three that are broken. ^w narrows it to those, and each one carries the reason in place of its guest operating system:

NAME     VC    PWR  SNAP  IP           HOST   WHY
old01    v108  on   1     10.0.0.31    esx02  /var 97 % full · no VMware Tools
db01     v308  on   3     10.0.0.12    esx01  disks need consolidating · snapshot base is 63 days old
win7     v38   on   -     -            esx07  vCenter says yellow

The four figures — vCPUs, processor load, memory and memory in use — are not there. A machine is in this list because something is wrong with it, and how hard its processors happen to be working at this second says nothing about any of the reasons: they would be four columns of arithmetic between the machine's name and the answer to the question that was asked. They are one keystroke away in the ordinary list, and on the machine's own sheet.

Nothing new is asked of the servers: this is a filter over the sweep that is already on screen, so it costs a keystroke and no waiting. ^w again gives the whole list back, the typed filter still applies inside it — ^w web is the broken web servers — and the title says issues only for as long as it is on, because a filtered list that does not say so is a lie told by omission. ^o w puts the worst of them at the top.

What counts as an issue is deliberately narrow, because a list that cries wolf is one nobody opens:

reason
disconnected, orphaned, inaccessible vCenter cannot see the machine properly
waiting for an answer in vCenter a question nobody has answered; the machine is stopped until somebody does
disks need consolidating deltas left behind by a snapshot removal that did not finish, growing quietly
alarm: name what vCenter itself is complaining about, by the name somebody gave the alarm
vCenter says red / yellow the rolled-up status, when no alarm came with it to explain it
no VMware Tools and only while the machine is running
/var 97 % full a guest filesystem past 90 %, named with the figure
snapshot name is 63 days old past a month, which is where the table's red begins

Broken is red and wants-a-look is yellow, worst first — which matters because the column is truncated from the right. An alarm somebody has acknowledged is one a person has dealt with already and is not reported; a machine that is switched off is not a fault; and the things that are only true of a running machine are not held against a stopped one.

The same list prints: gvm vm -l --issues, which is the morning's glance and the one worth a cron job.

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
e               recent events
h               ssh to the guest, as root
y               copy that name or address to the clipboard
w               open in the vSphere client

Pressing ^a or ^s 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.

Snapshots are taken without memory and without quiescing, and both are deliberate. Memory would keep the running machine's RAM too, so that a rollback came back mid-flight — at the price of writing all of it to the datastore every time, and of a rollback that restores a process tree along with the disks. What these are for is the moment before a patch, where coming back to a machine that boots is the point. Leaving quiescing off makes the disk state crash-consistent — what a machine finds after the plug is pulled, which a journalling filesystem handles and a database may not — and means a snapshot neither depends on VMware Tools running nor stops when they are not.

Making a machine from a template

p in a template's action menu, gvm new on the command line. It is the one thing gvm does that brings a machine into being rather than acting on one that already exists, and that makes its hard question where rather than whether.

A template is not a machine that happens to be switched off. vSphere takes its resource pool away when it is marked as one, so a copy of it has nowhere to run until somebody says where — and that is the one thing that cannot be inherited from the source. Everything else is: the folder it sits in, the datastore it lives on, the hardware it was built with.

So the placement is worked out first and the question says it in full:

make web05 from ubuntu-24.04-template on v308?   y = yes, anything else = no
it will run in prod-cluster, powered off

The resource pool is the one the template's own host belongs to — the cluster's, where it is in a cluster, which leaves the choice of host to DRS the way every other deployment there does. --host overrides that and pins it; --datastore puts it somewhere other than the template's own.

It does not wait. A clone copies every disk the template has, which is minutes to the half hour, and a list frozen for that long is a list nobody would start one from. The task hangs off the template in vCenter, so the row it was started from shows how far along it is in its TASK column. gvm new on the command line does wait: a script that gets its prompt back wants the machine to be there.

And it goes to that column. The machine's sheet — which is where the menu was opened and a page of facts about the template — is put away, the list comes back with the cursor still on the row the clone hangs off, and live mode turns itself on if it was off, so the percentage moves on its own and the line under the table announces the new machine when it exists. ^l ends it again. Naming the key that would make an invisible figure move is three steps of housekeeping after the one decision that mattered.

The rest of a template's menu is greyed out with "a template" next to it, because vSphere will not start one, snapshot one or reconfigure one. They are greyed rather than left out: a menu that changes shape between rows is one nobody learns.

Telling the guest what it is

A copy of a template comes up as the template: same hostname, same address. Those two are the facts that are about this machine, and everything else — the domain, the resolvers, the netmask, the gateway, the timezone — is the site's answer, the same for every machine. So the two are typed and the rest is kept somewhere it is written once.

Two places it can be kept, and gvm takes it from either.

In ~/.gvmrc, which is the short road for a site that keeps no specifications in its vCenter:

domain    = fhi-berlin.mpg.de
dns       = 141.14.128.1, 141.14.128.2
netmask   = 255.255.255.0
gateway   = 10.0.0.1
timezone  = Europe/Berlin

gvm new --from ubuntu-tpl --name web05 --ip 10.0.0.55

gvm writes the customisation itself from those: hostname from the machine's name (--hostname where they differ), address from --ip, and the five above. It is Linux only — a Windows guest is a Sysprep, which is a licence key, an administrator password and a domain to join, and none of that belongs in a configuration file next to the SMTP relay. gvm config shows what it would write, and names the settings that are still missing.

In the vCenter, as a customisation specification made in the vSphere client under Policies and Profiles:

gvm new --from ubuntu-tpl --name web05 --spec linux-static --ip 10.0.0.55

--spec names one, and gvm overrides exactly two things in it — the hostname and the address — and leaves the rest alone. This is the road for Windows, and the one to prefer where the specifications already exist: the policy is then where the web client and every other tool can see it too. A tool that knew better than the vCenter about any of the rest is how one ends up with a network policy nobody remembers agreeing to.

In the list it is a step of its own: after the name, a picker of whichever roads are open —

customise web05 how   v308

  leave the guest as the template made it
▸ this site — fhi-berlin.mpg.de, gateway 10.0.0.1
  linux-static
  windows-domain

The cursor starts on gvm's own road wherever the site is set up: a domain, a netmask and a gateway written into ~/.gvmrc are written down so that machines get them, and the answer wanted nearly every time should not be the one that has to be arrowed to. Where the site is not set up it starts at the top instead, on the answer that is never wrong. Nothing is decided by where it sits — the picker picks on Enter, and the confirmation still says in full what will happen.

Then one line for the address, which says in words what leaving it empty would do:

address for web05, empty for DHCP:
address for web05, empty to leave it to linux-static:

Where that picker does not appear, the confirmation says why rather than leaving a step to look broken — neither road is open, because the vCenter holds no specifications (or will not say) and ~/.gvmrc has not been told the site:

make web05 from ubuntu-tpl on v308?   y = yes, anything else = no
no specifications on v308, and ~/.gvmrc has no domain, netmask, gateway · in prod-cluster, powered off

gvm new --specs lists what a server holds, and prints the server's own words when it will not say.

An address it fetches rather than one you look up

gvm new --from ubuntu-tpl --name web05 --ip auto

Where the site has a tool that hands addresses out, --ip auto asks it instead of making you look one up first. In the list the same word does it, and it is already standing in the line — a site with a tool hands addresses out from it, so that is the answer Enter gives:

address for web05, "auto" for one from dns, empty for DHCP: auto

It is offered rather than made the meaning of the empty line, so every answer keeps the meaning it had: erased back to nothing it is DHCP again, or whatever the chosen specification says. And Enter commits nothing — an address fetched here is handed straight back when the deployment is not confirmed.

It shells out to dns, the Infoblox helper — found on the path by that name, or named as dnstool in ~/.gvmrc where it lives somewhere else. Everything it knows (the appliance, the credentials, the default domain) stays its own; a second copy of any of that inside gvm would be a second thing to keep right. gvm config says whether one was found, because "why was I not offered that" is the question that follows an option quietly not being there.

The name has to be free. It is asked before anything is added, and a name the appliance already knows is refused rather than added to — what dns -a would do with one is either refuse in worse words, or hang a second address on somebody else's host record:

web05 already exists (web05.fhi.mpg.de at 141.14.140.182) — give the machine
another name, or free that one with 'dns -d web05'

Which is the same question gvm asks the vCenter about the machine's name, one answer short of the same answer.

And the address is said where it stays. In the confirmation, and again in the line that is left on the screen afterwards — this is a number somebody writes down:

web05 is being made at 141.14.140.182 — live on, ubuntu-tpl shows how far
along it is

Three more things about it are deliberate:

  • It is asked with -j, and nothing older than v2.5.0 is asked at all. From that version the helper answers in an envelope: ok, the reason in an error field of its own, the record — and an exit status that agrees with it.

    {"ok":true, "action":"showhost","name":"v308.fhi.mpg.de","record":{...}}
    {"ok":false,"action":"showhost","error":"host '...' not found"}
    

    What came before said the same things in a sentence and exited 0 while refusing, which is what an earlier version of this got wrong by believing exit statuses. Rather than keep reading both, gvm asks the helper its version once and uses it only from 2.5.0 up. An older one is not silently ignored — "there is none" and "the one you have is too old" send somebody to two different places, so it says which:

    /usr/local/bin/dns is 2.4.4, and gvm wants 2.5.0 or newer — 'dns --update' fetches it
    

    gvm config says the same thing when nothing is being deployed.

    An appliance that cannot be reached is an error and not a free name: concluding "nobody has this name" from a server that is down is the last thing to do before asking it for an address.

  • The record is read back, not taken from the sentence the helper prints. OK: host 'web05.fhi.mpg.de' added with IP '141.14.140.182' is written for a person and gets reworded between versions; the record is the appliance's own answer to the same question, and where the two disagree the record is what the machine will have.

  • It is fetched before the confirmation, so that the question says the address rather than a promise of one:

    make web05 from ubuntu-tpl on v308?   y = yes, anything else = no
    in prod, powered off · named and addressed for fhi.mpg.de at 141.14.140.182 (web05.fhi.mpg.de, from dns)
    
  • And given back where it is not used. A deployment abandoned at that question, or one the vCenter then refuses, releases the address rather than leaving a record behind for a machine that was never made.

The name asked for is the machine's own — the short one, since the helper puts the record in the site's default domain itself — which is also the hostname the guest is given, so the two cannot disagree.

Three things it refuses rather than guesses at, on either road:

  • An address with no netmask to go with it. Where the specification's adapter takes its address from DHCP it carries no mask, and a machine given an address without one half works. Where to get a mask from is a question for the vCenter.
  • A hand-written Windows answer file, and a cloud-init specification. Both carry the hostname inside a format gvm does not own, and reaching into either to change one line would be guessing.
  • Nothing at all, silently. If the template reports no VMware Tools — which is what carries a customisation out inside the guest — it says so before the clone rather than leaving it to be discovered twenty minutes later on a machine that came up under the template's name. It is a guess and is said as one: the machine is made either way.

One more thing it says rather than refuses: a gateway that is not on the machine's own network. A routed setup can put one anywhere, so it is not an error — but almost every time it is a typo, and a machine that cannot reach its gateway is one somebody drives to the console for.

Deploying without either road still works, and the guest comes up as the template made it.

The sheet's four letters

The four letters are the things that change nothing, on the machine or on the vCenter: they read its history, copy its address, open it somewhere else. They are letters of the sheet rather than entries in the menu because nothing they do needs thinking about first, and — unlike the table, whose filter swallows every ordinary letter — the sheet has nothing else to do with them:

  • e puts the machine's own recent events at the foot of its sheet and scrolls down to them — why is this thing off, who rebooted it, what happened at four this morning. gvm log is the whole vCenter over the last hour, which is the right shape for a mail and the wrong one for that question. They are fetched when they are asked for: opening a machine stays one call.

  • h logs in to the guest as root — ssh root@<name> — by its own hostname where it reports one and by its address otherwise. The terminal goes back to what it was for as long as that lasts. ssh = ssh -l someone %h in ~/.gvmrc replaces that command whole, root and all; the target is always one argument and never goes through a shell, because it is a name the guest chose for itself. ^C while it hangs on a machine that is not answering kills the login and no more than that: gvm catches the signal for as long as the child has the screen — the terminal is in its ordinary mode there, where the keystroke goes to every process in the foreground group — and comes back to the sheet saying the login was interrupted.

  • y copies exactly what h would connect to — the hostname where the guest reports one, the address otherwise — and the status line names which of the two it was and which clipboard it went into, because a clipboard is invisible and "copied" on its own is something one has to go and check.

    Two routes, because neither alone is enough. pbcopy (or wl-copy, xclip, xsel) is the one that always works where there is one, and the terminal's own OSC 52 escape sequence is the only one that reaches the right machine from the far end of an ssh login — where a local pbcopy would copy into the clipboard of a machine nobody is sitting at. So the sequence is always sent and the command is used as well where there is one; a login is recognised by SSH_CONNECTION, and there the sequence is the whole story. It is also the route a terminal is free to ignore — iTerm2 keeps it behind Applications in terminal may access clipboard, tmux behind set-clipboard — which is why the line says when it was the only one used.

  • w opens the machine's page in the vSphere client. The link needs the vCenter's instance UUID, which is the serverGuid that client puts in its URLs and the one thing gvm cannot work out from the configuration; where there is no browser to hand off to, the URL is said and copied instead.

h and y need somewhere to connect to: on a machine whose guest is not reporting an address they say so on the status line rather than doing nothing.

The action menu

In a machine's sheet, ^a opens the menu for it. It is everything that changes a machine, and it 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.

Above the choices the menu 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 one at a time, least useful first: the state stays longest because every choice depends on it, then the address and the hostname, which say which machine this is about.

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)
c  change the vCPU count ...
m  change the memory ...

Of the power pairs, lowercase asks the guest and 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", "no snapshots", "needs it off" — rather than left out, and picking it anyway spells the reason out instead of running it.

The three groups are what the machine has been, what it is, and what it is doing.

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.

The sweep the list makes at the start brings back everything the table and the sheet show, the snapshot trees and the running tasks included — they are properties of a machine, and reading them for every machine is one call, not one per row. Two things are asked for afterwards, for one machine at a time: its snapshots when its sheet is opened, because everything that acts on a snapshot addresses it by reference and a reference out of a sweep that ran minutes ago may name one somebody has since removed; and its events, when e asks for them.

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 -s asks the guest's operating system to shut down; power --off cuts 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, --off and --reset have 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. -y does 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. -y answers in advance, which is what cron needs. Without a terminal and without -y the 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.

What a machine has

c and m in the menu, gvm size on the command line: the vCPU count and the memory. Nothing is lost by either, so they get the plain y/n question rather than the page that wants YES typed out — a number set wrongly is set back.

vSphere refuses most of what one might ask for on a running machine, and gvm refuses it first, with the reason, rather than sending it to be bounced:

  • A running machine can only grow, and only where it was built to. CPU and memory hot-add are per-machine settings, turned on when the machine was made. Without them the entry says "needs it off". Memory can never shrink while a machine runs — there is no hot-remove for it in vSphere at all — and vCPUs only where hot-remove is on as well.
  • The socket topology is the machine's own. A vCPU count has to be a multiple of the cores per socket, and gvm does not quietly change the sockets to make a number fit: software is licensed per socket, and a tool that turns 2 sockets into 4 to accept an odd number would be writing somebody an invoice. It names the two counts that do fit instead.
  • Memory is whole multiples of 4 MB. It is typed in gigabytes, because that is what the sheet shows and what anybody says out loud; 512m means megabytes, and 1.5g is 1536 MB.
  • Read again immediately before it is sent. The menu's grey-out says only that a change of this kind is possible at all; between drawing it and answering the question somebody else may have started the machine.
  • A machine whose configuration cannot be read is left alone. Not knowing what it has means there is nothing honest to put on the left of the arrow, so both entries grey out and say so rather than offering a change from a figure gvm does not have.
  • gvm size -c 8 -m 1026m does neither. Both numbers are read and checked before either is sent: a command line that sets the vCPUs and then refuses the memory has half happened, which is the one outcome nobody asked for.

gvm size --vm <machine> on its own changes nothing and says what there is:

machine        web01  (running)
vCPU           4, in 1 per socket
memory         8.0GB
while it runs  add vCPUs, add memory

That last line is the one worth having before planning the work — it is the difference between a change now and a maintenance window. Where a machine has none of the three settings it says so plainly: "nothing — it has to be powered off to be changed".

Hot-added resources are not necessarily in use the moment vCenter reports the task done: the message says as much, because an operating system does not always notice on its own that it has been given another four processors.

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.

The reports

Two things nothing in vCenter does for you, in the shape a cron job wants: every server at once, one line per thing, and -m to put it in the post.

Old snapshots

gvm snap --old                # older than 30 days, on every vCenter
gvm snap --old -d 7           # or than a week
gvm snap --old -m             # and mail it

Somebody takes a snapshot before an upgrade, the upgrade goes well, and the snapshot stays. Six weeks later its delta disk is bigger than the machine and the datastore is the thing that pages you.

3 snapshots older than 30 days, on 2 machines:

MACHINE  VC    SNAPSHOT      AGE  TAKEN             SIZE
old01    v108  base         208d  12.02.2026 03:00   8.9GB
db01     v308  before-patch  63d  06.07.2026 22:14  41.2GB
db01     v308  hotfix        61d  08.07.2026 09:40   2.1GB

52.2GB in 3 snapshots, on 2 machines

Oldest first, which is the order the work is done in and puts the worst line where a mail gets read. The age takes the table's colours — yellow past a week, red past a month — and the mail carries the same table with the colours left off, out of the same cells, so the two cannot come to different conclusions.

The size is what removing that snapshot would give back: its own state file and the last link of each of its disk chains. The links in front of those belong to its ancestors, and the delta the machine is writing to right now belongs to no snapshot at all — so summing whole chains, which is the obvious thing to do, reports the same delta once per descendant. A snapshot whose file layout could not be read shows a dash rather than 0 B: nought bytes and "not known" are different answers, and the second must not invite somebody to remove the wrong snapshot.

The file layout is only asked for for the machines that actually have snapshots, and for all of them at once per server — it lists every file of every machine, which is far too much to carry through the ordinary sweep.

Datastores

gvm ds                        # one line per datastore
gvm ds -t                     # and post the numbers

The gap next to gvm host: a cluster is watched by its processor load and its memory, and then it falls over because a datastore filled up.

DATASTORE   TYPE  CAPACITY   FREE  USED%  PROVISIONED  OVER%    VM  STATUS  STATE
ppb-ssd-1   VMFS    4.0TB  412GB     90        5.1TB    128    41  green   ok
ppb-sata-2  VMFS    8.0TB  3.2TB     60        6.0TB     75    88  green   ok
ppb-old     VMFS        -      -      -            -      -     3  red     inaccessible

3 datastores  3.6TB of 12.0TB free (70 % used)

Provisioned is what has been promised out of the datastore: what is in use plus what thin disks are still entitled to grow into. Past the capacity that is a promise the datastore cannot keep if every machine takes what it was offered, which is why it has a column of its own rather than being folded into "used" — ordinary practice, so a hundred per cent is a word of warning in yellow and half again as much is an alarm in red.

Every figure comes out of the datastore's summary, and vSphere only vouches for those while the datastore is accessible: an unreachable one reports dashes rather than zeroes, because a datastore that says 0 B free looks like an emergency and one nobody can reach is a different one.

The listing as a document

gvm vm -l --json is the same sweep, for something other than a person:

{
  "generated": "2026-09-08T11:42:07+02:00",
  "answered": ["v308", "v108"],
  "failed": ["v38: login failed: ..."],
  "count": 212,
  "machines": [
    {
      "name": "db01",
      "vcenter": "v308",
      "power": "poweredOn",
      "cpu_percent": 12.4,
      "memory_percent": 64.1,
      "snapshots": [{"name": "before-patch", "days": 63, "current": true, ...}],
      "oldest_snapshot_days": 63,
      "task": {"what": "consolid", "progress": 40, ...},
      "issues": ["disks need consolidating"],
      ...
    }
  ]
}

Two things about the shape, because a document is a promise:

It is one object and not an array of machines, because a listing that quietly leaves out a vCenter which did not answer is worse than no listing at all — a script handed a bare array cannot tell an empty cluster from an unreachable one. The servers that answered and the ones that did not are in the document, and a failure is not also printed as prose: a line of English in the middle of the JSON would break whatever is reading it.

And a figure that is not known is null, never 0. A stopped machine has no processor load and a machine whose guest is silent has no address; a spreadsheet that averages a column of zeroes reports a fleet that is idle.

--json and --issues mean -l without having to be told twice, and both take -m, --sort and --reverse like any other listing.

Shell completion

eval "$(gvm completion zsh)"                       # ~/.zshrc
gvm completion bash > /etc/bash_completion.d/gvm

Machine names are long and there are hundreds of them, which is what makes the non-interactive half hard to type — gvm -v v308 snap -l dbse<tab>. The completion offers them after the options that take a machine, the server names after -v, and every subcommand and option otherwise.

That last half is not written down anywhere: flaggy generates it out of the parser itself, so no list can fall behind the options that exist. gvm answers the completion subcommand one step before flaggy would, keeps what flaggy wrote, and adds the names on top — a wrapper that falls back to flaggy's own function by the name it installed it under, read off the script rather than written down a second time. fish, powershell and nushell are left to flaggy entirely; the names are wired up for zsh and bash.

The names cannot come from the vCenters: a completion runs on every Tab and has to answer in milliseconds, and three logins take seconds. So they come out of what gvm last saw — every sweep of the machine list leaves them in the cache directory, per server and with the time on them, and --complete-vms reads that file and nothing else. A sweep of one server leaves the others' names where they were, so completion keeps working for a vCenter that is down.

Neither the subcommand nor those two options read ~/.gvmrc: a Tab key must not rewrite a file, and reading the configuration seals any password standing in it in the clear.

Nothing else in gvm reads that cache. Every command resolves the name it was given against the server itself, because "what gvm saw last time somebody looked" is the right currency for a Tab key and no currency at all for anything that acts on a machine. gvm config says how old it is, so that a completion offering a machine deleted last month can be explained.

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.

Every line at the foot of the screen that wants an answer is that one yellow, whatever kind of question it is: the sort legend, both its lines; a yes/no question and its hint; the label in front of a snapshot name or the YES of a confirmation. They are different kinds of question and one state — gvm is waiting for a key — and that state is worth learning once, in one place and one tone, rather than being worked out per screen. What is typed in answer stays white: it is the operator's, not part of the question.

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, an event's history
yellow / red a load past 75 / 90 %, a snapshot past a week / a month, something that wants a look / something broken
yellow what is being done to a machine right now: the task column

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.

So are the judgements the reports are made of, which are the ones that would go wrong quietly:

  • every reason a machine can be in the issues list, one by one, and the ones that must not put it there — a stopped machine, an acknowledged alarm, a filesystem with room, this morning's snapshot, a status reported twice
  • that a snapshot's size counts each delta once and not once per descendant, which is what summing whole disk chains does
  • that an unknown figure is a dash on screen and null in the document, for the load, the address, the uptime, a datastore that cannot be reached and a snapshot whose file layout could not be read
  • that the ssh target is one argument and never shell code — it is a name the guest chose for itself
  • that y names what it copied and that an ssh login uses no local clipboard tool — the tests say they are a login, which also keeps them off the clipboard of whoever is running them
  • that a step which does not happen says why — a vCenter with no customisation specifications, and one that will not let them be read, both used to skip the question in silence — and that the reason and the placement both fit on the line they share
  • that a name already in the appliance is refused before anything is added to it, that an appliance which cannot be reached is not mistaken for a free name, and that a helper older than 2.5.0 is refused by name and version rather than talked to — against a stand-in, since a suite that called the real one would be editing the institute's network every time it ran
  • that the version is read off the tool's own, and not off the toolbox version in the same line — which is the wrong answer that check exists to prevent
  • that an address fetched from the site's tool is read back from the record rather than from its printed sentence, that the tool's own words come back when it refuses, that a fetched address is given back when the deployment does not happen, and that nothing is ever asked of it for an address that was typed — against a stand-in, because a test suite that called the real one would be editing the institute's network every time it ran
  • that the specification gvm writes itself carries the site's five settings and this machine's two, invents nothing where an address was not given, and is refused for a Windows template and for a configuration that is not complete — named field by field
  • that a customisation writes the hostname and the address into a vCenter's own specification and changes nothing else in it, for Linux and for Windows; that an address with no netmask behind it is refused, as are the two specification kinds gvm will not reach into; and — against the simulator's four real specifications — that the whole way through works
  • that a deployment is refused from anything that is not a template, that a name already in use is refused before the clone starts, and — against the simulator — that what comes out is a machine rather than another template, in the pool it was given, switched off
  • every rule a resize is held to: what a running machine may and may not be given, that the cores per socket are never changed to make a vCPU count fit, that a refusal never names 0 vCPUs as a count that would, and — against the simulator — that a reconfigure actually lands, read back from the server rather than assumed, and that a command line asking for one possible change and one impossible one makes neither
  • that a ^C during a login does not take gvm with it, and that the login still dies of it: the signal is caught for as long as the child has the screen, and catching is not ignoring — an ignored one would be inherited by the ssh
  • that a refresh which half failed still leaves the rows and the view describing the same list, and that the screen draws — it panicked before
  • that every layout of the estate screen fits the terminal it was drawn for, at nine widths from 60 columns up
  • that a page step on that screen does not leap twice when it lands on a cluster name, and that an unreachable host draws no load
  • that the estate screen groups the hosts under their clusters, that a heading is the sum of what is under it, that every placed machine is charged to exactly one host, and that Enter comes back with the list filtered to it
  • that a ratio says nothing where there is room to spare, and that an unknown load does not draw the same bar as an idle one
  • that the live refresh reads over the session it already has and not a new login — with the session closed it fails rather than reconnecting — that it notices a machine stopped behind gvm's back and says so, and that the trend keeps its scale, is bounded, and is forgotten when a machine goes
  • that the column ladder still only ever drops columns with the task column in the table, and that a terminal of eighty still keeps the address
  • that the completion cache survives a sweep of one server, forgets a machine the server has forgotten, is written 0600, and that every option the completion scripts complete after is an option gvm.go actually declares
  • that the mailed report carries no escape sequences, and that --json stays readable when a vCenter does not answer

gvm --version, the help and the options answered before the flag parser are checked against each other too: anything the help promises has to be something gvm answers, and the two options the completion scripts call are deliberately not in the help.