From f909669670986f23c57f5219041177a759d71123 Mon Sep 17 00:00:00 2001 From: Andrei Tonkikh Date: Wed, 24 Jan 2018 16:35:04 +0300 Subject: [PATCH] Add basic functionality of creating VMs to the driver --- driver/datastore.go | 24 ++ driver/driver_test.go | 2 +- driver/host_acc_test.go | 6 +- driver/vm.go | 257 +++++++++++++++--- .../{vm_acc_test.go => vm_clone_acc_test.go} | 30 +- driver/vm_create_acc_test.go | 96 +++++++ 6 files changed, 355 insertions(+), 60 deletions(-) rename driver/{vm_acc_test.go => vm_clone_acc_test.go} (92%) create mode 100644 driver/vm_create_acc_test.go diff --git a/driver/datastore.go b/driver/datastore.go index 9aa6783c1..bce5962fb 100644 --- a/driver/datastore.go +++ b/driver/datastore.go @@ -29,6 +29,17 @@ func (d *Driver) FindDatastore(name string) (*Datastore, error) { }, nil } +func (d *Driver) FindDatastoreOrDefault(name string) (*Datastore, error) { + ds, err := d.finder.DatastoreOrDefault(d.ctx, name) + if err != nil { + return nil, err + } + return &Datastore{ + ds: ds, + driver: d, + }, nil +} + func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) { var p []string if len(params) == 0 { @@ -43,3 +54,16 @@ func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) { } return &info, nil } + +func (ds *Datastore) FileExists(path string) bool { + _, err := ds.ds.Stat(ds.driver.ctx, path) + return err == nil +} + +func (ds *Datastore) Name() string { + return ds.ds.Name() +} + +func (ds *Datastore) ResolvePath(path string) string { + return ds.ds.Path(path) +} diff --git a/driver/driver_test.go b/driver/driver_test.go index f6cd6bee5..5a9747b3d 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -10,7 +10,7 @@ import ( // Defines whether acceptance tests should be run const TestEnvVar = "VSPHERE_DRIVER_ACC" -const hostName = "esxi-1.vsphere65.test" +const TestHostName = "esxi-1.vsphere65.test" func newTestDriver(t *testing.T) *Driver { d, err := NewDriver(&ConnectConfig{ diff --git a/driver/host_acc_test.go b/driver/host_acc_test.go index bba4da7cf..037987bb4 100644 --- a/driver/host_acc_test.go +++ b/driver/host_acc_test.go @@ -8,7 +8,7 @@ func TestHostAcc(t *testing.T) { initDriverAcceptanceTest(t) d := newTestDriver(t) - host, err := d.FindHost(hostName) + host, err := d.FindHost(TestHostName) if err != nil { t.Fatalf("Cannot find the default host '%v': %v", "datastore1", err) } @@ -17,7 +17,7 @@ func TestHostAcc(t *testing.T) { if err != nil { t.Fatalf("Cannot read host properties: %v", err) } - if info.Name != hostName { - t.Errorf("Wrong host name: expected '%v', got: '%v'", hostName, info.Name) + if info.Name != TestHostName { + t.Errorf("Wrong host name: expected '%v', got: '%v'", TestHostName, info.Name) } } diff --git a/driver/vm.go b/driver/vm.go index a4d6734cf..0244cf04d 100644 --- a/driver/vm.go +++ b/driver/vm.go @@ -34,6 +34,22 @@ type HardwareConfig struct { NestedHV bool } +type CreateConfig struct { + HardwareConfig + + DiskThinProvisioned bool + DiskControllerType string // example: "scsi", "pvscsi" + + Annotation string + Name string + Folder string + Host string + ResourcePool string + Datastore string + GuestOS string // example: otherGuest + Overwrite bool +} + func (d *Driver) NewVM(ref *types.ManagedObjectReference) *VirtualMachine { return &VirtualMachine{ vm: object.NewVirtualMachine(d.client.Client, *ref), @@ -52,6 +68,75 @@ func (d *Driver) FindVM(name string) (*VirtualMachine, error) { }, nil } +func (d *Driver) CreateVM(config *CreateConfig) (*VirtualMachine, error) { + createSpec := config.toConfigSpec() + + folder, err := d.FindFolder(config.Folder) + if err != nil { + return nil, err + } + + resourcePool, err := d.FindResourcePool(config.Host, config.ResourcePool) + if err != nil { + return nil, err + } + + host, err := d.FindHost(config.Host) + if err != nil { + return nil, err + } + + datastore, err := d.FindDatastoreOrDefault(config.Datastore) + if err != nil { + return nil, err + } + + if !config.Overwrite { + vmxPath := fmt.Sprintf("%s/%s.vmx", config.Name, config.Name) + if datastore.FileExists(vmxPath) { + dsPath := datastore.ResolvePath(vmxPath) + return nil, fmt.Errorf("File '%s' already exists", dsPath) + } + } + + devices := object.VirtualDeviceList{} + + devices, err = addIDE(devices) + if err != nil { + return nil, err + } + devices, err = addDisk(d, devices, config) + if err != nil { + return nil, err + } + devices, err = addNetwork(d, devices) + if err != nil { + return nil, err + } + + createSpec.DeviceChange, err = devices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) + if err != nil { + return nil, err + } + + createSpec.Files = &types.VirtualMachineFileInfo{ + VmPathName: fmt.Sprintf("[%s]", datastore.Name()), + } + + task, err := folder.folder.CreateVM(d.ctx, createSpec, resourcePool.pool, host.host) + if err != nil { + return nil, err + } + taskInfo, err := task.WaitForResult(d.ctx, nil) + if err != nil { + return nil, err + } + + vmRef := taskInfo.Result.(types.ManagedObjectReference) + + return d.NewVM(&vmRef), nil +} + func (vm *VirtualMachine) Info(params ...string) (*mo.VirtualMachine, error) { var p []string if len(params) == 0 { @@ -82,32 +167,12 @@ func (template *VirtualMachine) Clone(config *CloneConfig) (*VirtualMachine, err poolRef := pool.pool.Reference() relocateSpec.Pool = &poolRef - if config.Datastore == "" { - host, err := template.driver.FindHost(config.Host) - if err != nil { - return nil, err - } - - info, err := host.Info("datastore") - if err != nil { + datastore, err := template.driver.FindDatastoreOrDefault(config.Datastore) + if err != nil { return nil, err - } - - if len(info.Datastore) > 1 { - return nil, fmt.Errorf("Target host has several datastores. Specify 'datastore' parameter explicitly") - } - - ref := info.Datastore[0].Reference() - relocateSpec.Datastore = &ref - } else { - ds, err := template.driver.FindDatastore(config.Datastore) - if err != nil { - return nil, err - } - - ref := ds.ds.Reference() - relocateSpec.Datastore = &ref } + datastoreRef := datastore.ds.Reference() + relocateSpec.Datastore = &datastoreRef var cloneSpec types.VirtualMachineCloneSpec cloneSpec.Location = relocateSpec @@ -137,8 +202,8 @@ func (template *VirtualMachine) Clone(config *CloneConfig) (*VirtualMachine, err return nil, err } - ref := info.Result.(types.ManagedObjectReference) - vm := template.driver.NewVM(&ref) + vmRef := info.Result.(types.ManagedObjectReference) + vm := template.driver.NewVM(&vmRef) return vm, nil } @@ -152,21 +217,7 @@ func (vm *VirtualMachine) Destroy() error { } func (vm *VirtualMachine) Configure(config *HardwareConfig) error { - var confSpec types.VirtualMachineConfigSpec - confSpec.NumCPUs = config.CPUs - confSpec.MemoryMB = config.RAM - - var cpuSpec types.ResourceAllocationInfo - cpuSpec.Reservation = config.CPUReservation - cpuSpec.Limit = config.CPULimit - confSpec.CpuAllocation = &cpuSpec - - var ramSpec types.ResourceAllocationInfo - ramSpec.Reservation = config.RAMReservation - confSpec.MemoryAllocation = &ramSpec - - confSpec.MemoryReservationLockedToMax = &config.RAMReserveAll - confSpec.NestedHVEnabled = &config.NestedHV + confSpec := config.toConfigSpec() if config.DiskSize > 0 { devices, err := vm.vm.Device(vm.driver.ctx) @@ -179,7 +230,7 @@ func (vm *VirtualMachine) Configure(config *HardwareConfig) error { return err } - disk.CapacityInKB = config.DiskSize * 1024 * 1024 // Gb + disk.CapacityInKB = convertGiBToKiB(config.DiskSize) confSpec.DeviceChange = []types.BaseVirtualDeviceConfigSpec{ &types.VirtualDeviceConfigSpec{ @@ -193,6 +244,7 @@ func (vm *VirtualMachine) Configure(config *HardwareConfig) error { if err != nil { return err } + _, err = task.WaitForResult(vm.driver.ctx, nil) return err } @@ -285,3 +337,124 @@ func (vm *VirtualMachine) CreateSnapshot(name string) error { func (vm *VirtualMachine) ConvertToTemplate() error { return vm.vm.MarkAsTemplate(vm.driver.ctx) } + +func (config HardwareConfig) toConfigSpec() types.VirtualMachineConfigSpec { + var confSpec types.VirtualMachineConfigSpec + confSpec.NumCPUs = config.CPUs + confSpec.MemoryMB = config.RAM + + var cpuSpec types.ResourceAllocationInfo + cpuSpec.Reservation = config.CPUReservation + cpuSpec.Limit = config.CPULimit + confSpec.CpuAllocation = &cpuSpec + + var ramSpec types.ResourceAllocationInfo + ramSpec.Reservation = config.RAMReservation + confSpec.MemoryAllocation = &ramSpec + + confSpec.MemoryReservationLockedToMax = &config.RAMReserveAll + confSpec.NestedHVEnabled = &config.NestedHV + + return confSpec +} + +func (config CreateConfig) toConfigSpec() types.VirtualMachineConfigSpec { + confSpec := config.HardwareConfig.toConfigSpec() + confSpec.Name = config.Name + confSpec.Annotation = config.Annotation + confSpec.GuestId = config.GuestOS + return confSpec +} + +func addDisk(d *Driver, devices object.VirtualDeviceList, config *CreateConfig) (object.VirtualDeviceList, error) { + device, err := devices.CreateSCSIController(config.DiskControllerType) + if err != nil { + return nil, err + } + devices = append(devices, device) + controller, err := devices.FindDiskController(devices.Name(device)) + if err != nil { + return nil, err + } + + disk := &types.VirtualDisk{ + VirtualDevice: types.VirtualDevice{ + Key: devices.NewKey(), + Backing: &types.VirtualDiskFlatVer2BackingInfo{ + DiskMode: string(types.VirtualDiskModePersistent), + ThinProvisioned: types.NewBool(config.DiskThinProvisioned), + }, + }, + CapacityInKB: convertGiBToKiB(config.DiskSize), + } + + devices.AssignController(disk, controller) + devices = append(devices, disk) + + return devices, nil +} + +func addNetwork(d *Driver, devices object.VirtualDeviceList) (object.VirtualDeviceList, error) { + network, err := d.finder.DefaultNetwork(d.ctx) + if err != nil { + return nil, err + } + + backing, err := network.EthernetCardBackingInfo(d.ctx) + if err != nil { + return nil, err + } + + device, err := object.EthernetCardTypes().CreateEthernetCard("", backing) + if err != nil { + return nil, err + } + + return append(devices, device), nil +} + +func addIDE(devices object.VirtualDeviceList) (object.VirtualDeviceList, error) { + ideDevice, err := devices.CreateIDEController() + if err != nil { + return nil, err + } + devices = append(devices, ideDevice) + + return devices, nil +} + +func (vm *VirtualMachine) AddCdrom(isoPath string) error { + devices, err := vm.vm.Device(vm.driver.ctx) + if err != nil { + return err + } + ide, err := devices.FindIDEController("") + if err != nil { + return err + } + + cdrom, err := devices.CreateCdrom(ide) + if err != nil { + return err + } + + if isoPath != "" { + cdrom = devices.InsertIso(cdrom, isoPath) + } + + newDevices := object.VirtualDeviceList{cdrom} + confSpec := types.VirtualMachineConfigSpec{} + confSpec.DeviceChange, err = newDevices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) + + task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) + if err != nil { + return err + } + + _, err = task.WaitForResult(vm.driver.ctx, nil) + return err +} + +func convertGiBToKiB(gib int64) int64 { + return gib * 1024 * 1024 +} diff --git a/driver/vm_acc_test.go b/driver/vm_clone_acc_test.go similarity index 92% rename from driver/vm_acc_test.go rename to driver/vm_clone_acc_test.go index ccde75b47..0f1ed17ce 100644 --- a/driver/vm_acc_test.go +++ b/driver/vm_clone_acc_test.go @@ -28,7 +28,7 @@ func TestVMAcc_clone(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tc.config.Host = hostName + tc.config.Host = TestHostName tc.config.Name = newVMName() templateName := "alpine" @@ -45,17 +45,7 @@ func TestVMAcc_clone(t *testing.T) { t.Fatalf("Cannot clone vm '%v': %v", templateName, err) } - defer func() { - log.Printf("[DEBUG] Removing the clone") - if err := vm.Destroy(); err != nil { - t.Errorf("!!! ERROR REMOVING VM '%v': %v!!!", tc.config.Name, err) - } - - // Check that the clone is no longer exists - if _, err := d.FindVM(tc.config.Name); err == nil { - t.Errorf("!!! STILL CAN FIND VM '%v'. IT MIGHT NOT HAVE BEEN DELETED !!!", tc.config.Name) - } - }() + defer destroyVM(t, vm, tc.config.Name) log.Printf("[DEBUG] Running check function") tc.checkFunction(t, vm, tc.config) @@ -94,8 +84,8 @@ func cloneDefaultCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { if err != nil { t.Fatal("Cannot read host properties: ", err) } - if hostInfo.Name != hostName { - t.Errorf("Invalid host name: expected '%v', got '%v'", hostName, hostInfo.Name) + if hostInfo.Name != TestHostName { + t.Errorf("Invalid host name: expected '%v', got '%v'", TestHostName, hostInfo.Name) } p := d.NewResourcePool(vmInfo.ResourcePool) @@ -278,3 +268,15 @@ func startVM(t *testing.T, vm *VirtualMachine, vmName string) (stopper func()) { } } } + +func destroyVM(t *testing.T, vm *VirtualMachine, vmName string) { + log.Printf("[DEBUG] Deleting the VM") + if err := vm.Destroy(); err != nil { + t.Errorf("!!! ERROR DELETING VM '%v': %v!!!", vmName, err) + } + + // Check that the clone is no longer exists + if _, err := vm.driver.FindVM(vmName); err == nil { + t.Errorf("!!! STILL CAN FIND VM '%v'. IT MIGHT NOT HAVE BEEN DELETED !!!", vmName) + } +} diff --git a/driver/vm_create_acc_test.go b/driver/vm_create_acc_test.go new file mode 100644 index 000000000..5084d3737 --- /dev/null +++ b/driver/vm_create_acc_test.go @@ -0,0 +1,96 @@ +package driver + +import ( + "log" + "testing" +) + +func TestVMAcc_create(t *testing.T) { + initDriverAcceptanceTest(t) + + testCases := []struct { + name string + config *CreateConfig + checkFunction func(*testing.T, *VirtualMachine, *CreateConfig) + }{ + {"MinimalConfiguration", &CreateConfig{}, createDefaultCheck}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.config.Host = TestHostName + tc.config.Name = newVMName() + + d := newTestDriver(t) + + log.Printf("[DEBUG] Creating VM") + vm, err := d.CreateVM(tc.config) + if err != nil { + t.Fatalf("Cannot create VM: %v", err) + } + + defer destroyVM(t, vm, tc.config.Name) + + log.Printf("[DEBUG] Adding CDRom to the created VM") + vm.AddCdrom("[datastore1] ISO/alpine-standard-3.6.2-x86_64.iso") + + log.Printf("[DEBUG] Running check function") + tc.checkFunction(t, vm, tc.config) + }) + } +} + +func createDefaultCheck(t *testing.T, vm *VirtualMachine, config *CreateConfig) { + d := vm.driver + + // Check that the clone can be found by its name + if _, err := d.FindVM(config.Name); err != nil { + t.Errorf("Cannot find created vm '%v': %v", config.Name, err) + } + + 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 != config.Name { + t.Errorf("Invalid VM name: expected '%v', got '%v'", config.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 != TestHostName { + t.Errorf("Invalid host name: expected '%v', got '%v'", TestHostName, 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.Errorf("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 != "datastore1" { + t.Errorf("Invalid datastore name: expected 'datastore1', got '%v'", dsInfo.Name) + } +}