The second road: gvm writes the customisation itself, out of ~/.gvmrc.
Five settings, which are the site's answers and not any machine's — domain,
dns, netmask, gateway, timezone — written once in the configuration file where
every other site answer already lives. The two that are about the one machine
stay on the command line:
gvm new --from ubuntu-tpl --name web05 --ip 10.0.0.55
--spec stays exactly as it was, and both roads are now offered side by side:
the picker lists gvm's own alongside whatever specifications the vCenter holds,
and the deployment takes whichever was chosen. A site with specifications
should still prefer them — the policy is then somewhere the web client can see
it too — and Windows has no other option, since a Sysprep is a licence key, an
administrator password and a domain to join, none of which belongs in a file
next to the SMTP relay. A Windows template is refused by name, pointing at
--spec.
What it writes: LinuxPrep with the hostname (the machine's name unless
--hostname says otherwise), the domain and the timezone; the resolvers and the
search domain in the global settings; and one adapter with the address, the
netmask and the gateway. Without --ip the adapter is left on DHCP, which is a
whole answer — the name is still set, and that is what was asked for.
Refused rather than guessed: a configuration that is not complete, named field
by field; an address, netmask or gateway that is not one, each said to be the
configuration's; and a Windows template. Said rather than refused: 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.
`gvm config` grew a line for it, which says either what a new guest would be
told or which settings are still missing. browseVMs takes the configuration
rather than one string out of it, since it now needs two things from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1901 lines
61 KiB
Go
1901 lines
61 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/vmware/govmomi/object"
|
|
"github.com/vmware/govmomi/simulator"
|
|
"github.com/vmware/govmomi/vim25/mo"
|
|
"github.com/vmware/govmomi/vim25/types"
|
|
)
|
|
|
|
// The tests in this file run against govmomi's own vCenter simulator, started
|
|
// inside the test process and thrown away with it. That is deliberate: the
|
|
// vCenters gvm is configured for are production, and nothing here — least of all
|
|
// the snapshot round trip below — may go anywhere near them.
|
|
|
|
// simVCenter brings up a throwaway vCenter and returns it as gvm's own
|
|
// configuration type, so the code under test connects to it exactly the way it
|
|
// connects to a real one.
|
|
func simVCenter(t *testing.T) VCenter {
|
|
vc, _ := simVCenterModel(t)
|
|
return vc
|
|
}
|
|
|
|
// simVCenterModel also hands back the model, so a test can reach into the
|
|
// simulated inventory — starting VMware Tools on a machine, say, which no client
|
|
// can do from outside.
|
|
func simVCenterModel(t *testing.T) (VCenter, *simulator.Model) {
|
|
t.Helper()
|
|
|
|
// Every sweep leaves the machine names in the cache directory for the shell
|
|
// to complete against (complete.go), so the cache is pointed at a temporary
|
|
// home first: a test run must no more write "DC0_C0_RP0_VM0" into the
|
|
// completion cache of the person running it than it may touch a real vCenter.
|
|
cacheHome(t)
|
|
|
|
model := simulator.VPX()
|
|
model.Datacenter = 1
|
|
model.Host = 2
|
|
model.Machine = 3
|
|
if err := model.Create(); err != nil {
|
|
t.Fatalf("cannot create the simulated vCenter: %v", err)
|
|
}
|
|
t.Cleanup(model.Remove)
|
|
|
|
s := model.Service.NewServer()
|
|
t.Cleanup(s.Close)
|
|
|
|
pw, _ := s.URL.User.Password()
|
|
return VCenter{
|
|
Name: "sim",
|
|
URL: s.URL.Scheme + "://" + s.URL.Host + "/",
|
|
User: s.URL.User.Username(),
|
|
Password: pw,
|
|
Datacenter: "DC0",
|
|
Insecure: "true",
|
|
}, model
|
|
}
|
|
|
|
// startTools makes VMware Tools report as running on one simulated machine. The
|
|
// simulator leaves it "not installed", which is realistic for a fresh machine but
|
|
// means the graceful operations can never be exercised end to end.
|
|
func startTools(t *testing.T, model *simulator.Model, ref types.ManagedObjectReference) {
|
|
t.Helper()
|
|
vm, ok := model.Map().Get(ref).(*simulator.VirtualMachine)
|
|
if !ok {
|
|
t.Fatalf("%v is not a simulated machine", ref)
|
|
}
|
|
vm.Guest.ToolsRunningStatus = string(types.VirtualMachineToolsRunningStatusGuestToolsRunning)
|
|
vm.Guest.ToolsStatus = types.VirtualMachineToolsStatusToolsOk
|
|
vm.Summary.Guest.ToolsRunningStatus = vm.Guest.ToolsRunningStatus
|
|
vm.Summary.Guest.ToolsStatus = vm.Guest.ToolsStatus
|
|
}
|
|
|
|
// joinSnaps flattens what snapshotLines found, dropping the colour it chose.
|
|
func joinSnaps(lines []string, _ string) string { return strings.Join(lines, "\n") }
|
|
|
|
// powerOf reads a machine's power state straight from the server, which is the
|
|
// only answer worth asserting on after a power operation.
|
|
func powerOf(t *testing.T, s *session, ref types.ManagedObjectReference) types.VirtualMachinePowerState {
|
|
t.Helper()
|
|
state, err := object.NewVirtualMachine(s.client.Client, ref).PowerState(s.ctx)
|
|
if err != nil {
|
|
t.Fatalf("cannot read the power state: %v", err)
|
|
}
|
|
return state
|
|
}
|
|
|
|
// oneRow connects and returns the machine of that name as the list would hold it.
|
|
func oneRow(t *testing.T, vc VCenter, name string) (*session, vmRow) {
|
|
t.Helper()
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
t.Cleanup(func() { closeSessions(sessions) })
|
|
for _, r := range rows {
|
|
if r.name == name {
|
|
return r.sess, r
|
|
}
|
|
}
|
|
t.Fatalf("no machine called %q", name)
|
|
return nil, vmRow{}
|
|
}
|
|
|
|
// quiet sends the P/PF family to /dev/null for the duration of one test: the
|
|
// commands are meant to print, and their output is not what is being checked.
|
|
func quiet(t *testing.T) {
|
|
t.Helper()
|
|
devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
saved := os.Stdout
|
|
os.Stdout = devnull
|
|
t.Cleanup(func() { os.Stdout = saved; devnull.Close() })
|
|
}
|
|
|
|
func TestSimGatherVMs(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
t.Fatal("the simulated vCenter yielded no machines")
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
for _, r := range rows {
|
|
if r.name == "" {
|
|
t.Error("a machine came back without a name")
|
|
}
|
|
if r.sess == nil {
|
|
t.Errorf("%s has no session to ask for details", r.name)
|
|
}
|
|
if r.vc.Name != "sim" {
|
|
t.Errorf("%s claims to be on %q", r.name, r.vc.Name)
|
|
}
|
|
if r.host == "" || r.host == "-" {
|
|
t.Errorf("%s did not resolve its host: %q", r.name, r.host)
|
|
}
|
|
seen[r.name] = true
|
|
}
|
|
if len(seen) != len(rows) {
|
|
t.Errorf("%d machines but only %d distinct names", len(rows), len(seen))
|
|
}
|
|
|
|
// Sorted by name, which is what the list relies on.
|
|
for i := 1; i < len(rows); i++ {
|
|
if strings.ToLower(rows[i-1].name) > strings.ToLower(rows[i].name) {
|
|
t.Errorf("rows are not sorted: %q before %q", rows[i-1].name, rows[i].name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// The whole interaction, minus the terminal: filter, select, open the sheet. The
|
|
// sheet is where the live session is used a second time, for the snapshots.
|
|
func TestSimBrowseAndOpenDetail(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
|
|
b := &browser{targets: []VCenter{vc}, rows: rows}
|
|
b.refilter()
|
|
if len(b.view) != len(rows) {
|
|
t.Fatalf("the unfiltered view holds %d of %d machines", len(b.view), len(rows))
|
|
}
|
|
|
|
want := rows[0].name
|
|
b.filter = want
|
|
b.refilter()
|
|
if len(b.view) == 0 || b.current().name != want {
|
|
t.Fatalf("filtering for %q left %d rows", want, len(b.view))
|
|
}
|
|
|
|
b.openDetail()
|
|
if b.detail == nil {
|
|
t.Fatal("enter did not open the sheet")
|
|
}
|
|
// The machine and where it lives are the title; the sheet itself is the
|
|
// facts about it.
|
|
if !strings.Contains(b.dtitle, want) {
|
|
t.Errorf("the title is %q, expected it to name %s", b.dtitle, want)
|
|
}
|
|
if b.dvc != "sim" {
|
|
t.Errorf("the title names the vCenter as %q", b.dvc)
|
|
}
|
|
if !strings.Contains(b.dwhere, "DC0") {
|
|
t.Errorf("the title's tail %q does not say which datacenter", b.dwhere)
|
|
}
|
|
sheet := sheetText(b.detail)
|
|
for _, s := range []string{"snapshots", "moref", "state"} {
|
|
if !strings.Contains(sheet, s) {
|
|
t.Errorf("the sheet does not mention %q\n%s", s, sheet)
|
|
}
|
|
}
|
|
if strings.Contains(sheet, "cannot read the snapshots") {
|
|
t.Errorf("the snapshot query failed against the simulator\n%s", sheet)
|
|
}
|
|
if !strings.Contains(b.dtitle, want) {
|
|
t.Errorf("the title is %q", b.dtitle)
|
|
}
|
|
|
|
// Esc goes back to the list, and nothing is left behind.
|
|
b.detailKey(key{special: keyEsc})
|
|
if b.detail != nil || b.dscroll != 0 {
|
|
t.Error("Esc did not close the sheet")
|
|
}
|
|
}
|
|
|
|
// Taking a snapshot, seeing it, removing it again — against the simulator, and
|
|
// only there.
|
|
func TestSimSnapshotRoundTrip(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
name := rows[0].name
|
|
closeSessions(sessions)
|
|
|
|
if err := snapNew(vc, name); err != nil {
|
|
t.Fatalf("snapNew: %v", err)
|
|
}
|
|
if err := snapList(vc, name); err != nil {
|
|
t.Fatalf("snapList: %v", err)
|
|
}
|
|
|
|
// The sheet has to show it now.
|
|
found, err = gatherVMs([]VCenter{vc})
|
|
rows, sessions = found.rows, found.sessions
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
defer closeSessions(sessions)
|
|
var row vmRow
|
|
for _, r := range rows {
|
|
if r.name == name {
|
|
row = r
|
|
}
|
|
}
|
|
lines := joinSnaps(snapshotLines(row))
|
|
if strings.Contains(lines, "none") || strings.Contains(lines, "cannot read") {
|
|
t.Errorf("the sheet shows no snapshot after taking one: %q", lines)
|
|
}
|
|
|
|
if err := snapRemoveAll(vc, name, true); err != nil {
|
|
t.Fatalf("snapRemoveAll: %v", err)
|
|
}
|
|
if lines := joinSnaps(snapshotLines(row)); !strings.Contains(lines, "none") {
|
|
t.Errorf("snapshots left after removing all: %q", lines)
|
|
}
|
|
}
|
|
|
|
func TestSimReadOnlyCommands(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
cfg := Config{VCenters: []VCenter{vc}, Default: "sim"}
|
|
|
|
if err := lsvm([]VCenter{vc}, lsOptions{}); err != nil {
|
|
t.Errorf("lsvm: %v", err)
|
|
}
|
|
if err := lsvm([]VCenter{vc}, lsOptions{match: "DC0"}); err != nil {
|
|
t.Errorf("lsvm with a pattern: %v", err)
|
|
}
|
|
if err := hoststat(vc, ""); err != nil {
|
|
t.Errorf("hoststat: %v", err)
|
|
}
|
|
if err := vmstat(vc); err != nil {
|
|
t.Errorf("vmstat: %v", err)
|
|
}
|
|
if err := vmlog(cfg, vc, 60, false); err != nil {
|
|
t.Errorf("vmlog: %v", err)
|
|
}
|
|
|
|
// A pattern that is not a regexp is a message, not a panic.
|
|
if err := lsvm([]VCenter{vc}, lsOptions{match: "web("}); err == nil {
|
|
t.Error("lsvm accepted a broken pattern")
|
|
}
|
|
}
|
|
|
|
// The error paths that used to end in a nil dereference.
|
|
func TestSimMissingThings(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, err := connect(vc)
|
|
if err != nil {
|
|
t.Fatalf("connect: %v", err)
|
|
}
|
|
defer s.close()
|
|
|
|
if _, err := s.vm("no-such-machine"); err == nil {
|
|
t.Error("a machine that does not exist came back without an error")
|
|
}
|
|
|
|
wrongDC := vc
|
|
wrongDC.Datacenter = "no-such-datacenter"
|
|
if err := snapList(wrongDC, "anything"); err == nil {
|
|
t.Error("a datacenter that does not exist came back without an error")
|
|
}
|
|
|
|
wrongPW := vc
|
|
wrongPW.URL = "http://127.0.0.1:1/"
|
|
if err := hoststat(wrongPW, ""); err == nil {
|
|
t.Error("a refused connection came back without an error")
|
|
}
|
|
}
|
|
|
|
// The one key in the list that changes anything: ^s. Driven here with a pipe for
|
|
// the keyboard and /dev/null for the screen, against the simulator — the
|
|
// confirmation, the refusal and the snapshot itself.
|
|
func TestSimInteractiveSnapshot(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
|
|
b := &browser{targets: []VCenter{vc}, rows: rows}
|
|
b.refilter()
|
|
|
|
screen, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer screen.Close()
|
|
keysR, keysW, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer keysW.Close()
|
|
b.tty, b.keys = screen, newKeyReader(keysR)
|
|
|
|
r := b.current()
|
|
if r == nil {
|
|
t.Fatal("no machine selected")
|
|
}
|
|
t.Cleanup(func() { snapRemoveAll(vc, r.name, true) })
|
|
|
|
// 0x13 is what a terminal sends for ^s, and it is the sheet that answers it:
|
|
// from the table it does nothing but point at ⏎.
|
|
keysW.WriteString("\x13")
|
|
if k := b.keys.next(); k.special != keyCtrlS {
|
|
t.Fatalf("^s decoded as %v", k)
|
|
}
|
|
b.listKey(key{special: keyCtrlS})
|
|
if b.edit != nil || b.confirm != nil {
|
|
t.Fatal("^s started something from the table")
|
|
}
|
|
b.listKey(key{special: keyEnter}) // open the sheet, where the actions live
|
|
if b.detail == nil {
|
|
t.Fatal("the sheet did not open")
|
|
}
|
|
|
|
// Abandoned at the name: Esc, and nothing happens.
|
|
keysW.WriteString("\x1b")
|
|
b.snapshot()
|
|
if !strings.Contains(b.status, "nothing done") {
|
|
t.Errorf("status after Esc in the name field: %q", b.status)
|
|
}
|
|
if lines := joinSnaps(snapshotLines(*r)); !strings.Contains(lines, "none") {
|
|
t.Fatalf("a snapshot was taken although the name was abandoned: %q", lines)
|
|
}
|
|
|
|
// Declined at the question: a name is typed, then anything but y.
|
|
keysW.WriteString("declined\rn")
|
|
b.snapshot()
|
|
if !strings.Contains(b.status, "nothing done") {
|
|
t.Errorf("status after declining: %q", b.status)
|
|
}
|
|
if lines := joinSnaps(snapshotLines(*r)); !strings.Contains(lines, "none") {
|
|
t.Fatalf("a snapshot was taken although the question was declined: %q", lines)
|
|
}
|
|
|
|
// Accepted, with a name of one's own: it is what ends up on the machine.
|
|
const want = "before-the-upgrade"
|
|
keysW.WriteString(want + "\ry")
|
|
b.snapshot()
|
|
if !strings.Contains(b.status, "created") {
|
|
t.Fatalf("status after ^s: %q", b.status)
|
|
}
|
|
if b.statusCol != colInfo {
|
|
t.Errorf("a snapshot that worked is not reported in the ok colour")
|
|
}
|
|
if !strings.Contains(b.status, want) {
|
|
t.Errorf("the status does not name the snapshot: %q", b.status)
|
|
}
|
|
lines := joinSnaps(snapshotLines(*r))
|
|
if !strings.Contains(lines, want) {
|
|
t.Fatalf("the snapshot is not called %q:\n%s", want, lines)
|
|
}
|
|
if strings.Contains(lines, "declined") {
|
|
t.Errorf("the declined snapshot was taken after all:\n%s", lines)
|
|
}
|
|
|
|
// An empty name takes the generated one, and with the sheet open the new
|
|
// snapshot is there to see without hunting for it.
|
|
b.openDetail()
|
|
keysW.WriteString("\ry")
|
|
b.snapshot()
|
|
generated := strings.Fields(b.status)[1] // "snapshot XXXXXX created on ..."
|
|
if generated == want || len(generated) != 6 {
|
|
t.Errorf("an empty name did not fall back to a generated one: %q", b.status)
|
|
}
|
|
sheet := sheetText(b.detail)
|
|
if !strings.Contains(sheet, want) || !strings.Contains(sheet, generated) {
|
|
t.Errorf("the open sheet does not show both snapshots:\n%s", sheet)
|
|
}
|
|
if b.detail[b.dscroll].label != "snapshots" {
|
|
t.Errorf("the sheet did not jump to the snapshots, it is at %q", b.detail[b.dscroll].plain())
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------- the destructive half
|
|
//
|
|
// Everything below runs against the simulator started in this process, and only
|
|
// ever against that. The configured vCenters are production: no test here reads
|
|
// ~/.gvmrc, and none of them can, because the VCenter they are handed is built
|
|
// from the simulator's own address.
|
|
|
|
// The three hard power operations, end to end, asserted on the state the server
|
|
// reports rather than on what gvm printed.
|
|
func TestSimHardPowerOperations(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
s, r := oneRow(t, vc, "DC0_H1_VM0")
|
|
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOn {
|
|
t.Fatalf("the machine starts out %s, expected it running", got)
|
|
}
|
|
|
|
if _, err := runPower(s, r, opPowerOff); err != nil {
|
|
t.Fatalf("power off: %v", err)
|
|
}
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOff {
|
|
t.Fatalf("after power off the machine is %s", got)
|
|
}
|
|
|
|
// The row still says "running" — runPower has to re-check against what it is
|
|
// handed, so powering off twice must be refused rather than sent.
|
|
if _, err := runPower(s, r, opPowerOff); err == nil {
|
|
t.Error("powering off a stopped machine was sent")
|
|
}
|
|
|
|
_, off := oneRow(t, vc, "DC0_H1_VM0")
|
|
if _, err := runPower(s, off, opPowerOn); err != nil {
|
|
t.Fatalf("power on: %v", err)
|
|
}
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOn {
|
|
t.Fatalf("after power on the machine is %s", got)
|
|
}
|
|
|
|
_, on := oneRow(t, vc, "DC0_H1_VM0")
|
|
if _, err := runPower(s, on, opReset); err != nil {
|
|
t.Fatalf("reset: %v", err)
|
|
}
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOn {
|
|
t.Fatalf("after a reset the machine is %s", got)
|
|
}
|
|
}
|
|
|
|
// Without VMware Tools the graceful operations must be refused by gvm itself,
|
|
// and nothing may reach vCenter — the machine has to still be running afterwards.
|
|
func TestSimGracefulRefusedWithoutToolsSendsNothing(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
s, r := oneRow(t, vc, "DC0_H1_VM1")
|
|
|
|
if r.toolsRunning() {
|
|
t.Fatal("the simulated machine unexpectedly has Tools running")
|
|
}
|
|
for _, op := range []powerOp{opShutdownGuest, opRebootGuest} {
|
|
if _, err := runPower(s, r, op); err == nil {
|
|
t.Errorf("%s was sent although Tools is not running", op.spec().verb)
|
|
}
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOn {
|
|
t.Fatalf("the machine is %s after a refused %s — something was sent", got, op.spec().verb)
|
|
}
|
|
}
|
|
}
|
|
|
|
// With Tools running, a graceful shutdown goes through and the machine stops.
|
|
func TestSimGracefulShutdownWithTools(t *testing.T) {
|
|
quiet(t)
|
|
vc, model := simVCenterModel(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_H1_VM2")
|
|
startTools(t, model, r.ref)
|
|
|
|
_, r = oneRow(t, vc, "DC0_H1_VM2") // read again, now that Tools answers
|
|
if !r.toolsRunning() {
|
|
t.Fatal("Tools still does not report as running")
|
|
}
|
|
msg, err := runPower(s, r, opShutdownGuest)
|
|
if err != nil {
|
|
t.Fatalf("shutdown: %v", err)
|
|
}
|
|
if !strings.Contains(msg, "asked to shut down") {
|
|
t.Errorf("the message does not say the guest was asked: %q", msg)
|
|
}
|
|
if got := powerOf(t, s, r.ref); got != types.VirtualMachinePowerStatePoweredOff {
|
|
t.Errorf("after the guest shutdown the machine is %s", got)
|
|
}
|
|
}
|
|
|
|
// Reverting: the machine ends up on the snapshot that was picked, and powered
|
|
// off, whatever it was before.
|
|
func TestSimRevertGoesToTheSnapshotAndLeavesTheMachineOff(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
|
|
if err := snapshotNow(s, r.ref, "first", "one"); err != nil {
|
|
t.Fatalf("first snapshot: %v", err)
|
|
}
|
|
if err := snapshotNow(s, r.ref, "second", "two"); err != nil {
|
|
t.Fatalf("second snapshot: %v", err)
|
|
}
|
|
|
|
snaps, err := snapshotsOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("snapshotsOf: %v", err)
|
|
}
|
|
var first snapEntry
|
|
for _, e := range snaps {
|
|
if e.name == "first" {
|
|
first = e
|
|
}
|
|
}
|
|
if first.ref.Value == "" {
|
|
t.Fatalf("the first snapshot is not among %v", snaps)
|
|
}
|
|
|
|
if err := revertToSnapshot(s, first.ref, "first of DC0_H2_VM0"); err != nil {
|
|
t.Fatalf("revert: %v", err)
|
|
}
|
|
|
|
var mvm mo.VirtualMachine
|
|
vm := object.NewVirtualMachine(s.client.Client, r.ref)
|
|
if err := vm.Properties(s.ctx, r.ref, []string{"snapshot"}, &mvm); err != nil {
|
|
t.Fatalf("cannot read the snapshots back: %v", err)
|
|
}
|
|
if mvm.Snapshot == nil || mvm.Snapshot.CurrentSnapshot == nil {
|
|
t.Fatal("the machine is on no snapshot after the revert")
|
|
}
|
|
if *mvm.Snapshot.CurrentSnapshot != first.ref {
|
|
t.Errorf("the machine is on %v, want %v", *mvm.Snapshot.CurrentSnapshot, first.ref)
|
|
}
|
|
// What the revert does to the power state cannot be checked here: the
|
|
// simulator's RevertToSnapshotTask only moves snapshot.currentSnapshot and
|
|
// leaves the machine's power alone, whatever the request asked for. The part
|
|
// gvm is responsible for — asking for the machine not to be started again —
|
|
// is checked in TestRevertRequestSuppressesPowerOn instead.
|
|
}
|
|
|
|
// The one thing about a revert that no server here will show back: that gvm asks
|
|
// for the machine not to be powered on again. Reverting to a snapshot taken while
|
|
// a machine was running otherwise starts it, with a rewound disk, while the
|
|
// operator is still reading the message.
|
|
func TestRevertRequestSuppressesPowerOn(t *testing.T) {
|
|
req := revertRequest(types.ManagedObjectReference{Type: "VirtualMachineSnapshot", Value: "snapshot-7"})
|
|
if req.SuppressPowerOn == nil {
|
|
t.Fatal("SuppressPowerOn is not set at all, so vCenter decides")
|
|
}
|
|
if !*req.SuppressPowerOn {
|
|
t.Error("SuppressPowerOn is false: a reverted machine would start again on its own")
|
|
}
|
|
if req.This.Value != "snapshot-7" {
|
|
t.Errorf("the request points at %v", req.This)
|
|
}
|
|
}
|
|
|
|
// Two snapshots, one name. Removing by reference takes the one that was picked;
|
|
// resolving by name could not tell them apart at all. This is the reason
|
|
// snapshot_ops.go addresses snapshots the way it does.
|
|
func TestSimRemovesTheSnapshotThatWasPickedNotTheName(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM1")
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if err := snapshotNow(s, r.ref, "nightly", "same name on purpose"); err != nil {
|
|
t.Fatalf("snapshot %d: %v", i, err)
|
|
}
|
|
}
|
|
snaps, err := snapshotsOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("snapshotsOf: %v", err)
|
|
}
|
|
if len(snaps) != 2 {
|
|
t.Fatalf("expected two snapshots, got %d", len(snaps))
|
|
}
|
|
|
|
doomed, survivor := snaps[0], snaps[1]
|
|
if err := removeSnapshot(s, doomed.ref, "nightly"); err != nil {
|
|
t.Fatalf("remove: %v", err)
|
|
}
|
|
|
|
left, err := snapshotsOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("snapshotsOf: %v", err)
|
|
}
|
|
if len(left) != 1 {
|
|
t.Fatalf("expected one snapshot left, got %d", len(left))
|
|
}
|
|
if left[0].ref != survivor.ref {
|
|
t.Errorf("the wrong snapshot was removed: %v is left, expected %v", left[0].ref, survivor.ref)
|
|
}
|
|
|
|
// And by name, gvm refuses rather than guessing which of two it means.
|
|
_, err = findSnap(s, r.ref, "nightly")
|
|
if err != nil {
|
|
t.Errorf("one snapshot called nightly should now resolve: %v", err)
|
|
}
|
|
if err := snapshotNow(s, r.ref, "nightly", "again"); err != nil {
|
|
t.Fatalf("third snapshot: %v", err)
|
|
}
|
|
if _, err := findSnap(s, r.ref, "nightly"); err == nil {
|
|
t.Error("an ambiguous name resolved to something")
|
|
} else if !strings.Contains(err.Error(), "2 snapshots") {
|
|
t.Errorf("the refusal does not say why: %v", err)
|
|
}
|
|
if _, err := findSnap(s, r.ref, "no-such-snapshot"); err == nil {
|
|
t.Error("a name that is not there resolved to something")
|
|
}
|
|
}
|
|
|
|
// The whole interactive path for a destructive operation: the menu, the picker,
|
|
// the confirmation — and nothing happening when the confirmation is not given.
|
|
func TestSimBrowserDestructiveNeedsTheName(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
b := &browser{targets: []VCenter{vc}, rows: rows}
|
|
b.refilter()
|
|
b.filter = "DC0_C0_RP0_VM2"
|
|
b.refilter()
|
|
r := b.current()
|
|
if r == nil || r.name != "DC0_C0_RP0_VM2" {
|
|
t.Fatalf("the wrong machine is selected: %v", r)
|
|
}
|
|
|
|
screen, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer screen.Close()
|
|
keysR, keysW, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer keysW.Close()
|
|
b.tty, b.keys = screen, newKeyReader(keysR)
|
|
|
|
if err := snapshotNow(r.sess, r.ref, "keep-me", "for the test"); err != nil {
|
|
t.Fatalf("snapshot: %v", err)
|
|
}
|
|
|
|
// The real route: ⏎ for the sheet, then ^a for the menu, which knows the
|
|
// machine now has one snapshot.
|
|
b.listKey(key{special: keyEnter})
|
|
b.detailKey(key{special: keyCtrlA})
|
|
if len(b.menu) == 0 {
|
|
t.Fatal("the menu did not open")
|
|
}
|
|
if len(b.menuSnaps) != 1 {
|
|
t.Fatalf("the menu sees %d snapshots", len(b.menuSnaps))
|
|
}
|
|
// And the facts above the choices, read from the machine as it is now.
|
|
if len(b.menuInfo) == 0 {
|
|
t.Error("the menu shows no facts above the choices")
|
|
}
|
|
if b.menuInfo[0].label != "state" || !strings.Contains(b.menuInfo[0].value, "poweredOn") {
|
|
t.Errorf("the first fact is %+v, expected the machine's state", b.menuInfo[0])
|
|
}
|
|
|
|
// 'd' opens the picker; Enter chooses the snapshot and lands on the
|
|
// confirmation, which is answered with the wrong name.
|
|
keysW.WriteString("yes\r") // lower case: not enough
|
|
b.menuKey(key{special: keyRune, r: 'd'})
|
|
if b.pick == nil {
|
|
t.Fatal("the picker did not open")
|
|
}
|
|
b.pickerKey(key{special: keyEnter})
|
|
|
|
if !strings.Contains(b.status, "nothing done") {
|
|
t.Errorf("status after lower-case yes: %q", b.status)
|
|
}
|
|
left, err := snapshotsOf(r.sess, r.ref)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(left) != 1 {
|
|
t.Fatalf("the snapshot was removed although the confirmation was not given (%d left)", len(left))
|
|
}
|
|
|
|
// The same again with YES: this time it happens.
|
|
keysW.WriteString("YES\r")
|
|
b.detailKey(key{special: keyCtrlA})
|
|
b.menuKey(key{special: keyRune, r: 'd'})
|
|
b.pickerKey(key{special: keyEnter})
|
|
|
|
if !strings.Contains(b.status, "removed") {
|
|
t.Errorf("status after YES: %q", b.status)
|
|
}
|
|
left, err = snapshotsOf(r.sess, r.ref)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(left) != 0 {
|
|
t.Errorf("%d snapshots left after the removal was confirmed", len(left))
|
|
}
|
|
}
|
|
|
|
// Powering off through the browser: declined first, then confirmed, with the
|
|
// machine's real state checked after each.
|
|
func TestSimBrowserPowerOffNeedsTheName(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
b := &browser{targets: []VCenter{vc}, rows: rows, filter: "DC0_H0_VM2"}
|
|
b.refilter()
|
|
r := b.current()
|
|
if r == nil || r.name != "DC0_H0_VM2" {
|
|
t.Fatalf("the wrong machine is selected: %v", r)
|
|
}
|
|
sess := r.sess
|
|
|
|
screen, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
defer screen.Close()
|
|
keysR, keysW, _ := os.Pipe()
|
|
defer keysW.Close()
|
|
b.tty, b.keys = screen, newKeyReader(keysR)
|
|
|
|
// Esc at the confirmation.
|
|
keysW.WriteString("\x1b")
|
|
b.listKey(key{special: keyEnter}) // the sheet first: the table has no actions
|
|
b.detailKey(key{special: keyCtrlA})
|
|
b.menuKey(key{special: keyRune, r: 'S'})
|
|
if got := powerOf(t, sess, r.ref); got != types.VirtualMachinePowerStatePoweredOn {
|
|
t.Fatalf("the machine is %s after Esc at the confirmation", got)
|
|
}
|
|
|
|
// YES.
|
|
keysW.WriteString("YES\r")
|
|
b.detailKey(key{special: keyCtrlA})
|
|
b.menuKey(key{special: keyRune, r: 'S'})
|
|
if got := powerOf(t, sess, r.ref); got != types.VirtualMachinePowerStatePoweredOff {
|
|
t.Fatalf("the machine is %s after the power off was confirmed", got)
|
|
}
|
|
if !strings.Contains(b.status, "power off done") {
|
|
t.Errorf("status: %q", b.status)
|
|
}
|
|
|
|
// The row was refreshed, so the list shows "off" rather than what the sweep
|
|
// found — and the menu now offers power on instead of power off.
|
|
if b.current().running() {
|
|
t.Error("the list still shows the machine as running")
|
|
}
|
|
b.detailKey(key{special: keyCtrlA})
|
|
for _, m := range b.menu {
|
|
switch m.key {
|
|
case 'o':
|
|
if !m.available() {
|
|
t.Error("power on is not offered for the machine that was just stopped")
|
|
}
|
|
case 'S':
|
|
if m.available() {
|
|
t.Error("power off is still offered for a stopped machine")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A guest shutdown is a request, not a task: there is no moment for gvm to wait
|
|
// for, so the row stays as it was until something re-reads it. Opening the menu
|
|
// again is that something — and it has to offer power on rather than power off.
|
|
func TestSimMenuRereadsTheRow(t *testing.T) {
|
|
quiet(t)
|
|
vc, model := simVCenterModel(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
b := &browser{targets: []VCenter{vc}, rows: rows, filter: "DC0_H1_VM0"}
|
|
b.refilter()
|
|
r := b.current()
|
|
if r == nil {
|
|
t.Fatal("no machine selected")
|
|
}
|
|
startTools(t, model, r.ref)
|
|
|
|
screen, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
defer screen.Close()
|
|
keysR, keysW, _ := os.Pipe()
|
|
defer keysW.Close()
|
|
b.tty, b.keys = screen, newKeyReader(keysR)
|
|
|
|
// The menu re-reads, so it now sees Tools and offers the graceful shutdown.
|
|
b.listKey(key{special: keyEnter}) // the sheet, then the menu
|
|
b.detailKey(key{special: keyCtrlA})
|
|
var shutdown menuItem
|
|
for _, m := range b.menu {
|
|
if m.key == 's' {
|
|
shutdown = m
|
|
}
|
|
}
|
|
if !shutdown.available() {
|
|
t.Fatalf("shut down is not offered although Tools was started: %s", shutdown.why)
|
|
}
|
|
|
|
keysW.WriteString("YES\r")
|
|
b.menuKey(key{special: keyRune, r: 's'})
|
|
if !strings.Contains(b.status, "asked to shut down") {
|
|
t.Fatalf("status: %q", b.status)
|
|
}
|
|
if got := powerOf(t, r.sess, r.ref); got != types.VirtualMachinePowerStatePoweredOff {
|
|
t.Fatalf("the machine is %s on the server", got)
|
|
}
|
|
|
|
// Opening the menu again picks the new state up without a full reload.
|
|
b.detailKey(key{special: keyCtrlA})
|
|
if b.current().running() {
|
|
t.Error("the row was not re-read when the menu was opened")
|
|
}
|
|
for _, m := range b.menu {
|
|
switch m.key {
|
|
case 'o':
|
|
if !m.available() {
|
|
t.Errorf("power on is not offered for the machine that has shut down: %s", m.why)
|
|
}
|
|
case 'S':
|
|
if m.available() {
|
|
t.Error("power off is still offered for a machine that has shut down")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A row that cannot be re-read must be reported, not passed over: an operation
|
|
// that worked plus a row that still says "on" would otherwise read as nothing
|
|
// having happened.
|
|
func TestBrowserSaysWhenTheRowCannotBeReread(t *testing.T) {
|
|
b := &browser{
|
|
rows: []vmRow{{vc: VCenter{Name: "v308"}, name: "web01"}}, // no session
|
|
view: []int{0},
|
|
}
|
|
if err := b.refreshRow(); err == nil {
|
|
t.Fatal("re-reading a row with no connection reported success")
|
|
}
|
|
b.done("power off done")
|
|
if !strings.Contains(b.status, "power off done") {
|
|
t.Errorf("the status lost what actually happened: %q", b.status)
|
|
}
|
|
if !strings.Contains(b.status, "could not be re-read") {
|
|
t.Errorf("the status does not say the row is stale: %q", b.status)
|
|
}
|
|
if b.statusCol != colWarn {
|
|
t.Error("a stale row is not flagged as a warning")
|
|
}
|
|
}
|
|
|
|
// A vCenter that does not answer has to be visible in the list that is meant to
|
|
// show every vCenter. It used to be printed and then wiped by the first screen,
|
|
// while the title went on naming the server as though its machines were there.
|
|
func TestSimUnreachableVCenterIsNamed(t *testing.T) {
|
|
quiet(t)
|
|
live := simVCenter(t)
|
|
dead := VCenter{
|
|
Name: "dead", URL: "http://127.0.0.1:9/",
|
|
User: "user", Password: "pass", Datacenter: "DC0",
|
|
}
|
|
|
|
found, err := gatherVMs([]VCenter{live, dead})
|
|
defer closeSessions(found.sessions)
|
|
if err != nil {
|
|
t.Fatalf("one server answering should not be a failure: %v", err)
|
|
}
|
|
if len(found.rows) == 0 {
|
|
t.Fatal("the machines of the server that answered were thrown away")
|
|
}
|
|
if strings.Join(found.answered, ",") != "sim" {
|
|
t.Errorf("answered: %v, want only sim", found.answered)
|
|
}
|
|
if strings.Join(found.lost, ",") != "dead" {
|
|
t.Errorf("lost: %v, want only dead", found.lost)
|
|
}
|
|
if len(found.failed) != 1 || !strings.Contains(found.failed[0], "dead") {
|
|
t.Errorf("the reason does not name the server: %v", found.failed)
|
|
}
|
|
|
|
// The title names one as holding the machines and the other as missing, and
|
|
// does not claim the dead one holds anything.
|
|
b := &browser{targets: []VCenter{live, dead}, rows: found.rows,
|
|
answered: found.answered, lost: found.lost}
|
|
b.applySort()
|
|
|
|
t.Setenv("COLUMNS", "120")
|
|
t.Setenv("LINES", "14")
|
|
frame := stripEscapes(renderToPipe(t, b, b.renderList))
|
|
title := strings.SplitN(frame, "\r\n", 2)[0]
|
|
|
|
if !strings.Contains(title, "on sim") {
|
|
t.Errorf("the title does not say whose machines these are: %q", title)
|
|
}
|
|
if !strings.Contains(title, "dead unreachable") {
|
|
t.Errorf("the title does not say that dead is missing: %q", title)
|
|
}
|
|
if strings.Contains(title, "on dead") || strings.Contains(title, "sim, dead") {
|
|
t.Errorf("the title still claims the list covers dead: %q", title)
|
|
}
|
|
}
|
|
|
|
// Every vCenter failing is a failure of the command; one failing is not.
|
|
func TestSimAllVCentersUnreachable(t *testing.T) {
|
|
quiet(t)
|
|
dead := func(name, port string) VCenter {
|
|
return VCenter{Name: name, URL: "http://127.0.0.1:" + port + "/",
|
|
User: "u", Password: "p", Datacenter: "DC0"}
|
|
}
|
|
found, err := gatherVMs([]VCenter{dead("a", "9"), dead("b", "9")})
|
|
closeSessions(found.sessions)
|
|
if err == nil {
|
|
t.Fatal("no server answered and that was not an error")
|
|
}
|
|
if len(found.failed) != 2 {
|
|
t.Errorf("%d reasons for two failures", len(found.failed))
|
|
}
|
|
if len(found.answered) != 0 {
|
|
t.Errorf("answered: %v", found.answered)
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------- what the new commands read
|
|
|
|
// The sweep brings the snapshots back with the machines, which is what the
|
|
// snapshot column, the issues filter and the age report are all made of.
|
|
func TestSimSweepCarriesTheSnapshots(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
if err := snapshotNow(s, r.ref, "sweep-test", "from the tests"); err != nil {
|
|
t.Fatalf("cannot take a snapshot: %v", err)
|
|
}
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
defer closeSessions(found.sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
|
|
for _, row := range found.rows {
|
|
if row.name != "DC0_C0_RP0_VM0" {
|
|
continue
|
|
}
|
|
if row.snapCount() != 1 {
|
|
t.Fatalf("the sweep found %d snapshots, want 1", row.snapCount())
|
|
}
|
|
if row.snapCell() != "1" {
|
|
t.Errorf("the column shows %q", row.snapCell())
|
|
}
|
|
e, ok := row.oldest()
|
|
if !ok {
|
|
t.Fatal("the snapshot came back without a date")
|
|
}
|
|
if e.name != "sweep-test" {
|
|
t.Errorf("the oldest snapshot is %q", e.name)
|
|
}
|
|
// Taken a moment ago, so it is neither stale nor an issue.
|
|
if days := e.days(); days != 0 {
|
|
t.Errorf("a snapshot taken just now is %d days old", days)
|
|
}
|
|
if row.hasIssues() && strings.Contains(strings.Join(row.issues(), " "), "snapshot") {
|
|
t.Errorf("a fresh snapshot is held against the machine: %v", row.issues())
|
|
}
|
|
return
|
|
}
|
|
t.Fatal("the machine was not in the sweep")
|
|
}
|
|
|
|
// The age report, end to end: a snapshot taken now is not old, and the same
|
|
// report with an age of nothing finds it.
|
|
func TestSimSnapshotAgeReport(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM1")
|
|
if err := snapshotNow(s, r.ref, "ancient", "from the tests"); err != nil {
|
|
t.Fatalf("cannot take a snapshot: %v", err)
|
|
}
|
|
|
|
// Nothing is a month old on a vCenter that was created a second ago — and
|
|
// where somebody is reading, the report says so rather than printing an
|
|
// empty table.
|
|
onATerminal(t)
|
|
out := captureStdout(t, func() {
|
|
if err := snapOldReport(Config{}, []VCenter{vc}, snapOldDays, false); err != nil {
|
|
t.Fatalf("snapOldReport: %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(stripEscapes(out), "no snapshot") {
|
|
t.Errorf("the report of old snapshots says:\n%s", out)
|
|
}
|
|
|
|
// Everything is nought days old, so an age of nought finds it.
|
|
out = captureStdout(t, func() {
|
|
if err := snapOldReport(Config{}, []VCenter{vc}, 0, false); err != nil {
|
|
t.Fatalf("snapOldReport: %v", err)
|
|
}
|
|
})
|
|
for _, want := range []string{"ancient", "DC0_C0_RP0_VM1", "0d"} {
|
|
if !strings.Contains(stripEscapes(out), want) {
|
|
t.Errorf("the report leaves out %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A mailed report with no relay configured is refused before the sweep, not
|
|
// after it: finding out that the mail cannot be sent is of no use once the
|
|
// report has been printed.
|
|
func TestSimSnapshotReportChecksTheMailFirst(t *testing.T) {
|
|
quiet(t)
|
|
err := snapOldReport(Config{}, []VCenter{{Name: "nowhere", URL: "https://127.0.0.1:1/",
|
|
User: "u", Password: "p", Datacenter: "DC0"}}, 30, true)
|
|
if err == nil || !strings.Contains(err.Error(), "cannot send mail") {
|
|
t.Errorf("a report with -m and no relay failed with: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSimDatastores(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
out := captureStdout(t, func() {
|
|
if err := dsstat(vc, ""); err != nil {
|
|
t.Fatalf("dsstat: %v", err)
|
|
}
|
|
})
|
|
plain := stripEscapes(out)
|
|
for _, want := range []string{"DATASTORE", "CAPACITY", "USED%", "LocalDS_0", "datastore"} {
|
|
if !strings.Contains(plain, want) {
|
|
t.Errorf("the datastore table leaves out %q:\n%s", want, plain)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The printed listing as a document: what a monitoring check would read.
|
|
func TestSimJSONListing(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
out := captureStdout(t, func() {
|
|
if err := lsvm([]VCenter{vc}, lsOptions{json: true}); err != nil {
|
|
t.Fatalf("lsvm --json: %v", err)
|
|
}
|
|
})
|
|
|
|
var doc struct {
|
|
Answered []string `json:"answered"`
|
|
Failed []string `json:"failed"`
|
|
Count int `json:"count"`
|
|
Machines []struct {
|
|
Name string `json:"name"`
|
|
VCenter string `json:"vcenter"`
|
|
Moref string `json:"moref"`
|
|
Power string `json:"power"`
|
|
} `json:"machines"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &doc); err != nil {
|
|
t.Fatalf("the listing is not JSON: %v\n%s", err, out)
|
|
}
|
|
if len(doc.Answered) != 1 || doc.Answered[0] != "sim" {
|
|
t.Errorf("answered = %v", doc.Answered)
|
|
}
|
|
if doc.Count != len(doc.Machines) || doc.Count == 0 {
|
|
t.Errorf("count = %d, machines = %d", doc.Count, len(doc.Machines))
|
|
}
|
|
for _, m := range doc.Machines {
|
|
if m.Name == "" || m.Moref == "" || m.VCenter != "sim" || m.Power == "" {
|
|
t.Errorf("a machine came out as %+v", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A vCenter that does not answer is in the document rather than printed into
|
|
// the middle of it, where it would break whatever is reading it.
|
|
func TestSimJSONKeepsTheProseOut(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
dead := VCenter{Name: "dead", URL: "https://127.0.0.1:1/", User: "u", Password: "p", Datacenter: "DC0"}
|
|
|
|
out := captureStdout(t, func() {
|
|
if err := lsvm([]VCenter{vc, dead}, lsOptions{json: true}); err != nil {
|
|
t.Fatalf("lsvm --json: %v", err)
|
|
}
|
|
})
|
|
|
|
var doc struct {
|
|
Answered []string `json:"answered"`
|
|
Failed []string `json:"failed"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &doc); err != nil {
|
|
t.Fatalf("a failed server made the document unreadable: %v\n%s", err, out)
|
|
}
|
|
if len(doc.Failed) != 1 || !strings.Contains(doc.Failed[0], "dead") {
|
|
t.Errorf("failed = %v", doc.Failed)
|
|
}
|
|
if strings.Contains(out, "ERROR") {
|
|
t.Errorf("the failure was printed as prose as well:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// The issues listing runs against a real inventory. The simulated machines have
|
|
// no VMware Tools, which is exactly the kind of thing it is for.
|
|
func TestSimIssuesListing(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
out := captureStdout(t, func() {
|
|
if err := lsvm([]VCenter{vc}, lsOptions{issues: true}); err != nil {
|
|
t.Fatalf("lsvm --issues: %v", err)
|
|
}
|
|
})
|
|
plain := stripEscapes(out)
|
|
if !strings.Contains(plain, "WHY") {
|
|
t.Errorf("the issues listing has no reason column:\n%s", plain)
|
|
}
|
|
if !strings.Contains(plain, "VMware Tools") {
|
|
t.Errorf("the running machines without Tools were not reported:\n%s", plain)
|
|
}
|
|
}
|
|
|
|
// The machine's own events, which is what 'e' on a machine's sheet fetches.
|
|
func TestSimEventsOfOneMachine(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
// Something to find: a snapshot leaves events behind it.
|
|
if err := snapshotNow(s, r.ref, "for-the-log", "from the tests"); err != nil {
|
|
t.Fatalf("cannot take a snapshot: %v", err)
|
|
}
|
|
|
|
lines, err := eventsOf(r)
|
|
if err != nil {
|
|
t.Fatalf("eventsOf: %v", err)
|
|
}
|
|
for _, l := range lines {
|
|
if strings.TrimSpace(l.text) == "" {
|
|
t.Error("an event came back with nothing in it")
|
|
}
|
|
if strings.ContainsAny(l.text, "\n\r") {
|
|
t.Errorf("an event carries a newline, which would break the sheet: %q", l.text)
|
|
}
|
|
}
|
|
|
|
// And they go on the sheet under one label, in their own colours.
|
|
sheet := eventSheet(lines)
|
|
if len(sheet) != len(lines) {
|
|
t.Errorf("the sheet holds %d of %d events", len(sheet), len(lines))
|
|
}
|
|
if len(sheet) > 0 && sheet[0].label != "events" {
|
|
t.Errorf("the first event line is labelled %q", sheet[0].label)
|
|
}
|
|
for _, l := range sheet[1:] {
|
|
if l.label != "" {
|
|
t.Errorf("an event line carries a second label: %q", l.label)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A machine read over a connection that has gone cannot be asked for anything,
|
|
// and says so rather than looking empty.
|
|
func TestSimEventsWithoutAConnection(t *testing.T) {
|
|
r := testRow("web01", true, "10.0.0.5")
|
|
if _, err := eventsOf(r); err == nil {
|
|
t.Error("a row with no session read its events anyway")
|
|
}
|
|
}
|
|
|
|
// Making a machine from a template, against a server that answers: the source
|
|
// has to be a template, the placement is worked out rather than asked for, and
|
|
// the machine that comes out is a machine and not another template.
|
|
func TestSimDeployFromATemplate(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
|
|
// An ordinary machine is refused before anything is sent: gvm copies
|
|
// templates, and a clone of a running machine is a different operation with
|
|
// different consequences.
|
|
src, err := sourceOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sourceOf: %v", err)
|
|
}
|
|
if src.template {
|
|
t.Fatal("the simulator handed back a template where a machine was asked for")
|
|
}
|
|
if _, err := startDeploy(s, src, deployTarget{}, "copy01", deployOpts{}); err == nil {
|
|
t.Error("a machine was copied as though it were a template")
|
|
}
|
|
|
|
// Make it one. A template has no resource pool of its own from here on,
|
|
// which is the whole reason the placement has to be worked out.
|
|
vm := object.NewVirtualMachine(s.client.Client, r.ref)
|
|
if _, err := runPower(s, r, opPowerOff); err != nil {
|
|
t.Fatalf("cannot stop the machine to template it: %v", err)
|
|
}
|
|
if err := vm.MarkAsTemplate(s.ctx); err != nil {
|
|
t.Fatalf("cannot mark it as a template: %v", err)
|
|
}
|
|
|
|
src, err = sourceOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sourceOf after templating: %v", err)
|
|
}
|
|
if !src.template {
|
|
t.Fatal("a machine marked as a template does not read as one")
|
|
}
|
|
|
|
target, err := targetFor(s, src, deployOpts{})
|
|
if err != nil {
|
|
t.Fatalf("targetFor: %v", err)
|
|
}
|
|
if target.where == "" {
|
|
t.Error("the placement has nothing to say where it would run")
|
|
}
|
|
if target.pool.Value == "" {
|
|
t.Error("no resource pool was worked out, so nothing could run")
|
|
}
|
|
|
|
// A name that is already taken is refused before the clone starts, rather
|
|
// than several seconds in by vCenter.
|
|
if _, err := startDeploy(s, src, target, "DC0_C0_RP0_VM1", deployOpts{}); err == nil {
|
|
t.Error("a name that is already in use was accepted")
|
|
}
|
|
|
|
task, err := startDeploy(s, src, target, "made-from-template", deployOpts{})
|
|
if err != nil {
|
|
t.Fatalf("startDeploy: %v", err)
|
|
}
|
|
if err := waitTask(s.ctx, task, cloneWait, "making it"); err != nil {
|
|
t.Fatalf("the clone did not finish: %v", err)
|
|
}
|
|
|
|
// It exists, it is a machine rather than a template, and it is where it was
|
|
// said it would be.
|
|
made, err := s.vm("made-from-template")
|
|
if err != nil {
|
|
t.Fatalf("the new machine cannot be found: %v", err)
|
|
}
|
|
var mvm mo.VirtualMachine
|
|
if err := made.Properties(s.ctx, made.Reference(), []string{"summary", "resourcePool"}, &mvm); err != nil {
|
|
t.Fatalf("cannot read what was made: %v", err)
|
|
}
|
|
if mvm.Summary.Config.Template {
|
|
t.Error("what came out is another template, not a machine")
|
|
}
|
|
if mvm.ResourcePool == nil || *mvm.ResourcePool != target.pool {
|
|
t.Errorf("it landed in %v, not in the pool it was given (%v)", mvm.ResourcePool, target.pool)
|
|
}
|
|
if mvm.Summary.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff {
|
|
t.Errorf("it was started, though nothing asked for that: %s", mvm.Summary.Runtime.PowerState)
|
|
}
|
|
}
|
|
|
|
// Telling the guest what it is, against a server that holds real customisation
|
|
// specifications: gvm writes the two facts that are about this one machine into
|
|
// the one the vCenter keeps, and leaves the site's answers alone.
|
|
func TestSimCustomisationFromAVCenterSpec(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
|
|
names, err := specNames(s)
|
|
if err != nil {
|
|
t.Fatalf("specNames: %v", err)
|
|
}
|
|
if !contains(names, "vcsim-linux-static") || !contains(names, "vcsim-windows-static") {
|
|
t.Fatalf("the simulator's specifications are not what this test is written against: %v", names)
|
|
}
|
|
|
|
// A Linux specification with a netmask in it: both facts go in, and the
|
|
// netmask and gateway that came with it stay.
|
|
spec, err := customizationFor(s, deployOpts{spec: "vcsim-linux-static", ip: "10.0.0.55"}, deploySource{name: "tpl"}, "web05")
|
|
if err != nil {
|
|
t.Fatalf("customizationFor: %v", err)
|
|
}
|
|
if got := hostNameOf(spec); got != "web05" {
|
|
t.Errorf("the hostname is %q", got)
|
|
}
|
|
if got := addressOf(spec); !strings.Contains(got, "10.0.0.55") || !strings.Contains(got, "255.255.255.0") {
|
|
t.Errorf("the address line is %q", got)
|
|
}
|
|
|
|
// The machine's own name is the hostname unless something else is said.
|
|
spec, err = customizationFor(s, deployOpts{spec: "vcsim-linux-static", hostname: "web05.fhi"}, deploySource{name: "tpl"}, "web05")
|
|
if err != nil {
|
|
t.Fatalf("customizationFor with a hostname: %v", err)
|
|
}
|
|
if got := hostNameOf(spec); got != "web05.fhi" {
|
|
t.Errorf("--hostname was ignored: %q", got)
|
|
}
|
|
|
|
// Windows has its computer name somewhere else, and it is found there.
|
|
spec, err = customizationFor(s, deployOpts{spec: "vcsim-windows-static"}, deploySource{name: "tpl"}, "WEB05")
|
|
if err != nil {
|
|
t.Fatalf("a Windows specification was refused: %v", err)
|
|
}
|
|
if got := hostNameOf(spec); got != "WEB05" {
|
|
t.Errorf("the Windows computer name is %q", got)
|
|
}
|
|
|
|
// The one whose adapter takes its address from DHCP has no netmask to give
|
|
// a fixed address, and says so rather than making one up.
|
|
if _, err := customizationFor(s, deployOpts{spec: "vcsim-linux", ip: "10.0.0.55"}, deploySource{name: "tpl"}, "web05"); err == nil {
|
|
t.Error("an address was written into a DHCP specification")
|
|
}
|
|
// Without an address that same specification is perfectly usable.
|
|
if _, err := customizationFor(s, deployOpts{spec: "vcsim-linux"}, deploySource{name: "tpl"}, "web05"); err != nil {
|
|
t.Errorf("a DHCP specification was refused with no address asked for: %v", err)
|
|
}
|
|
|
|
// A name that is not there says what is.
|
|
_, err = customizationFor(s, deployOpts{spec: "no-such-spec"}, deploySource{name: "tpl"}, "web05")
|
|
if err == nil {
|
|
t.Fatal("a specification that does not exist was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "vcsim-linux-static") {
|
|
t.Errorf("it did not say what there is: %v", err)
|
|
}
|
|
|
|
// And the whole way through: a template, deployed with a customisation.
|
|
vm := object.NewVirtualMachine(s.client.Client, r.ref)
|
|
if _, err := runPower(s, r, opPowerOff); err != nil {
|
|
t.Fatalf("cannot stop the machine: %v", err)
|
|
}
|
|
if err := vm.MarkAsTemplate(s.ctx); err != nil {
|
|
t.Fatalf("cannot mark it as a template: %v", err)
|
|
}
|
|
src, err := sourceOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sourceOf: %v", err)
|
|
}
|
|
target, err := targetFor(s, src, deployOpts{})
|
|
if err != nil {
|
|
t.Fatalf("targetFor: %v", err)
|
|
}
|
|
|
|
opts := deployOpts{spec: "vcsim-linux-static", ip: "10.0.0.56"}
|
|
task, err := startDeploy(s, src, target, "customised01", opts)
|
|
if err != nil {
|
|
t.Fatalf("startDeploy: %v", err)
|
|
}
|
|
if err := waitTask(s.ctx, task, cloneWait, "making it"); err != nil {
|
|
t.Fatalf("the clone did not finish: %v", err)
|
|
}
|
|
if _, err := s.vm("customised01"); err != nil {
|
|
t.Errorf("the customised machine cannot be found: %v", err)
|
|
}
|
|
}
|
|
|
|
// The estate screen against a server that answers: the hosts come back grouped
|
|
// under their clusters, what they carry is added up from the rows the list
|
|
// already holds, and Enter narrows the list to the host under the cursor.
|
|
func TestSimEstateScreen(t *testing.T) {
|
|
quiet(t)
|
|
t.Setenv("COLUMNS", "150")
|
|
t.Setenv("LINES", "20")
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
defer closeSessions(found.sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
b := &browser{targets: []VCenter{vc}, rows: found.rows, sessions: found.sessions,
|
|
answered: found.answered}
|
|
b.applySort()
|
|
|
|
b.openEstate()
|
|
if b.estate == nil {
|
|
t.Fatalf("the estate screen did not open: %q", b.status)
|
|
}
|
|
e := b.estate
|
|
|
|
// Every host of the simulated inventory, under a heading each, and the
|
|
// cursor on a host rather than on a heading.
|
|
var hosts, headings int
|
|
for _, r := range e.rows {
|
|
if r.isHeading() {
|
|
headings++
|
|
continue
|
|
}
|
|
hosts++
|
|
}
|
|
if hosts == 0 || headings == 0 {
|
|
t.Fatalf("%d hosts under %d headings", hosts, headings)
|
|
}
|
|
if e.rows[e.sel].isHeading() {
|
|
t.Error("the cursor started on a heading")
|
|
}
|
|
|
|
// The machines are charged to the hosts they are on, and the total matches
|
|
// what the list holds — nothing counted twice, nothing dropped.
|
|
charged := 0
|
|
for _, r := range e.rows {
|
|
if !r.isHeading() {
|
|
charged += r.vms
|
|
}
|
|
}
|
|
placed := 0
|
|
for _, r := range b.rows {
|
|
if r.vm.Summary.Runtime.Host != nil {
|
|
placed++
|
|
}
|
|
}
|
|
if charged != placed {
|
|
t.Errorf("%d machines charged to hosts, %d placed on one", charged, placed)
|
|
}
|
|
|
|
// A heading is the sum of the hosts under it.
|
|
for i, r := range e.rows {
|
|
if !r.isHeading() {
|
|
continue
|
|
}
|
|
sum := 0
|
|
for _, h := range e.rows[i+1:] {
|
|
if h.isHeading() {
|
|
break
|
|
}
|
|
sum += h.vms
|
|
}
|
|
if r.vms != sum {
|
|
t.Errorf("%q says %d machines, its hosts hold %d", r.heading, r.vms, sum)
|
|
}
|
|
}
|
|
|
|
// It draws, with the figures on it.
|
|
frame := stripEscapes(renderToPipe(t, b, b.renderEstate))
|
|
for _, want := range []string{"Estate", "CLUSTER / HOST", "MEM ALLOC", "DC0_C0"} {
|
|
if !strings.Contains(frame, want) {
|
|
t.Errorf("the screen does not show %q:\n%s", want, frame)
|
|
}
|
|
}
|
|
|
|
// And Enter is the whole point of arrowing to a host: the machine list,
|
|
// narrowed to it.
|
|
//
|
|
// Arrowed to one that carries something, deliberately: the simulator does
|
|
// not spread its machines evenly and leaves some hosts empty, so starting
|
|
// from wherever the cursor happens to open would be a test that passes or
|
|
// fails by placement rather than by behaviour. (It did: one run in three.)
|
|
for i, r := range e.rows {
|
|
if !r.isHeading() && r.vms > 0 {
|
|
e.sel = i
|
|
break
|
|
}
|
|
}
|
|
host := e.rows[e.sel].host
|
|
if e.rows[e.sel].vms == 0 {
|
|
t.Fatal("no host in the simulated inventory carries a machine")
|
|
}
|
|
b.showHost()
|
|
if b.estate != nil {
|
|
t.Error("the estate screen stayed open after Enter")
|
|
}
|
|
if b.filter != host {
|
|
t.Errorf("the filter is %q, want the host %q", b.filter, host)
|
|
}
|
|
if len(b.view) == 0 {
|
|
t.Errorf("filtering to %s left no machines, though it carries some", host)
|
|
}
|
|
for _, i := range b.view {
|
|
if b.rows[i].host != host {
|
|
t.Errorf("%s is on %s, not on %s", b.rows[i].name, b.rows[i].host, host)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Live mode against a server that answers: the refresh goes over the connection
|
|
// that is already open, it notices what happened behind gvm's back, and it says
|
|
// what changed.
|
|
func TestSimLiveRefreshUsesTheOpenSession(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
rows, sessions := found.rows, found.sessions
|
|
defer closeSessions(sessions)
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
|
|
b := &browser{targets: []VCenter{vc}, rows: rows, sessions: sessions,
|
|
answered: found.answered}
|
|
b.applySort()
|
|
b.sample()
|
|
|
|
// The refresh reads the same machines back, and does not touch the
|
|
// sessions: a reload replaces them, this must not.
|
|
before := len(b.rows)
|
|
was := b.rows
|
|
if err := b.resweep(); err != nil {
|
|
t.Fatalf("resweep: %v", err)
|
|
}
|
|
if len(b.rows) != before {
|
|
t.Errorf("the refresh came back with %d of %d machines", len(b.rows), before)
|
|
}
|
|
if len(b.sessions) != len(sessions) || b.sessions[0] != sessions[0] {
|
|
t.Error("the refresh replaced the session it was supposed to reuse")
|
|
}
|
|
if got := changesBetween(was, b.rows); got != "" {
|
|
t.Errorf("a refresh with nothing happening in between said %q", got)
|
|
}
|
|
|
|
// Something happens that gvm did not do. A refresh has to notice, and say
|
|
// so — this is the whole reason to leave the list open.
|
|
var victim vmRow
|
|
for _, r := range b.rows {
|
|
if r.running() {
|
|
victim = r
|
|
break
|
|
}
|
|
}
|
|
if victim.name == "" {
|
|
t.Fatal("no running machine to stop")
|
|
}
|
|
if _, err := runPower(victim.sess, victim, opPowerOff); err != nil {
|
|
t.Fatalf("cannot stop %s: %v", victim.name, err)
|
|
}
|
|
|
|
was = b.rows
|
|
if err := b.resweep(); err != nil {
|
|
t.Fatalf("resweep after the change: %v", err)
|
|
}
|
|
said := changesBetween(was, b.rows)
|
|
if !strings.Contains(said, victim.name+" off") {
|
|
t.Errorf("the refresh did not report the machine stopping: %q", said)
|
|
}
|
|
for _, r := range b.rows {
|
|
if r.name == victim.name && r.running() {
|
|
t.Error("the refreshed row still says the machine is running")
|
|
}
|
|
}
|
|
|
|
// One server of two failing moves the title as well as the status line: the
|
|
// rows of the one that did not answer are kept, and it is named as lost
|
|
// rather than left in the list of servers holding machines. A title reading
|
|
// "on v308, v309" above a status line saying v309 did not answer is two
|
|
// lines contradicting each other, and the next keystroke clears the true
|
|
// one.
|
|
dead, stop := context.WithCancel(context.Background())
|
|
stop()
|
|
b.sessions = append(b.sessions, &session{vc: VCenter{Name: "v309"}, ctx: dead,
|
|
client: sessions[0].client, cancel: stop})
|
|
b.answered, b.lost = []string{vc.Name, "v309"}, nil
|
|
|
|
if err := b.resweep(); err == nil {
|
|
t.Error("a refresh over a dead session reported no trouble")
|
|
}
|
|
if contains(b.answered, "v309") {
|
|
t.Errorf("v309 did not answer and is still named as holding machines: %v", b.answered)
|
|
}
|
|
if !contains(b.lost, "v309") {
|
|
t.Errorf("v309 is not named as lost: %v", b.lost)
|
|
}
|
|
if !contains(b.answered, vc.Name) {
|
|
t.Errorf("%s answered and is not named: %v", vc.Name, b.answered)
|
|
}
|
|
b.sessions = b.sessions[:len(b.sessions)-1]
|
|
|
|
// And it really is the open session it reads over: with that closed, the
|
|
// refresh fails instead of quietly logging in again. Three logins a minute
|
|
// is what live mode exists not to do.
|
|
closeSessions(sessions)
|
|
if err := b.resweep(); err == nil {
|
|
t.Error("the refresh worked with every session closed, so it logged in again")
|
|
}
|
|
// The rows of a server that stopped answering are kept rather than dropped:
|
|
// an empty list because a vCenter is restarting would be worse than one
|
|
// that is a minute old.
|
|
if len(b.rows) != before {
|
|
t.Errorf("a failed refresh left %d of %d machines on screen", len(b.rows), before)
|
|
}
|
|
}
|
|
|
|
// Changing what a machine has, against a server that answers: the refusal on a
|
|
// running machine, and the change itself once it is off. The numbers are read
|
|
// back from the vCenter afterwards rather than assumed — a reconfigure that is
|
|
// accepted and does nothing would otherwise look exactly like one that worked.
|
|
func TestSimResizeAMachine(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
sz, err := sizingOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sizingOf: %v", err)
|
|
}
|
|
if !sz.known {
|
|
t.Fatal("the machine's configuration came back empty")
|
|
}
|
|
|
|
// The simulator's machines run, and are built without hot-plug — which is
|
|
// the case the objection exists for.
|
|
if !r.running() {
|
|
t.Fatal("the simulated machine is not running, so there is nothing to refuse")
|
|
}
|
|
if err := checkSize(r, sz, sizeCPUs, sz.cpus+2); err == nil {
|
|
t.Error("more vCPUs were allowed on a running machine with no hot-add")
|
|
}
|
|
if err := checkSize(r, sz, sizeMemory, sz.memoryMB*2); err == nil {
|
|
t.Error("more memory was allowed on a running machine with no hot-add")
|
|
}
|
|
|
|
// Off it goes, and with it the objection.
|
|
if _, err := runPower(s, r, opPowerOff); err != nil {
|
|
t.Fatalf("cannot power the machine off: %v", err)
|
|
}
|
|
if err := refreshOne(s, &r); err != nil {
|
|
t.Fatalf("cannot re-read the machine: %v", err)
|
|
}
|
|
|
|
wantCPUs, wantMemory := sz.cpus+2, sz.memoryMB+1024
|
|
for _, c := range []struct {
|
|
kind sizeKind
|
|
want int32
|
|
}{{sizeCPUs, wantCPUs}, {sizeMemory, wantMemory}} {
|
|
// Read again between the two, so the second change is checked and
|
|
// described against what the first one left behind rather than against
|
|
// what the machine looked like before either.
|
|
sz, err = sizingOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sizingOf: %v", err)
|
|
}
|
|
msg, err := runResize(s, r, sz, c.kind, c.want)
|
|
if err != nil {
|
|
t.Fatalf("runResize(%v): %v", c.kind.what(), err)
|
|
}
|
|
if !strings.Contains(msg, "→") {
|
|
t.Errorf("the message does not say what changed: %q", msg)
|
|
}
|
|
}
|
|
|
|
after, err := sizingOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sizingOf after the change: %v", err)
|
|
}
|
|
if after.cpus != wantCPUs {
|
|
t.Errorf("the machine has %d vCPUs, want %d", after.cpus, wantCPUs)
|
|
}
|
|
if after.memoryMB != wantMemory {
|
|
t.Errorf("the machine has %d MB, want %d", after.memoryMB, wantMemory)
|
|
}
|
|
|
|
// And the change that would take memory away from it while it runs is still
|
|
// refused after it has been given some.
|
|
r.vm.Summary.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn
|
|
if err := checkSize(r, after, sizeMemory, after.memoryMB-1024); err == nil {
|
|
t.Error("memory was taken away from a running machine")
|
|
}
|
|
}
|
|
|
|
// A command line that asks for two changes does neither until both are known to
|
|
// be possible: setting the vCPUs and then refusing the memory would leave half
|
|
// of what somebody asked for, which is the outcome nobody wanted.
|
|
func TestSimResizeRefusesBothOrNeither(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM1")
|
|
if _, err := runPower(s, r, opPowerOff); err != nil {
|
|
t.Fatalf("cannot power the machine off: %v", err)
|
|
}
|
|
before, err := sizingOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sizingOf: %v", err)
|
|
}
|
|
|
|
// The vCPU count is good and the memory is not a multiple of 4 MB. -y so
|
|
// that a confirmation cannot be what stops it: the refusal has to come from
|
|
// the check, before anything is sent.
|
|
err = sizeCLI(vc, "DC0_C0_RP0_VM1", Itoa(int(before.cpus+2)), "1026m", true)
|
|
if err == nil {
|
|
t.Fatal("a half-impossible command line was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "multiples of 4 MB") {
|
|
t.Errorf("refused for the wrong reason: %v", err)
|
|
}
|
|
|
|
after, err := sizingOf(s, r.ref)
|
|
if err != nil {
|
|
t.Fatalf("sizingOf after the refusal: %v", err)
|
|
}
|
|
if after.cpus != before.cpus {
|
|
t.Errorf("the vCPUs were changed anyway: %d, was %d", after.cpus, before.cpus)
|
|
}
|
|
if after.memoryMB != before.memoryMB {
|
|
t.Errorf("the memory was changed anyway: %d, was %d", after.memoryMB, before.memoryMB)
|
|
}
|
|
}
|
|
|
|
// refreshOne re-reads the properties the sweep reads, for a row a test has just
|
|
// changed something about.
|
|
func refreshOne(s *session, r *vmRow) error {
|
|
var fresh mo.VirtualMachine
|
|
vm := object.NewVirtualMachine(s.client.Client, r.ref)
|
|
if err := vm.Properties(s.ctx, r.ref, sweepProps, &fresh); err != nil {
|
|
return err
|
|
}
|
|
r.vm = fresh
|
|
return nil
|
|
}
|
|
|
|
// The instance UUID is what the vSphere client's links are made of, and the one
|
|
// thing gvm cannot work out from the configuration.
|
|
func TestSimVsphereLinkFromASession(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
_, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
url := vsphereURL(r)
|
|
if url == "" {
|
|
t.Fatal("no link was built from a live connection")
|
|
}
|
|
if !strings.Contains(url, "urn:vmomi:VirtualMachine:"+r.ref.Value+":") {
|
|
t.Errorf("the link does not name the machine: %s", url)
|
|
}
|
|
if !strings.HasSuffix(url, "/summary") {
|
|
t.Errorf("the link does not end at the machine's page: %s", url)
|
|
}
|
|
}
|
|
|
|
// -v takes several servers for the sweeps, and the same server twice is still
|
|
// one login.
|
|
func TestMultipleVCentersInOneSweep(t *testing.T) {
|
|
cfg := Config{VCenters: []VCenter{
|
|
{Name: "v308", URL: "https://a/", User: "u", Password: "p", Datacenter: "DC"},
|
|
{Name: "v108", URL: "https://b/", User: "u", Password: "p", Datacenter: "DC"},
|
|
{Name: "v38", URL: "https://c/", User: "u", Password: "p", Datacenter: "DC"},
|
|
}}
|
|
|
|
got, err := cfg.targets("v308,v38")
|
|
if err != nil {
|
|
t.Fatalf("targets: %v", err)
|
|
}
|
|
if len(got) != 2 || got[0].Name != "v308" || got[1].Name != "v38" {
|
|
t.Errorf("-v v308,v38 gave %v", vcNames(got))
|
|
}
|
|
|
|
// The order given is the order used, and a name given twice is one server.
|
|
got, _ = cfg.targets("v38, v308 ,v38")
|
|
if len(got) != 2 || got[0].Name != "v38" || got[1].Name != "v308" {
|
|
t.Errorf("-v with a repeat gave %v", vcNames(got))
|
|
}
|
|
|
|
// An unknown name among them is an error, not a shorter list: a sweep that
|
|
// quietly left a server out would report a cluster that is not there.
|
|
if _, err := cfg.targets("v308,nowhere"); err == nil {
|
|
t.Error("an unknown server in the list was skipped")
|
|
}
|
|
|
|
// And the commands that act on one machine refuse a list outright.
|
|
if _, err := cfg.pick("v308,v38"); err == nil {
|
|
t.Error("a single-server command took a list of servers")
|
|
} else if !strings.Contains(err.Error(), "one vCenter at a time") {
|
|
t.Errorf("it refused with: %v", err)
|
|
}
|
|
}
|
|
|
|
// The task path itself: that the references off a machine can be read back as
|
|
// tasks at all. runningTasks swallows a failure here on purpose — a table is
|
|
// worth having without the column — so a broken read would otherwise look
|
|
// exactly like a quiet cluster, on every vCenter, for ever.
|
|
func TestSimTasksAreReadable(t *testing.T) {
|
|
quiet(t)
|
|
vc := simVCenter(t)
|
|
|
|
s, r := oneRow(t, vc, "DC0_C0_RP0_VM0")
|
|
if err := snapshotNow(s, r.ref, "leaves-a-task", "from the tests"); err != nil {
|
|
t.Fatalf("cannot take a snapshot: %v", err)
|
|
}
|
|
|
|
var fresh mo.VirtualMachine
|
|
if err := object.NewVirtualMachine(s.client.Client, r.ref).
|
|
Properties(s.ctx, r.ref, []string{"recentTask"}, &fresh); err != nil {
|
|
t.Fatalf("cannot read recentTask: %v", err)
|
|
}
|
|
// A re-read machine has to know which machine it is: runningTasks keys what
|
|
// it finds by the machine's own reference, and refreshRow looks its answer
|
|
// up by the reference of the row. A property read that did not fill that in
|
|
// would leave the task column empty on exactly the row that was just acted
|
|
// on, and nowhere else.
|
|
if fresh.Reference() != r.ref {
|
|
t.Fatalf("a re-read machine came back as %v, not %v", fresh.Reference(), r.ref)
|
|
}
|
|
if len(fresh.RecentTask) == 0 {
|
|
t.Skip("this vCenter keeps no recent tasks on the machine")
|
|
}
|
|
|
|
tasks, err := s.tasks(fresh.RecentTask)
|
|
if err != nil {
|
|
t.Fatalf("the task references could not be read: %v", err)
|
|
}
|
|
if len(tasks) == 0 {
|
|
t.Fatal("no task came back for a machine that has just had one")
|
|
}
|
|
for _, task := range tasks {
|
|
if task.Info.DescriptionId == "" {
|
|
t.Error("a task came back with nothing to call it")
|
|
}
|
|
}
|
|
|
|
// And a task that has finished is not something being done to the machine.
|
|
if busy := runningTasks(s, []mo.VirtualMachine{fresh}); len(busy) != 0 {
|
|
t.Errorf("a finished snapshot is still reported as going on: %v", busy)
|
|
}
|
|
}
|
|
|
|
// The issues listing is the one that belongs in cron, so with nothing to report
|
|
// it prints nothing at all — cron mails whatever a command prints, and a daily
|
|
// "nothing wrong" is a daily mail nobody reads. On a terminal it says so.
|
|
func TestSimIssuesListingIsQuietUnderCron(t *testing.T) {
|
|
quiet(t)
|
|
vc, model := simVCenterModel(t)
|
|
|
|
// A healthy inventory: the simulated machines are only in the issues list
|
|
// because they run without VMware Tools, so switching Tools on for all of
|
|
// them leaves nothing to report.
|
|
found, err := gatherVMs([]VCenter{vc})
|
|
if err != nil {
|
|
t.Fatalf("gatherVMs: %v", err)
|
|
}
|
|
for _, r := range found.rows {
|
|
startTools(t, model, r.ref)
|
|
}
|
|
closeSessions(found.sessions)
|
|
|
|
out := captureStdout(t, func() {
|
|
if err := lsvm([]VCenter{vc}, lsOptions{issues: true}); err != nil {
|
|
t.Fatalf("lsvm --issues: %v", err)
|
|
}
|
|
})
|
|
if out != "" {
|
|
t.Errorf("a clean estate printed this into a pipe:\n%s", out)
|
|
}
|
|
|
|
onATerminal(t)
|
|
out = captureStdout(t, func() {
|
|
if err := lsvm([]VCenter{vc}, lsOptions{issues: true}); err != nil {
|
|
t.Fatalf("lsvm --issues: %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(stripEscapes(out), "nothing to report") {
|
|
t.Errorf("on a terminal a clean estate printed %q", out)
|
|
}
|
|
}
|