From c339fbfb39414a71b44efcdcfe3c5ff3aa84120b Mon Sep 17 00:00:00 2001 From: Andrey Tonkikh Date: Wed, 25 Oct 2017 23:20:59 +0300 Subject: [PATCH] Add some acceptance tests for `VirtualMachine` and `Datastore` --- builder_acc_test.go | 153 +++++---------------------- driver/test.sh | 5 + driver/testing/config.go | 19 ++++ driver/testing/datastore_acc_test.go | 30 ++++++ driver/testing/utility.go | 142 +++++++++++++++++++++++++ driver/testing/vm_acc_test.go | 131 +++++++++++++++++++++++ driver/vm.go | 9 +- test.sh | 2 +- 8 files changed, 356 insertions(+), 135 deletions(-) create mode 100755 driver/test.sh create mode 100644 driver/testing/config.go create mode 100644 driver/testing/datastore_acc_test.go create mode 100644 driver/testing/utility.go create mode 100644 driver/testing/vm_acc_test.go diff --git a/builder_acc_test.go b/builder_acc_test.go index faa660ade..ab51d4ade 100644 --- a/builder_acc_test.go +++ b/builder_acc_test.go @@ -3,11 +3,10 @@ package main import ( "testing" builderT "github.com/hashicorp/packer/helper/builder/testing" - "fmt" "github.com/hashicorp/packer/packer" "encoding/json" - "math/rand" "github.com/jetbrains-infra/packer-builder-vsphere/driver" + driverT "github.com/jetbrains-infra/packer-builder-vsphere/driver/testing" ) func TestBuilderAcc_default(t *testing.T) { @@ -21,77 +20,26 @@ func TestBuilderAcc_default(t *testing.T) { func defaultConfig() map[string]interface{} { config := map[string]interface{}{ - "vcenter_server": "vcenter.vsphere55.test", - "username": "root", - "password": "jetbrains", + "vcenter_server": driverT.DefaultVCenterServer, + "username": driverT.DefaultVCenterUsername, + "password": driverT.DefaultVCenterPassword, "insecure_connection": true, - "template": "alpine", - "host": "esxi-1.vsphere55.test", + "template": driverT.DefaultTemplate, + "host": driverT.DefaultHost, "ssh_username": "root", "ssh_password": "jetbrains", } - config["vm_name"] = fmt.Sprintf("test-%v", rand.Intn(1000)) + config["vm_name"] = driverT.NewVMName() return config } func checkDefault(t *testing.T, name string, host string, datastore string) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) - - vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore", "layoutEx.disk") - if err != nil { - t.Fatalf("Cannot read VM properties: %v", err) - } - - if vmInfo.Name != name { - t.Errorf("Invalid VM name: expected '%v', got '%v'", name, vmInfo.Name) - } - - f := d.NewFolder(vmInfo.Parent) - folderPath, err := f.Path() - if err != nil { - t.Fatalf("Cannot read folder name: %v", err) - } - if folderPath != "" { - t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) - } - - h := d.NewHost(vmInfo.Runtime.Host) - hostInfo, err := h.Info("name") - if err != nil { - t.Fatal("Cannot read host properties: ", err) - } - if hostInfo.Name != host { - t.Errorf("Invalid host name: expected '%v', got '%v'", host, hostInfo.Name) - } - - p := d.NewResourcePool(vmInfo.ResourcePool) - poolPath, err := p.Path() - if err != nil { - t.Fatalf("Cannot read resource pool name: %v", err) - } - if poolPath != "" { - t.Error("Invalid resource pool: expected '/', got '%v'", poolPath) - } - - dsr := vmInfo.Datastore[0].Reference() - ds := d.NewDatastore(&dsr) - dsInfo, err := ds.Info("name") - if err != nil { - t.Fatal("Cannot read datastore properties: ", err) - } - if dsInfo.Name != datastore { - t.Errorf("Invalid datastore name: expected '%v', got '%v'", datastore, dsInfo.Name) - } - - if len(vmInfo.LayoutEx.Disk[0].Chain) != 1 { - t.Error("Not a full clone") - } - - return nil + return driverT.VMCheckDefault(t, d, vm, name, host, datastore) } } @@ -137,7 +85,7 @@ func folderConfig() string { func checkFolder(t *testing.T, folder string) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("parent") @@ -175,7 +123,7 @@ func resourcePoolConfig() string { func checkResourcePool(t *testing.T, pool string) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("resourcePool") @@ -196,6 +144,7 @@ func checkResourcePool(t *testing.T, pool string) builderT.TestCheckFunc { } } +// FIXME: why do we need this??? Why don't perform these checks in checkDefault? func TestBuilderAcc_datastore(t *testing.T) { builderT.Test(t, builderT.TestCase{ Builder: &Builder{}, @@ -212,7 +161,7 @@ func datastoreConfig() string { func checkDatastore(t *testing.T, name string) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("datastore") @@ -226,20 +175,14 @@ func checkDatastore(t *testing.T, name string) builderT.TestCheckFunc { } ds := d.NewDatastore(&vmInfo.Datastore[0]) - info, err := ds.Info("name") - if err != nil { - t.Fatalf("Cannot read datastore properties: %v", err) - } - if info.Name != name { - t.Errorf("Wrong datastore. expected: %v, got: %v", name, info.Name) - } + driverT.CheckDatastoreName(t, ds, name) return nil } } func TestBuilderAcc_multipleDatastores(t *testing.T) { - t.Skip("test must fail") + t.Skip("test must fail") // FIXME builderT.Test(t, builderT.TestCase{ Builder: &Builder{}, @@ -269,7 +212,7 @@ func linkedCloneConfig() string { func checkLinkedClone(t *testing.T) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("layoutEx.disk") @@ -295,11 +238,11 @@ func TestBuilderAcc_hardware(t *testing.T) { func hardwareConfig() string { config := defaultConfig() - config["CPUs"] = 2 - config["CPU_reservation"] = 1000 - config["CPU_limit"] = 1500 - config["RAM"] = 2048 - config["RAM_reservation"] = 1024 + config["CPUs"] = driverT.DefaultCPUs + config["CPU_reservation"] = driverT.DefaultCPUReservation + config["CPU_limit"] = driverT.DefaultCPULimit + config["RAM"] = driverT.DefaultRAM + config["RAM_reservation"] = driverT.DefaultRAMReservation config["linked_clone"] = true // speed up return renderConfig(config) @@ -307,40 +250,9 @@ func hardwareConfig() string { func checkHardware(t *testing.T) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) - + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) - vmInfo, err := vm.Info("config") - if err != nil { - t.Fatalf("Cannot read VM properties: %v", err) - } - - cpuSockets := vmInfo.Config.Hardware.NumCPU - if cpuSockets != 2 { - t.Errorf("VM should have 2 CPU sockets, got %v", cpuSockets) - } - - cpuReservation := vmInfo.Config.CpuAllocation.GetResourceAllocationInfo().Reservation - if cpuReservation != 1000 { - t.Errorf("VM should have CPU reservation for 1000 Mhz, got %v", cpuReservation) - } - - cpuLimit := vmInfo.Config.CpuAllocation.GetResourceAllocationInfo().Limit - if cpuLimit != 1500 { - t.Errorf("VM should have CPU reservation for 1500 Mhz, got %v", cpuLimit) - } - - ram := vmInfo.Config.Hardware.MemoryMB - if ram != 2048 { - t.Errorf("VM should have 2048 MB of RAM, got %v", ram) - } - - ramReservation := vmInfo.Config.MemoryAllocation.GetResourceAllocationInfo().Reservation - if ramReservation != 1024 { - t.Errorf("VM should have RAM reservation for 1024 MB, got %v", ramReservation) - } - - return nil + return driverT.VMCheckHardware(t, d, vm) } } @@ -362,7 +274,7 @@ func RAMReservationConfig() string { func checkRAMReservation(t *testing.T) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("config") @@ -409,7 +321,7 @@ func snapshotConfig() string { func checkSnapshot(t *testing.T) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("layoutEx.disk") @@ -443,7 +355,7 @@ func templateConfig() string { func checkTemplate(t *testing.T) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { - d := testConn(t) + d := driverT.NewTestDriver(t) vm := getVM(t, d, artifacts) vmInfo, err := vm.Info("config.template") @@ -475,19 +387,6 @@ func renderConfig(config map[string]interface{}) string { return string(j) } -func testConn(t *testing.T) *driver.Driver { - d, err := driver.NewDriver(&driver.ConnectConfig{ - VCenterServer: "vcenter.vsphere55.test", - Username: "root", - Password: "jetbrains", - InsecureConnection: true, - }) - if err != nil { - t.Fatal("Cannot connect: ", err) - } - return d -} - func getVM(t *testing.T, d *driver.Driver, artifacts []packer.Artifact) *driver.VirtualMachine { artifactRaw := artifacts[0] artifact, _ := artifactRaw.(*Artifact) diff --git a/driver/test.sh b/driver/test.sh new file mode 100755 index 000000000..a2bf973cb --- /dev/null +++ b/driver/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +export VSPHERE_DRIVER_ACC=1 +cd testing +go test -v "$@" diff --git a/driver/testing/config.go b/driver/testing/config.go new file mode 100644 index 000000000..947aeb90e --- /dev/null +++ b/driver/testing/config.go @@ -0,0 +1,19 @@ +package testing + +// Defines whether acceptance tests should be run +const TestEnvVar = "VSPHERE_DRIVER_ACC" + +// Describe the environment to run tests in +const DefaultDatastore = "datastore1" +const DefaultTemplate = "alpine" +const DefaultHost = "esxi-1.vsphere55.test" +const DefaultVCenterServer = "vcenter.vsphere55.test" +const DefaultVCenterUsername = "root" +const DefaultVCenterPassword = "jetbrains" + +// Default hardware settings for +const DefaultCPUs = 2 +const DefaultCPUReservation = 1000 +const DefaultCPULimit = 1500 +const DefaultRAM = 2048 +const DefaultRAMReservation = 1024 diff --git a/driver/testing/datastore_acc_test.go b/driver/testing/datastore_acc_test.go new file mode 100644 index 000000000..5434e3730 --- /dev/null +++ b/driver/testing/datastore_acc_test.go @@ -0,0 +1,30 @@ +package testing + +import ( + "testing" +) + +var infoParams = [][]string { + //{}, // FIXME: Doesn't work + //{"*"}, // FIXME: Doesn't work + {"host", "vm"}, // TODO: choose something more meaningful +} + +func TestDatastore(t *testing.T) { + initDriverAcceptanceTest(t) + + d := NewTestDriver(t) + ds, err := d.FindDatastore(DefaultDatastore) + if err != nil { + t.Fatalf("`FindDatatore` can't find default datastore '%v'. Error: %v", + DefaultDatastore, err) + } + for _, params := range infoParams { + _, err := ds.Info(params...) + if err != nil { + t.Errorf("Cannot read datastore properties with parameters %v: %v", + params, err) + } + } + // TODO: add more checks +} diff --git a/driver/testing/utility.go b/driver/testing/utility.go new file mode 100644 index 000000000..8b2e007e7 --- /dev/null +++ b/driver/testing/utility.go @@ -0,0 +1,142 @@ +package testing + +import ( + "fmt" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "math/rand" + "os" + "testing" + "time" +) + +func NewTestDriver(t *testing.T) *driver.Driver { + d, err := driver.NewDriver(&driver.ConnectConfig{ + VCenterServer: DefaultVCenterServer, + Username: DefaultVCenterUsername, + Password: DefaultVCenterPassword, + InsecureConnection: true, + }) + if err != nil { + t.Fatal("Cannot connect: ", err) + } + return d +} + +func NewVMName() string { + rand.Seed(time.Now().UTC().UnixNano()) + return fmt.Sprintf("test-%v", rand.Intn(1000)) +} + +func CheckDatastoreName(t *testing.T, ds *driver.Datastore, name string) { + info, err := ds.Info("name") + if err != nil { + t.Fatalf("Cannot read datastore properties: %v", err) + } + if info.Name != name { + t.Errorf("Wrong datastore. expected: %v, got: %v", name, info.Name) + } +} + +func initDriverAcceptanceTest(t *testing.T) { + // We only run acceptance tests if an env var is set because they're + // slow and require outside configuration. + if os.Getenv(TestEnvVar) == "" { + t.Skip(fmt.Sprintf( + "Acceptance tests skipped unless env '%s' set", + TestEnvVar)) + } + + // We require verbose mode so that the user knows what is going on. + if !testing.Verbose() { + t.Fatal("Acceptance tests must be run with the -v flag on tests") + } +} + +func VMCheckDefault(t *testing.T, d *driver.Driver, vm *driver.VirtualMachine, + name string, host string, datastore string) error { + vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore", "layoutEx.disk") + if err != nil { + t.Fatalf("Cannot read VM properties: %v", err) + } + + if vmInfo.Name != name { + t.Errorf("Invalid VM name: expected '%v', got '%v'", name, vmInfo.Name) + } + + f := d.NewFolder(vmInfo.Parent) + folderPath, err := f.Path() + if err != nil { + t.Fatalf("Cannot read folder name: %v", err) + } + if folderPath != "" { + t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) + } + + h := d.NewHost(vmInfo.Runtime.Host) + hostInfo, err := h.Info("name") + if err != nil { + t.Fatal("Cannot read host properties: ", err) + } + if hostInfo.Name != host { + t.Errorf("Invalid host name: expected '%v', got '%v'", host, hostInfo.Name) + } + + p := d.NewResourcePool(vmInfo.ResourcePool) + poolPath, err := p.Path() + if err != nil { + t.Fatalf("Cannot read resource pool name: %v", err) + } + if poolPath != "" { + t.Error("Invalid resource pool: expected '/', got '%v'", poolPath) + } + + dsr := vmInfo.Datastore[0].Reference() + ds := d.NewDatastore(&dsr) + dsInfo, err := ds.Info("name") + if err != nil { + t.Fatal("Cannot read datastore properties: ", err) + } + if dsInfo.Name != datastore { + t.Errorf("Invalid datastore name: expected '%v', got '%v'", datastore, dsInfo.Name) + } + + if len(vmInfo.LayoutEx.Disk[0].Chain) != 1 { + t.Error("Not a full clone") + } + + return nil +} + +func VMCheckHardware(t* testing.T, d *driver.Driver, vm *driver.VirtualMachine) error { + vmInfo, err := vm.Info("config") + if err != nil { + t.Fatalf("Cannot read VM properties: %v", err) + } + + cpuSockets := vmInfo.Config.Hardware.NumCPU + if cpuSockets != 2 { + t.Errorf("VM should have 2 CPU sockets, got %v", cpuSockets) + } + + cpuReservation := vmInfo.Config.CpuAllocation.GetResourceAllocationInfo().Reservation + if cpuReservation != 1000 { + t.Errorf("VM should have CPU reservation for 1000 Mhz, got %v", cpuReservation) + } + + cpuLimit := vmInfo.Config.CpuAllocation.GetResourceAllocationInfo().Limit + if cpuLimit != 1500 { + t.Errorf("VM should have CPU reservation for 1500 Mhz, got %v", cpuLimit) + } + + ram := vmInfo.Config.Hardware.MemoryMB + if ram != 2048 { + t.Errorf("VM should have 2048 MB of RAM, got %v", ram) + } + + ramReservation := vmInfo.Config.MemoryAllocation.GetResourceAllocationInfo().Reservation + if ramReservation != 1024 { + t.Errorf("VM should have RAM reservation for 1024 MB, got %v", ramReservation) + } + + return nil +} diff --git a/driver/testing/vm_acc_test.go b/driver/testing/vm_acc_test.go new file mode 100644 index 000000000..7f93ce59d --- /dev/null +++ b/driver/testing/vm_acc_test.go @@ -0,0 +1,131 @@ +package testing + +import ( + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "log" + "testing" + "net" + "time" +) + +func initVMAccTest(t *testing.T) (d *driver.Driver, vm *driver.VirtualMachine, vmName string, vmDestructor func()) { + initDriverAcceptanceTest(t) + d = NewTestDriver(t) + + template, err := d.FindVM(DefaultTemplate) // Don't destroy this VM! + if err != nil { + t.Fatalf("Cannot find template vm '%v': %v", DefaultTemplate, err) + } + + log.Printf("[DEBUG] Clonning VM") + vmName = NewVMName() + vm, err = template.Clone(&driver.CloneConfig{ + Name: vmName, + Host: DefaultHost, + }) + if err != nil { + t.Fatalf("Cannot clone vm '%v': %v", DefaultTemplate, err) + } + + vmDestructor = func() { + log.Printf("[DEBUG] Removing the clone") + if err := vm.Destroy(); err != nil { + t.Errorf("!!! ERROR REMOVING VM '%v': %v!!!", vmName, err) + } + + // Check that the clone is no longer exists + if _, err := d.FindVM(vmName); err == nil { + t.Errorf("!!! STILL CAN FIND VM '%v'. IT MIGHT NOT HAVE BEEN DELETED !!!", vmName) + } + } + + return +} + +func TestVMAcc_default(t *testing.T) { + d, vm, vmName, vmDestructor := initVMAccTest(t) + defer vmDestructor() + + // Check that the clone can be found by its name + if _, err := d.FindVM(vmName); err != nil { + t.Errorf("Cannot find created vm '%v': %v", vmName, err) + } + + // Run checks + log.Printf("[DEBUG] Running check function") + VMCheckDefault(t, d, vm, vmName, DefaultHost, DefaultDatastore) +} + +func TestVMAcc_hardware(t *testing.T) { + d, vm, _ /*vmName*/, vmDestructor := initVMAccTest(t) + defer vmDestructor() + + log.Printf("[DEBUG] Configuring the vm") + vm.Configure(&driver.HardwareConfig{ + CPUs: DefaultCPUs, + CPUReservation: DefaultCPUReservation, + CPULimit: DefaultCPULimit, + RAM: DefaultRAM, + RAMReservation: DefaultRAMReservation, + }) + log.Printf("[DEBUG] Running check function") + VMCheckHardware(t, d, vm) +} + +func startVM(t *testing.T, vm *driver.VirtualMachine, vmName string) (stopper func()) { + log.Printf("[DEBUG] Starting the vm") + if err := vm.PowerOn(); err != nil { + t.Fatalf("Cannot start created vm '%v': %v", vmName, err) + } + return func() { + log.Printf("[DEBUG] Powering off the vm") + if err := vm.PowerOff(); err != nil { + t.Errorf("Cannot power off started vm '%v': %v", vmName, err) + } + } +} + +func TestVMAcc_running(t *testing.T) { + _ /*d*/, vm, vmName, vmDestructor := initVMAccTest(t) + defer vmDestructor() + + stopper := startVM(t, vm, vmName) + defer stopper() + + switch ip, err := vm.WaitForIP(); { + case err != nil: + t.Errorf("Cannot obtain IP address from created vm '%v': %v", vmName, err) + case net.ParseIP(ip) == nil: + t.Errorf("'%v' is not a valid ip address", ip) + } + + vm.StartShutdown() + log.Printf("[DEBUG] Waiting max 1m0s for shutdown to complete") + // TODO: there is complex logic in WaitForShutdown. It's better to test it well. It might be reasonable to create + // unit tests for it. + vm.WaitForShutdown(1 * time.Minute) +} + +func TestVMAcc_running_snapshot(t *testing.T) { + _ /*d*/, vm, vmName, vmDestructor := initVMAccTest(t) + defer vmDestructor() + + stopper := startVM(t, vm, vmName) + defer stopper() + + vm.CreateSnapshot("test-snapshot") + // TODO: check +} + +func TestVMAcc_template(t *testing.T) { + _ /*d*/, vm, vmName, vmDestructor := initVMAccTest(t) + defer vmDestructor() + + vm.ConvertToTemplate() + switch info, err := vm.Info("config"); { + case err != nil: + t.Fatalf("Cannot read VM properties for '%v': %v", vmName, err) + case !info.Config.Template: + t.Fatalf("'%v' seems not to be a template", vmName) + } +} diff --git a/driver/vm.go b/driver/vm.go index 0457690df..967264fc9 100644 --- a/driver/vm.go +++ b/driver/vm.go @@ -183,11 +183,7 @@ func (vm *VirtualMachine) PowerOn() error { } func (vm *VirtualMachine) WaitForIP() (string, error) { - ip, err := vm.vm.WaitForIP(vm.driver.ctx) - if err != nil { - return "", err - } - return ip, nil + return vm.vm.WaitForIP(vm.driver.ctx) } func (vm *VirtualMachine) PowerOff() error { @@ -245,6 +241,5 @@ func (vm *VirtualMachine) CreateSnapshot(name string) error { } func (vm *VirtualMachine) ConvertToTemplate() error { - err := vm.vm.MarkAsTemplate(vm.driver.ctx) - return err + return vm.vm.MarkAsTemplate(vm.driver.ctx) } diff --git a/test.sh b/test.sh index 0c7683736..dd91fee3a 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ #!/bin/sh export PACKER_ACC=1 -go test -v +go test -v "$@"