Write down why snapshots are taken without memory and without quiescing. No change in behaviour — Mike confirmed both are what he wants. What was missing was the reason, on a call whose two false arguments decide what a rollback gets back, and which nothing in the code or the README explained. Memory would keep the running machine's RAM as well, so a rollback came back mid-flight, at the price of writing all of it to the datastore every time. These snapshots are for the moment before a patch, where coming back to a machine that boots is the point. Quiescing would have VMware Tools still the guest's filesystems first. Without it the disk state is crash-consistent — what a machine finds after the plug is pulled — which a journalling filesystem handles and a database may not; and the snapshot neither depends on Tools running nor stops when they are not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
359 lines
11 KiB
Go
359 lines
11 KiB
Go
// snap.go — snapshots from the command line: listing, taking, reverting to and
|
|
// removing them. The operations themselves are in snapshot_ops.go; what is here
|
|
// is the connecting, the confirming and the reporting around them.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/vmware/govmomi/object"
|
|
"github.com/vmware/govmomi/vim25/mo"
|
|
"github.com/vmware/govmomi/vim25/types"
|
|
)
|
|
|
|
// snapList prints the snapshot tree of one machine.
|
|
func snapList(vc VCenter, vmname string) error {
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.close()
|
|
|
|
vm, err := s.vm(vmname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tree, err := snapshotsOf(s, vm.Reference())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tree) == 0 {
|
|
P("no snapshots for", vm.Name())
|
|
return nil
|
|
}
|
|
|
|
PF("Snapshots for %s (%s):\n", Cwb(vm.Name()), vc.Name)
|
|
printSnapshots(tree)
|
|
return nil
|
|
}
|
|
|
|
// snapNew takes a snapshot and then shows what the machine now has. The name is
|
|
// a random one, as before, but it is printed: a snapshot whose name nobody knows
|
|
// cannot be removed again by name.
|
|
func snapNew(vc VCenter, vmname string) error {
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.close()
|
|
|
|
vm, err := s.vm(vmname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
name := GETid(6)
|
|
PF("taking snapshot %s of %s (%s)\n", Cwb(name), Cwb(vm.Name()), vc.Name)
|
|
|
|
if err := snapshotNow(s, vm.Reference(), name, "Created via gvm"); err != nil {
|
|
return err
|
|
}
|
|
PO("snapshot " + name + " created")
|
|
|
|
tree, err := snapshotsOf(s, vm.Reference())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printSnapshots(tree)
|
|
return nil
|
|
}
|
|
|
|
// snapshotNow takes a snapshot over a connection that is already open, which is
|
|
// what the interactive list needs: it holds the session the machine was read on
|
|
// and has the machine's reference, not its inventory path.
|
|
//
|
|
// The error the task returns is the whole point of this function. The old code
|
|
// assigned it to a variable that was overwritten three lines later, so a
|
|
// snapshot that failed for want of datastore space was followed by a listing of
|
|
// the older snapshots under the heading "Snapshots for VM:" — which reads
|
|
// exactly like success.
|
|
func snapshotNow(s *session, ref types.ManagedObjectReference, name, desc string) error {
|
|
vm := object.NewVirtualMachine(s.client.Client, ref)
|
|
|
|
// Without memory and without quiescing, and both are deliberate.
|
|
//
|
|
// Memory would keep the running machine's RAM as well, so that a rollback
|
|
// came back mid-flight — at the price of writing the whole of it to the
|
|
// datastore every time, and of a rollback that restores a process tree
|
|
// along with the disks. What these snapshots are for is the moment before
|
|
// a patch or an upgrade, where coming back to a machine that boots is the
|
|
// point and coming back to one that is still half way through the thing
|
|
// that went wrong is not.
|
|
//
|
|
// Quiescing would have VMware Tools still the guest's filesystems first.
|
|
// Leaving it off makes the disk state crash-consistent — what a machine
|
|
// would find after the plug was pulled — which a journalling filesystem
|
|
// handles and a database may not. It also means the snapshot does not
|
|
// depend on Tools running, and does not stop when they are not.
|
|
task, err := vm.CreateSnapshot(s.ctx, name, desc, false, false)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: cannot start the snapshot %s: %w", s.vc.Name, name, err)
|
|
}
|
|
// Bounded, like every other task gvm waits for. This one was not: it waited
|
|
// on the session's own context, which has no deadline, so a snapshot that
|
|
// vCenter never finished froze the interactive list with the screen mid-draw
|
|
// and no key being read — the one place where waiting for ever is worst.
|
|
return waitTask(s.ctx, task, snapshotWait, SF("snapshot %s of %s", name, vmName(s, ref)))
|
|
}
|
|
|
|
// vmName is the machine's name for a message, from its reference alone. Cheap:
|
|
// one property, and only asked for when something has gone wrong enough to be
|
|
// worth naming.
|
|
func vmName(s *session, ref types.ManagedObjectReference) string {
|
|
var mvm mo.VirtualMachine
|
|
if err := object.NewVirtualMachine(s.client.Client, ref).
|
|
Properties(s.ctx, ref, []string{"name"}, &mvm); err != nil {
|
|
return ref.Value
|
|
}
|
|
return mvm.Name
|
|
}
|
|
|
|
// snapRemove removes one snapshot, with its children left where they are, and
|
|
// consolidates the disks afterwards.
|
|
func snapRemove(vc VCenter, vmname, snapname string, yes bool) error {
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.close()
|
|
|
|
vm, err := s.vm(vmname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry, err := findSnap(s, vm.Reference(), snapname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ok, err := confirmDestructive(vc, SF("remove snapshot %s of %s", entry.name, vm.Name()),
|
|
[][2]string{{"machine", vm.Name()}, {"snapshot", entry.name}, {"taken", entry.created}},
|
|
"this rollback point is gone; the machine keeps running as it is", yes)
|
|
if err != nil || !ok {
|
|
return err
|
|
}
|
|
|
|
P("waiting for vCenter, consolidating disks can take a while ...")
|
|
if err := removeSnapshot(s, entry.ref, SF("%s of %s", entry.name, vm.Name())); err != nil {
|
|
return err
|
|
}
|
|
PO("snapshot " + entry.name + " removed")
|
|
return nil
|
|
}
|
|
|
|
// snapRevert puts a machine back to one of its snapshots. The most destructive
|
|
// thing gvm does: unlike a removed snapshot, what is thrown away here — every
|
|
// write since the snapshot was taken — has nowhere to come back from.
|
|
func snapRevert(vc VCenter, vmname, snapname string, yes bool) error {
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.close()
|
|
|
|
vm, err := s.vm(vmname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry, err := findSnap(s, vm.Reference(), snapname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state, err := vm.PowerState(s.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: cannot read the power state of %s: %w", vc.Name, vm.Name(), err)
|
|
}
|
|
|
|
ok, err := confirmDestructive(vc, SF("revert %s to %s", vm.Name(), entry.name),
|
|
[][2]string{
|
|
{"machine", vm.Name()},
|
|
{"machine state", string(state)},
|
|
{"snapshot", entry.name},
|
|
{"taken", entry.created},
|
|
},
|
|
"everything written since then is lost, and cannot be recovered — "+
|
|
"gvm asks for the machine not to be started again afterwards", yes)
|
|
if err != nil || !ok {
|
|
return err
|
|
}
|
|
|
|
P("waiting for vCenter ...")
|
|
if err := revertToSnapshot(s, entry.ref, SF("%s of %s", entry.name, vm.Name())); err != nil {
|
|
return err
|
|
}
|
|
now := "state unknown"
|
|
if state, err := vm.PowerState(s.ctx); err == nil {
|
|
now = string(state)
|
|
}
|
|
PO(SF("%s reverted to %s — now %s", vm.Name(), entry.name, now))
|
|
return nil
|
|
}
|
|
|
|
// findSnap resolves a snapshot name to the one snapshot it means. A name in
|
|
// vSphere is not unique, so an ambiguous one is refused with the candidates and
|
|
// their dates listed — picking the first would be picking a rollback point on the
|
|
// operator's behalf, which is not gvm's to do.
|
|
func findSnap(s *session, vmref types.ManagedObjectReference, name string) (snapEntry, error) {
|
|
all, err := snapshotsOf(s, vmref)
|
|
if err != nil {
|
|
return snapEntry{}, err
|
|
}
|
|
|
|
var hits []snapEntry
|
|
for _, e := range all {
|
|
if e.name == name {
|
|
hits = append(hits, e)
|
|
}
|
|
}
|
|
switch len(hits) {
|
|
case 1:
|
|
return hits[0], nil
|
|
case 0:
|
|
if len(all) == 0 {
|
|
return snapEntry{}, errf("no snapshots at all")
|
|
}
|
|
names := make([]string, len(all))
|
|
for i, e := range all {
|
|
names[i] = SF("%s (%s)", e.name, e.created)
|
|
}
|
|
return snapEntry{}, errf("no snapshot called %q — there is %s",
|
|
name, strings.Join(names, ", "))
|
|
}
|
|
|
|
when := make([]string, len(hits))
|
|
for i, e := range hits {
|
|
when[i] = e.created
|
|
}
|
|
return snapEntry{}, errf("%q is the name of %d snapshots (%s) — "+
|
|
"rename them, or use the interactive list, which picks by reference",
|
|
name, len(hits), strings.Join(when, ", "))
|
|
}
|
|
|
|
// snapRemoveAll removes every snapshot a machine has.
|
|
func snapRemoveAll(vc VCenter, vmname string, yes bool) error {
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.close()
|
|
|
|
vm, err := s.vm(vmname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tree, err := snapshotsOf(s, vm.Reference())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tree) == 0 {
|
|
P("no snapshots for", vm.Name())
|
|
return nil
|
|
}
|
|
printSnapshots(tree)
|
|
|
|
ok, err := confirmDestructive(vc, SF("remove ALL snapshots of %s", vm.Name()),
|
|
[][2]string{{"machine", vm.Name()}},
|
|
"every rollback point of this machine is gone; "+
|
|
"the machine keeps running as it is", yes)
|
|
if err != nil || !ok {
|
|
return err
|
|
}
|
|
|
|
P("waiting for vCenter, consolidating disks can take a while ...")
|
|
if err := removeAllSnapshots(s, vm.Reference(), vm.Name()); err != nil {
|
|
return err
|
|
}
|
|
PO("all snapshots of " + vm.Name() + " removed")
|
|
return nil
|
|
}
|
|
|
|
// printSnapshots writes the tree the interactive list draws, indented one step
|
|
// so it reads as a listing rather than as output. One drawing for all three
|
|
// places a snapshot tree appears.
|
|
func printSnapshots(entries []snapEntry) {
|
|
for _, e := range entries {
|
|
PF(" %s\n", e.line())
|
|
}
|
|
}
|
|
|
|
// confirm asks before something is destroyed, and separates the two ways of not
|
|
// getting a yes.
|
|
//
|
|
// Saying no is an answer: nothing happens and the command succeeds at doing
|
|
// nothing. Not being able to ask is not an answer, and it is reported as an
|
|
// error — otherwise `gvm power --off db01` in a script would print "nothing
|
|
// done", exit 0, and leave the script believing the machine was stopped. -y is
|
|
// how that script says it means it.
|
|
func confirm(question string, yes bool) (bool, error) {
|
|
if yes {
|
|
return true, nil
|
|
}
|
|
if err := haveTerminal(); err != nil {
|
|
return false, errf("refusing to go ahead without a confirmation: "+
|
|
"there is no terminal to ask on (%v) — pass -y if this is meant to run unattended", err)
|
|
}
|
|
if Yesno(question, false, false) {
|
|
return true, nil
|
|
}
|
|
P("nothing done")
|
|
return false, nil
|
|
}
|
|
|
|
// confirmDestructive is the command line's version of the interactive
|
|
// confirmation page: what is about to happen, to which machine, on which server,
|
|
// and what it costs — then the question, which defaults to no.
|
|
//
|
|
// It does not ask for the machine's name to be typed, as the interactive list
|
|
// does. There the machine is whatever the cursor happens to be on; here it was
|
|
// spelled out on the command line a moment ago, and asking for it twice would
|
|
// teach the habit of retyping without reading. -y skips the question entirely,
|
|
// which is what a cron job needs and what an operator has to type deliberately.
|
|
func confirmDestructive(vc VCenter, headline string, facts [][2]string, consequence string, yes bool) (bool, error) {
|
|
P()
|
|
PF("%s %s\n", Crb("⚠ "), Cwb(headline))
|
|
printFacts(vc, facts)
|
|
for _, l := range wrap(consequence, 72) {
|
|
PF(" %s\n", Cr(l))
|
|
}
|
|
P()
|
|
return confirm("continue?", yes)
|
|
}
|
|
|
|
// confirmFacts is that page without the warning, for something that makes a
|
|
// thing rather than destroying one. Deploying a machine is not a decision to be
|
|
// talked out of in red; it is one to be shown the placement of first, because
|
|
// "a new machine appeared somewhere on the estate" is not an outcome anybody
|
|
// should get from a keystroke.
|
|
func confirmFacts(vc VCenter, headline string, facts [][2]string, yes bool) (bool, error) {
|
|
P()
|
|
PF("%s\n", Cwb(headline))
|
|
printFacts(vc, facts)
|
|
return confirm("continue?", yes)
|
|
}
|
|
|
|
// printFacts is the block both of them show: the server first, because the same
|
|
// machine name exists on more than one, then whatever this particular question
|
|
// is about.
|
|
func printFacts(vc VCenter, facts [][2]string) {
|
|
P()
|
|
all := append([][2]string{{"vCenter", vc.Name + " " + vc.URL}, {"datacenter", vc.Datacenter}}, facts...)
|
|
for _, f := range all {
|
|
PF(" %-14s %s\n", f[0], f[1])
|
|
}
|
|
P()
|
|
}
|