From 26dec49658962f93829438c946a2abbc1b1090d6 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 31 Jul 2019 14:33:18 +0300 Subject: [PATCH 01/31] vm.network block supports update (with VM shutdown) --- go.mod | 2 + go.sum | 4 +- vcd/resource_vcd_vapp_vm.go | 18 ++++-- .../vmware/go-vcloud-director/v2/govcd/vm.go | 58 +++++++++++++++++++ vendor/modules.txt | 2 +- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5a84c773b..16568ec78 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/hashicorp/terraform v0.12.0 github.com/vmware/go-vcloud-director/v2 v2.3.1 ) + +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 diff --git a/go.sum b/go.sum index fc151f34e..77c89f525 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 h1:g/A9l4GsbtREQy2wnQ0j+f/IiLySylpU+Xv9djUjAJg= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= @@ -301,8 +303,6 @@ github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmware/go-vcloud-director/v2 v2.3.1 h1:O2VUliXvZyaS0T6/5ciNUM0eG2aaKPXu+1Kn83rcqHA= -github.com/vmware/go-vcloud-director/v2 v2.3.1/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 47bc1a0aa..674f92620 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -115,20 +115,17 @@ func resourceVcdVAppVm() *schema.Resource { }, "network": { ConflictsWith: []string{"ip", "network_name", "vapp_network_name", "network_href"}, - ForceNew: true, Optional: true, Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { - ForceNew: true, Required: true, Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{"vapp", "org", "none"}, false), Description: "Network type to use: 'vapp', 'org' or 'none'. Use 'vapp' for vApp network, 'org' to attach Org VDC network. 'none' for empty NIC.", }, "ip_allocation_mode": { - ForceNew: true, Optional: true, Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{"POOL", "DHCP", "MANUAL", "NONE"}, false), @@ -140,14 +137,12 @@ func resourceVcdVAppVm() *schema.Resource { }, "ip": { Computed: true, - ForceNew: true, Optional: true, Type: schema.TypeString, ValidateFunc: checkEmptyOrSingleIP(), // Must accept empty string to ease using HCL interpolation }, "is_primary": { Default: false, - ForceNew: true, Optional: true, // By default if the value is omitted it will report schema change // on every terraform operation. The below function @@ -541,7 +536,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || - d.HasChange("expose_hardware_virtualization") { + d.HasChange("expose_hardware_virtualization") || d.HasChange("network") { if status != "POWERED_OFF" { task, err := vm.PowerOff() if err != nil { @@ -624,6 +619,17 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } + if d.HasChange("network") { + networkConnectionSection, err := networksToConfig(d.Get("network").([]interface{}), vdc, vapp, vcdClient) + if err != nil { + return fmt.Errorf("unable to setup network configuration for update: %s", err) + } + err = vm.UpdateNetworkConnectionSection(&networkConnectionSection) + if err != nil { + return fmt.Errorf("unable to update network configuration: %s", err) + } + } + } return resourceVcdVAppVmRead(d, meta) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index a34ec77a2..be9c817e4 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" @@ -85,6 +86,38 @@ func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, er return networkConnectionSection, err } +// UpdateNetworkConnectionSection applies network configuration of types.NetworkConnectionSection for the VM +// Runs synchronously, VM is ready for another operation after this function returns. +func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSection) error { + if vm.VM.HREF == "" { + return fmt.Errorf("cannot refresh, Object is empty") + } + + // Retrieve current network configuration so that we are not altering any other internal fields + updateNetwork, err := vm.GetNetworkConnectionSection() + if err != nil { + return fmt.Errorf("cannot read network section for update: %s", err) + } + updateNetwork.PrimaryNetworkConnectionIndex = networks.PrimaryNetworkConnectionIndex + updateNetwork.NetworkConnection = networks.NetworkConnection + updateNetwork.Ovf = types.XMLNamespaceOVF + + err = vm.client.ExecuteRequestWithoutResponse(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut, + types.MimeNetworkConnectionSection, "error updating network connection: %s", updateNetwork) + if err != nil { + return err + } + + // Wait for the VM to be in resolved state so that this function is synchronous and VM is ready to handle next task + //vm.BlockWhileStatus() + err = vm.BlockWhileStatus("UNRESOLVED", vm.client.MaxRetryTimeout) + if err != nil { + fmt.Errorf("error waiting until VM exists UNRESOLVED state: %s", err) + } + + return nil +} + func (cli *Client) FindVMByHREF(vmHREF string) (VM, error) { newVm := NewVM(cli) @@ -598,3 +631,28 @@ func (vm *VM) ToggleHardwareVirtualization(isEnabled bool) (Task, error) { return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, "", errMessage, nil) } + +// BlockWhileStatus blocks until the status of VM exits unwantedStatus. +// It sleeps 200 milliseconds between iterations and times out after timeOutAfterSeconds +// of seconds. +func (vm *VM) BlockWhileStatus(unwantedStatus string, timeOutAfterSeconds int) error { + timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second) + tick := time.Tick(200 * time.Millisecond) + + for { + select { + case <-timeoutAfter: + return fmt.Errorf("timed out waiting for VM to exit state %s after %d seconds", + unwantedStatus, timeOutAfterSeconds) + case <-tick: + currentStatus, err := vm.GetStatus() + + if err != nil { + return fmt.Errorf("could not get VM status %s", err) + } + if currentStatus != unwantedStatus { + return nil + } + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ac3f25430..4f651ea72 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -216,7 +216,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.3.1 +# github.com/vmware/go-vcloud-director/v2 v2.3.1 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util From 8fbaaf5b7a54500e57f0ef981d3f53597cf95cb4 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 7 Aug 2019 09:20:28 +0300 Subject: [PATCH 02/31] WIP --- go.mod | 2 +- vcd/resource_vcd_vapp_vm.go | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 16568ec78..c651795ec 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.3.1 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 +replace github.com/vmware/go-vcloud-director/v2 => ../go-vcloud-director diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 674f92620..b65552111 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -197,6 +197,35 @@ func resourceVcdVAppVm() *schema.Resource { Default: false, Description: "Expose hardware-assisted CPU virtualization to guest OS.", }, + + "force_customization": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Expose hardware-assisted CPU virtualization to guest OS.", + }, + + "customization": &schema.Schema{ + Optional: true, + MinItems: 1, + MaxItems: 1, + Type: schema.TypeList, + Description: "Guest customization block", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "force": { + Type: schema.TypeBool, + Optional: true, + Default: false, + // This settings is used as a 'flag' and it does not matter what is set in the + // state. If it is 'true' - then it means that 'update' procedure must set the + // VM for customization at next boot and reboot it. + DiffSuppressFunc: suppressFalse(), + }, + + }, + }, + }, }, } } @@ -241,6 +270,13 @@ func falseBoolSuppress() schema.SchemaDiffSuppressFunc { } } +// suppressNewFalse always suppresses when new value is false +func suppressFalse() schema.SchemaDiffSuppressFunc { + return func(k string, old string, new string, d *schema.ResourceData) bool { + return new == "false" + } +} + func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { vcdClient := meta.(*VCDClient) @@ -632,6 +668,15 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } + customization := d.Get("customization") + custom := customization.([]map[string]interface{}) + cust := custom[0] + cu, ok := cust["force"] + if ok && cu.(bool){ + fmt.Println("customization was forced") + } + + return resourceVcdVAppVmRead(d, meta) } From 7de6ab5aadd6426a455bf80a6cf9044050726eff Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 7 Aug 2019 16:24:59 +0300 Subject: [PATCH 03/31] Tests for network update --- go.mod | 2 +- go.sum | 4 +- vcd/resource_vcd_vapp_vm.go | 97 ++++++---- vcd/resource_vcd_vapp_vm_multinetwork_test.go | 172 +++++++++++++++++- .../vmware/go-vcloud-director/v2/govcd/vm.go | 113 ++++++++---- .../v2/types/v56/constants.go | 2 + .../go-vcloud-director/v2/types/v56/types.go | 9 + vendor/modules.txt | 2 +- website/docs/r/vapp_vm.html.markdown | 11 +- 9 files changed, 340 insertions(+), 72 deletions(-) diff --git a/go.mod b/go.mod index c651795ec..498028365 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.3.1 ) -replace github.com/vmware/go-vcloud-director/v2 => ../go-vcloud-director +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0 diff --git a/go.sum b/go.sum index 77c89f525..1774983e6 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 h1:g/A9l4GsbtREQy2wnQ0j+f/IiLySylpU+Xv9djUjAJg= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0 h1:rVicbbI2vtAvv16beVtNRygmKsgUgcmwfMysaMfQ724= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index b65552111..33d1b5dba 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -198,23 +198,23 @@ func resourceVcdVAppVm() *schema.Resource { Description: "Expose hardware-assisted CPU virtualization to guest OS.", }, - "force_customization": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Expose hardware-assisted CPU virtualization to guest OS.", - }, + //"force_customization": &schema.Schema{ + // Type: schema.TypeBool, + // Optional: true, + // Default: false, + // Description: "Expose hardware-assisted CPU virtualization to guest OS.", + //}, "customization": &schema.Schema{ - Optional: true, - MinItems: 1, - MaxItems: 1, - Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Type: schema.TypeList, Description: "Guest customization block", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "force": { - Type: schema.TypeBool, + Type: schema.TypeBool, Optional: true, Default: false, // This settings is used as a 'flag' and it does not matter what is set in the @@ -222,7 +222,6 @@ func resourceVcdVAppVm() *schema.Resource { // VM for customization at next boot and reboot it. DiffSuppressFunc: suppressFalse(), }, - }, }, }, @@ -531,9 +530,9 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error getting VM2: %#v", err) } - status, err := vm.GetStatus() + vmStatusBeforeUpdate, err := vm.GetStatus() if err != nil { - return fmt.Errorf("error getting VM status: %#v", err) + return fmt.Errorf("error getting VM status before update: %#v", err) } // VM does not have to be in POWERED_OFF state for metadata operations @@ -573,7 +572,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || d.HasChange("expose_hardware_virtualization") || d.HasChange("network") { - if status != "POWERED_OFF" { + if vmStatusBeforeUpdate != "POWERED_OFF" { task, err := vm.PowerOff() if err != nil { return fmt.Errorf("error Powering Off: %#v", err) @@ -642,19 +641,6 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } - if d.Get("power_on").(bool) { - - task, err := vm.PowerOn() - if err != nil { - return fmt.Errorf("error Powering Up: %#v", err) - } - - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf(errorCompletingTask, err) - } - } - if d.HasChange("network") { networkConnectionSection, err := networksToConfig(d.Get("network").([]interface{}), vdc, vapp, vcdClient) if err != nil { @@ -669,13 +655,58 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } customization := d.Get("customization") - custom := customization.([]map[string]interface{}) - cust := custom[0] - cu, ok := cust["force"] - if ok && cu.(bool){ - fmt.Println("customization was forced") + customizationSlice := customization.([]interface{}) + if len(customizationSlice) > 0 { + cust := customizationSlice[0] + fc := cust.(map[string]interface{}) + forceCustomization, ok := fc["force"] + if ok && forceCustomization.(bool) { + log.Printf("[DEBUG] Customization was forced for VM %s", vm.VM.Name) + vmStatusBeforeForceCustomization, err := vm.GetStatus() + if err != nil { + return fmt.Errorf("error getting VM status before forcing customization: %s", err) + } + + if vmStatusBeforeForceCustomization != "POWERED_OFF" { + log.Printf("[DEBUG] Customization was forced for VM %s, state was %s. Powering off", + vm.VM.Name, vmStatusBeforeForceCustomization) + task, err := vm.PowerOff() + if err != nil { + return fmt.Errorf("error powering off: %#v", err) + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf(errorCompletingTask, err) + } + } + err = vm.CustomizeAtNextPowerOn() + if err != nil { + return fmt.Errorf("unable to set VM for customization on next boot") + } + } } + // If the VM was powered off during update but it has to be powered of + if d.Get("power_on").(bool) { + + vmStatus, err := vm.GetStatus() + if err != nil { + return fmt.Errorf("error getting VM status before ensuring it is powered on: %s", err) + } + + if vmStatus != "POWERED_ON" { + log.Printf("[TRACE] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) + task, err := vm.PowerOn() + if err != nil { + return fmt.Errorf("error powering on: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf(errorCompletingTask, err) + } + } + } return resourceVcdVAppVmRead(d, meta) } diff --git a/vcd/resource_vcd_vapp_vm_multinetwork_test.go b/vcd/resource_vcd_vapp_vm_multinetwork_test.go index 72b1deb69..f1788ffaf 100644 --- a/vcd/resource_vcd_vapp_vm_multinetwork_test.go +++ b/vcd/resource_vcd_vapp_vm_multinetwork_test.go @@ -28,7 +28,17 @@ func TestAccVcdVAppVmMultiNIC(t *testing.T) { "Tags": "vapp vm", } - configTextVM := templateFill(testAccCheckVcdVAppVmNetworkVM, params) + configTextVM := templateFill(testAccCheckVcdVAppVmNetworkVm, params) + + params["FuncName"] = t.Name() + "-step1" + configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmNetworkVmStep1, params) + + params["FuncName"] = t.Name() + "-step2" + configTextVMUpdateStep2 := templateFill(testAccCheckVcdVAppVmNetworkVmStep2, params) + + params["FuncName"] = t.Name() + "-step3" + configTextVMUpdateStep3 := templateFill(testAccCheckVcdVAppVmNetworkVmStep3, params) + if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -40,6 +50,7 @@ func TestAccVcdVAppVmMultiNIC(t *testing.T) { Providers: testAccProviders, CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), Steps: []resource.TestStep{ + // Step 0 - Create with variations of all possible NICs resource.TestStep{ Config: configTextVM, Check: resource.ComposeAggregateTestCheckFunc( @@ -96,11 +107,78 @@ func TestAccVcdVAppVmMultiNIC(t *testing.T) { resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.6.mac"), ), }, + // Step 1 - update + resource.TestStep{ + Config: configTextVMUpdateStep1, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm."+netVmName1, &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "name", netVmName1), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.name", "multinic-net"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.type", "org"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.is_primary", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.ip_allocation_mode", "POOL"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.ip", "11.10.0.152"), + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.0.mac"), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.name", "multinic-net"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.type", "org"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.is_primary", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.ip_allocation_mode", "DHCP"), + //resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.1.ip"), // We cannot guarantee DHCP + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.1.mac"), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.name", "multinic-net"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.type", "org"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.is_primary", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.ip_allocation_mode", "MANUAL"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.ip", "11.10.0.170"), + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.2.mac"), + ), + }, + // Step 2 - update (remove all NICs) + resource.TestStep{ + Config: configTextVMUpdateStep2, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm."+netVmName1, &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.#", "0"), + ), + }, + // Step 3 - Add one nic of each type + resource.TestStep{ + Config: configTextVMUpdateStep3, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm."+netVmName1, &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "name", netVmName1), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.name", ""), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.type", "none"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.is_primary", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.ip_allocation_mode", "NONE"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.0.ip", ""), + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.0.mac"), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.name", "vapp-net"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.type", "vapp"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.is_primary", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.ip_allocation_mode", "POOL"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.1.ip", "192.168.2.51"), + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.1.mac"), + + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.name", "multinic-net"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.type", "org"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.is_primary", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.ip_allocation_mode", "POOL"), + resource.TestCheckResourceAttr("vcd_vapp_vm."+netVmName1, "network.2.ip", "11.10.0.152"), + resource.TestCheckResourceAttrSet("vcd_vapp_vm."+netVmName1, "network.2.mac"), + ), + }, }, }) } -const testAccCheckVcdVAppVmNetworkVM = ` +const testAccCheckVcdVAppVmNetworkShared =` resource "vcd_vapp" "{{.VAppName}}" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -161,7 +239,9 @@ resource "vcd_network_routed" "net2" { end_address = "12.10.0.254" } } +` +const testAccCheckVcdVAppVmNetworkVm = testAccCheckVcdVAppVmNetworkShared + ` resource "vcd_vapp_vm" "{{.VMName}}" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -222,3 +302,91 @@ resource "vcd_vapp_vm" "{{.VMName}}" { } } ` + +const testAccCheckVcdVAppVmNetworkVmStep1 = testAccCheckVcdVAppVmNetworkShared + ` +# skip-binary-test: only for updates +resource "vcd_vapp_vm" "{{.VMName}}" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.{{.VAppName}}.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + + network { + type = "org" + name = "${vcd_network_routed.net.name}" + ip_allocation_mode = "POOL" + is_primary = false + } + + network { + type = "org" + name = "${vcd_network_routed.net.name}" + ip_allocation_mode = "DHCP" + is_primary = true + } + + network { + type = "org" + name = "${vcd_network_routed.net.name}" + ip = "11.10.0.170" + ip_allocation_mode = "MANUAL" + is_primary = false + } +} +` + +const testAccCheckVcdVAppVmNetworkVmStep2 = testAccCheckVcdVAppVmNetworkShared + ` +# skip-binary-test: only for updates +resource "vcd_vapp_vm" "{{.VMName}}" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.{{.VAppName}}.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 +} +` + +const testAccCheckVcdVAppVmNetworkVmStep3 = testAccCheckVcdVAppVmNetworkShared + ` +# skip-binary-test: only for updates +resource "vcd_vapp_vm" "{{.VMName}}" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.{{.VAppName}}.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + + network { + type = "none" + ip_allocation_mode = "NONE" + } + + network { + type = "vapp" + name = "${vcd_vapp_network.vappNet.name}" + ip_allocation_mode = "POOL" + is_primary = true + } + + network { + type = "org" + name = "${vcd_network_routed.net.name}" + ip_allocation_mode = "POOL" + } +} +` diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index be9c817e4..7c04430aa 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -102,17 +102,14 @@ func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSe updateNetwork.NetworkConnection = networks.NetworkConnection updateNetwork.Ovf = types.XMLNamespaceOVF - err = vm.client.ExecuteRequestWithoutResponse(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut, + task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut, types.MimeNetworkConnectionSection, "error updating network connection: %s", updateNetwork) if err != nil { return err } - - // Wait for the VM to be in resolved state so that this function is synchronous and VM is ready to handle next task - //vm.BlockWhileStatus() - err = vm.BlockWhileStatus("UNRESOLVED", vm.client.MaxRetryTimeout) + err = task.WaitTaskCompletion() if err != nil { - fmt.Errorf("error waiting until VM exists UNRESOLVED state: %s", err) + return fmt.Errorf("error waiting for task completion after network update for vm %s: %s", vm.VM.Name, err) } return nil @@ -140,6 +137,15 @@ func (vm *VM) PowerOn() (Task, error) { } +// CustomizeAtNextPowerOn forces VM customization on next power on +func (vm *VM) CustomizeAtNextPowerOn() error { + apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint.Path += "/action/customizeAtNextPowerOn" + + return vm.client.ExecuteRequestWithoutResponse(apiEndpoint.String(), http.MethodPost, "", + "error setting forced VM customization for next power on: %s", nil) +} + func (vm *VM) PowerOff() (Task, error) { apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) @@ -335,6 +341,76 @@ func (vm *VM) RunCustomizationScript(computername, script string) (Task, error) return vm.Customize(computername, script, false) } +// GetGuestCustomizationSection retrieves +func (vm *VM) GetGuestCustomizationSection() (*types.GuestCustomizationSection, error) { + guestCustomizationSection := &types.GuestCustomizationSection{} + + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot load guest customization, VM HREF is empty") + } + + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestCustomizationSection/", http.MethodGet, + types.MimeGuestCustomizationSection, "error retrieving guest customization: %s", nil, guestCustomizationSection) + + return guestCustomizationSection, err +} + +func (vm *VM) GetGuestCustomizationStatus() (*types.GuestCustomizationStatusSection, error) { + guestCustomizationStatus := &types.GuestCustomizationStatusSection{} + + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot load guest customization, VM HREF is empty") + } + + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestcustomizationstatus", http.MethodGet, + types.MimeGuestCustomizationStatus, "error retrieving guest customization status: %s", nil, guestCustomizationStatus) + + // The request was successful + return guestCustomizationStatus, err +} + +//func (vm *VM) UpdateGuestCustomizationSection(*types.GuestCustomizationSection) (*types.GuestCustomizationSection, error) { +// guestCustomizationSection := &types.GuestCustomizationSection{} +// +// if vm.VM.HREF == "" { +// return nil, fmt.Errorf("cannot load guest customization, VM HREF is empty") +// } +// +// _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestCustomizationSection/", http.MethodGet, +// types.MimeGuestCustomizationSection, "error retrieving guest customization: %s", nil, guestCustomizationSection) +// +// // The request was successful +// return guestCustomizationSection, err +//} + + +// BlockWhileGuestCustomizationStatus blocks until the customization status of VM exits unwantedStatus. +// It sleeps 200 milliseconds between iterations and times out after timeOutAfterSeconds +// of seconds. +func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutAfterSeconds int) error { + timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second) + //tick := time.Tick(200 * time.Millisecond) + tick := time.Tick(1 * time.Second) + + for { + select { + case <-timeoutAfter: + return fmt.Errorf("timed out waiting for VM guest customization status to exit state %s after %d seconds", + unwantedStatus, timeOutAfterSeconds) + case <-tick: + currentStatus, err := vm.GetGuestCustomizationStatus() + if err != nil { + return fmt.Errorf("could not get VM customization status %s", err) + } + fmt.Println("current status: ", currentStatus.GuestCustStatus) + if currentStatus.GuestCustStatus != unwantedStatus { + fmt.Println("exiting because of status: ", currentStatus.GuestCustStatus) + return nil + } + } + } +} + func (vm *VM) Customize(computername, script string, changeSid bool) (Task, error) { err := vm.Refresh() if err != nil { @@ -631,28 +707,3 @@ func (vm *VM) ToggleHardwareVirtualization(isEnabled bool) (Task, error) { return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, "", errMessage, nil) } - -// BlockWhileStatus blocks until the status of VM exits unwantedStatus. -// It sleeps 200 milliseconds between iterations and times out after timeOutAfterSeconds -// of seconds. -func (vm *VM) BlockWhileStatus(unwantedStatus string, timeOutAfterSeconds int) error { - timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second) - tick := time.Tick(200 * time.Millisecond) - - for { - select { - case <-timeoutAfter: - return fmt.Errorf("timed out waiting for VM to exit state %s after %d seconds", - unwantedStatus, timeOutAfterSeconds) - case <-tick: - currentStatus, err := vm.GetStatus() - - if err != nil { - return fmt.Errorf("could not get VM status %s", err) - } - if currentStatus != unwantedStatus { - return nil - } - } - } -} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go index 8dde388e1..17375eb2f 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go @@ -73,6 +73,8 @@ const ( MimeRasdItem = "application/vnd.vmware.vcloud.rasdItem+xml" // Mime for guest customization section MimeGuestCustomizationSection = "application/vnd.vmware.vcloud.guestCustomizationSection+xml" + // Mime for guest customization status + MimeGuestCustomizationStatus = "application/vnd.vmware.vcloud.guestcustomizationstatussection" // Mime for network config section MimeNetworkConfigSection = "application/vnd.vmware.vcloud.networkconfigsection+xml" // Mime for recompose vApp params diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go index 988fd0c8c..b713e8240 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go @@ -1455,6 +1455,15 @@ type DeployVAppParams struct { ForceCustomization bool `xml:"forceCustomization,attr,omitempty"` // Used to specify whether to force customization on deployment, if not set default value is false } +// GuestCustomizationStatusSection holds information about guest customization status +// https://vdc-repo.vmware.com/vmwb-repository/dcr-public/76f491b4-679c-4e1e-8428-f813d668297a/a2555a1b-22f1-4cca-b481-2a98ab874022/doc/doc/operations/GET-GuestCustStatus.html +type GuestCustomizationStatusSection struct { + XMLName xml.Name `xml:"GuestCustomizationStatusSection"` + Xmlns string `xml:"xmlns,attr"` + + GuestCustStatus string `xml:"GuestCustStatus"` +} + // GuestCustomizationSection represents guest customization settings // Type: GuestCustomizationSectionType // Namespace: http://www.vmware.com/vcloud/v1.5 diff --git a/vendor/modules.txt b/vendor/modules.txt index 4f651ea72..20a956c6e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -216,7 +216,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.3.1 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190731113052-87a5af498696 +# github.com/vmware/go-vcloud-director/v2 v2.3.1 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index dc514de8b..2d45b5c98 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -135,8 +135,8 @@ DHCP. guest operating system so that applications that require hardware virtualization can run on virtual machines without binary translation or paravirtualization. Useful for hypervisor nesting provided underlying hardware supports it. Default is `false`. * `network` - (Optional; *v2.2+*) A block to define network interface. Multiple can be used. See [Network](#network) and -example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_name`. **Note**: this property and all -its parameters do force recreation of VMs! +example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_name`. +* `customization` - (Optional; *v2.5+*) A block to define for guest customization options. See [Customization](#customization) ## Disk @@ -175,3 +175,10 @@ its parameters do force recreation of VMs! * `ip_allocation_mode=MANUAL` - **`ip`** value must be valid IP address from a subnet defined in `static pool` for network. * `ip_allocation_mode=NONE` - **`ip`** field can be omitted or set to an empty string "". Empty string may be useful when doing HCL variable interpolation. + + +## Customization + +* `force` (Optional) This field works as a flag and triggers force customization when `true` during an update +(`terraform apply`) every time. It never complains about a change in statefile. It can be used when guest customization +is needed after a NIC change and then set back to `false`. **Note** this setting will cause a VM reboot From 94255fdab3dee23d8abd9dc02731c1d8ad3c11b5 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 8 Aug 2019 09:40:24 +0300 Subject: [PATCH 04/31] make fmt --- vcd/resource_vcd_vapp_vm_multinetwork_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vcd/resource_vcd_vapp_vm_multinetwork_test.go b/vcd/resource_vcd_vapp_vm_multinetwork_test.go index f1788ffaf..80b400cb9 100644 --- a/vcd/resource_vcd_vapp_vm_multinetwork_test.go +++ b/vcd/resource_vcd_vapp_vm_multinetwork_test.go @@ -178,7 +178,7 @@ func TestAccVcdVAppVmMultiNIC(t *testing.T) { }) } -const testAccCheckVcdVAppVmNetworkShared =` +const testAccCheckVcdVAppVmNetworkShared = ` resource "vcd_vapp" "{{.VAppName}}" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -338,6 +338,10 @@ resource "vcd_vapp_vm" "{{.VMName}}" { ip_allocation_mode = "MANUAL" is_primary = false } + + customization { + force = true + } } ` From 7b5fcbf76c447a83ed3df07d7b54ec27af78e935 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Fri, 9 Aug 2019 14:13:23 +0300 Subject: [PATCH 05/31] Stuck --- go.mod | 3 +- vcd/resource_vcd_vapp_vm.go | 82 +++---- ...resource_vcd_vapp_vm_customization_test.go | 224 ++++++++++++++++++ vcd/resource_vcd_vapp_vm_multinetwork_test.go | 4 - 4 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 vcd/resource_vcd_vapp_vm_customization_test.go diff --git a/go.mod b/go.mod index 498028365..76d40da5c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.12 require ( github.com/hashicorp/terraform v0.12.0 + github.com/kr/pretty v0.1.0 github.com/vmware/go-vcloud-director/v2 v2.3.1 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0 +replace github.com/vmware/go-vcloud-director/v2 => ../go-vcloud-director diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 33d1b5dba..bfc5e014a 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -197,14 +197,6 @@ func resourceVcdVAppVm() *schema.Resource { Default: false, Description: "Expose hardware-assisted CPU virtualization to guest OS.", }, - - //"force_customization": &schema.Schema{ - // Type: schema.TypeBool, - // Optional: true, - // Default: false, - // Description: "Expose hardware-assisted CPU virtualization to guest OS.", - //}, - "customization": &schema.Schema{ Optional: true, MinItems: 1, @@ -570,8 +562,11 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } + // Check if the user request for forced reconfiguration of VM + customizationNeeded :=isForcedConfiguration(d.Get("customization")) + if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || - d.HasChange("expose_hardware_virtualization") || d.HasChange("network") { + d.HasChange("expose_hardware_virtualization") || d.HasChange("network") || customizationNeeded { if vmStatusBeforeUpdate != "POWERED_OFF" { task, err := vm.PowerOff() if err != nil { @@ -654,53 +649,27 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } - customization := d.Get("customization") - customizationSlice := customization.([]interface{}) - if len(customizationSlice) > 0 { - cust := customizationSlice[0] - fc := cust.(map[string]interface{}) - forceCustomization, ok := fc["force"] - if ok && forceCustomization.(bool) { - log.Printf("[DEBUG] Customization was forced for VM %s", vm.VM.Name) - vmStatusBeforeForceCustomization, err := vm.GetStatus() - if err != nil { - return fmt.Errorf("error getting VM status before forcing customization: %s", err) - } - - if vmStatusBeforeForceCustomization != "POWERED_OFF" { - log.Printf("[DEBUG] Customization was forced for VM %s, state was %s. Powering off", - vm.VM.Name, vmStatusBeforeForceCustomization) - task, err := vm.PowerOff() - if err != nil { - return fmt.Errorf("error powering off: %#v", err) - } - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf(errorCompletingTask, err) - } - } - err = vm.CustomizeAtNextPowerOn() - if err != nil { - return fmt.Errorf("unable to set VM for customization on next boot") - } - } - } - - // If the VM was powered off during update but it has to be powered of + // If the VM was powered off during update but it has to be powered off if d.Get("power_on").(bool) { - vmStatus, err := vm.GetStatus() if err != nil { return fmt.Errorf("error getting VM status before ensuring it is powered on: %s", err) } - if vmStatus != "POWERED_ON" { - log.Printf("[TRACE] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) + log.Printf("[TRACE] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) + + if vmStatus != "POWERED_ON" && customizationNeeded { + err = vm.PowerOnAndForceCustomization() + if err != nil { + return fmt.Errorf("failed powering on with customization: %s", err) + } + } + + if vmStatus != "POWERED_ON" && !customizationNeeded { task, err := vm.PowerOn() if err != nil { return fmt.Errorf("error powering on: %s", err) } - err = task.WaitTaskCompletion() if err != nil { return fmt.Errorf(errorCompletingTask, err) @@ -1168,3 +1137,24 @@ func readNetworks(vm govcd.VM, vapp govcd.VApp) ([]map[string]interface{}, error } return nets, nil } + +// isForcedConfiguration checks "customization" block in resource and checks if the value of field "force" +// is set to "true". It returns false if the value is not set or is set to false +func isForcedConfiguration(customizationBlock interface{}) bool { + customizationSlice := customizationBlock.([]interface{}) + + if len(customizationSlice) != 1 { + return false + } + + cust := customizationSlice[0] + fc := cust.(map[string]interface{}) + forceCust, ok := fc["force"] + forceCustBool := forceCust.(bool) + + if !ok || !forceCustBool { + return false + } + + return true +} \ No newline at end of file diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go new file mode 100644 index 000000000..dc6b6a283 --- /dev/null +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -0,0 +1,224 @@ +// +build vapp vm ALL functional + +package vcd + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/vmware/go-vcloud-director/v2/govcd" + "testing" +) + +func TestAccVcdVAppVmCustomization(t *testing.T) { + var ( + vapp govcd.VApp + vm govcd.VM + netVappName string = t.Name() + netVmName1 string = t.Name() + "VM" + ) + + var params = StringMap{ + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "Catalog": testSuiteCatalogName, + "CatalogItem": testSuiteCatalogOVAItem, + "VAppName": netVappName, + "VMName": netVmName1, + "Tags": "vapp vm", + } + + configTextVM := templateFill(testAccCheckVcdVAppVmCustomization, params) + + params["FuncName"] = t.Name() + "-step1" + configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmCustomizationStep1, params) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + debugPrintf("#[DEBUG] CONFIGURATION: %s\n", configTextVM) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), + Steps: []resource.TestStep{ + // Step 0 - Create without customization flag + resource.TestStep{ + Config: configTextVM, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVMCustomization("vcd_vapp_vm.test-vm", false), + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "1"), + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "0"), + ), + }, + // Step 1 - Set change network configuration and + resource.TestStep{ + Config: configTextVMUpdateStep1, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVMCustomization("vcd_vapp_vm.test-vm", true), + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "2"), + + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "true"), + ), + }, + }, + }) +} + +// testAccCheckVcdVMCustomization functions acts as a check and a function which waits until +// the VM exits its original "GC_PENDING" state after provisioning. This is needed in order to +// be able to check that setting customization.force flag to `true` actually has impact on VM +// settings. +func testAccCheckVcdVMCustomization(node string, customizationPending bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[node] + if !ok { + return fmt.Errorf("not found: %s", node) + } + + if rs.Primary.Attributes["vapp_name"] == "" { + return fmt.Errorf("no vApp name specified: %+#v", rs) + } + + if rs.Primary.Attributes["name"] == "" { + return fmt.Errorf("no VM name specified: %+#v", rs) + } + + conn := testAccProvider.Meta().(*VCDClient) + + _, vdc, err := conn.GetOrgAndVdc(testConfig.VCD.Org, testConfig.VCD.Vdc) + if err != nil { + return fmt.Errorf(errorRetrievingVdcFromOrg, testConfig.VCD.Vdc, testConfig.VCD.Org, err) + } + + vapp, err := vdc.FindVAppByName(rs.Primary.Attributes["vapp_name"]) + if err != nil { + return err + } + + vm, err := vdc.FindVMByName(vapp, rs.Primary.Attributes["name"]) + + if err != nil { + return err + } + + // Wait until the VM exits from its original GC_PENDING state after provisioning if it is not expected + // that VM is in that state. This takes some time until the VM boots starts guest tools and + // reports success. + if !customizationPending { + // Not using maxRetryTimeout here because it would force for maxRetryTimeout to be quite long + // time by default as it takes some time (around 150s during testing) for Photon OS to boot + // first time and get rid of "GC_PENDING" state + err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) + if err != nil { + return err + } + } + + customizationStatus, err := vm.GetGuestCustomizationStatus() + if err != nil { + return fmt.Errorf("unable to get VM customization status: %s", err) + } + + // At the stage where "GC_PENDING" should not be set. The state should be something else or this + // is an error + if !customizationPending && customizationStatus == "GC_PENDING" { + return fmt.Errorf("customizationStatus should not be in pending state for vm %s", vm.VM.Name) + } + + // Customization status of "GC_PENDING" is expected now and it is an error if something else is set + if customizationPending && customizationStatus != "GC_PENDING" { + return fmt.Errorf("customizationStatus should not be in pending state for vm %s", vm.VM.Name) + } + + return nil + } +} + +const testAccCheckVcdVAppVmCustomizationShared = ` +resource "vcd_vapp" "test-vapp" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + name = "{{.VAppName}}" +} + +resource "vcd_vapp_network" "vappNet" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + name = "vapp-net" + vapp_name = "${vcd_vapp.test-vapp.name}" + gateway = "192.168.2.1" + netmask = "255.255.255.0" + dns1 = "192.168.2.1" + dns2 = "192.168.2.2" + dns_suffix = "mybiz.biz" + + static_ip_pool { + start_address = "192.168.2.51" + end_address = "192.168.2.100" + } +} +` + +const testAccCheckVcdVAppVmCustomization = testAccCheckVcdVAppVmCustomizationShared + ` +resource "vcd_vapp_vm" "test-vm" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.test-vapp.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + + network { + type = "vapp" + name = "${vcd_vapp_network.vappNet.name}" + ip_allocation_mode = "POOL" + } +} +` + +const testAccCheckVcdVAppVmCustomizationStep1 = testAccCheckVcdVAppVmCustomizationShared + ` +resource "vcd_vapp_vm" "test-vm" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.test-vapp.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + + network { + type = "vapp" + name = "${vcd_vapp_network.vappNet.name}" + ip_allocation_mode = "POOL" + } + + network { + type = "none" + ip_allocation_mode = "NONE" + } + + customization { + force = true + } +} +` diff --git a/vcd/resource_vcd_vapp_vm_multinetwork_test.go b/vcd/resource_vcd_vapp_vm_multinetwork_test.go index 80b400cb9..b297ad245 100644 --- a/vcd/resource_vcd_vapp_vm_multinetwork_test.go +++ b/vcd/resource_vcd_vapp_vm_multinetwork_test.go @@ -338,10 +338,6 @@ resource "vcd_vapp_vm" "{{.VMName}}" { ip_allocation_mode = "MANUAL" is_primary = false } - - customization { - force = true - } } ` From 312e25a38d2d42280764acc087457cf6418a700d Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 12 Aug 2019 14:16:31 +0300 Subject: [PATCH 06/31] Working example --- go.mod | 3 +-- go.sum | 4 +-- vcd/resource_vcd_vapp_vm.go | 11 ++++++++ ...resource_vcd_vapp_vm_customization_test.go | 27 +++++++++++++------ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 76d40da5c..71644cecc 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,7 @@ go 1.12 require ( github.com/hashicorp/terraform v0.12.0 - github.com/kr/pretty v0.1.0 github.com/vmware/go-vcloud-director/v2 v2.3.1 ) -replace github.com/vmware/go-vcloud-director/v2 => ../go-vcloud-director +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e diff --git a/go.sum b/go.sum index 1774983e6..7e2cfb462 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0 h1:rVicbbI2vtAvv16beVtNRygmKsgUgcmwfMysaMfQ724= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190807132354-01bd283118b0/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e h1:XiBlVwXQrYGkwFes3ihVSStf42FvkGWm1Ln10eQGFJo= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index bfc5e014a..4a06b7b52 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -659,6 +659,17 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er log.Printf("[TRACE] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) if vmStatus != "POWERED_ON" && customizationNeeded { + + // The VM must be un-deployed for customization to actually work. The option "Power off" in GUI + // actually does un-deploy as well. + task, err := vm.Undeploy() + if err != nil { + return fmt.Errorf("error triggering undeploy for VM %s: %s", vm.VM.Name, err) + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for undeploy task for VM %s: %s", vm.VM.Name, err) + } err = vm.PowerOnAndForceCustomization() if err != nil { return fmt.Errorf("failed powering on with customization: %s", err) diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index dc6b6a283..e57434ccf 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -10,6 +10,11 @@ import ( "testing" ) +// TestAccVcdVAppVmCustomization tests that setting attribute customizaton.force to `true` triggers VM customization +// and waits until it is completed. +// It is important to wait until the operation is completed to test what VM was properly handled before triggering +// power on and force customization. (VM must be un-deployed for customization to work, otherwise it would stay in +// "GC_PENDING" state for long time) func TestAccVcdVAppVmCustomization(t *testing.T) { var ( vapp govcd.VApp @@ -57,7 +62,7 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "0"), ), }, - // Step 1 - Set change network configuration and + // Step 1 - Change network configuration and force customization resource.TestStep{ Config: configTextVMUpdateStep1, Check: resource.ComposeAggregateTestCheckFunc( @@ -112,11 +117,10 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso return err } - // Wait until the VM exits from its original GC_PENDING state after provisioning if it is not expected - // that VM is in that state. This takes some time until the VM boots starts guest tools and - // reports success. + // When force customization was not explicitly triggered - wait until the VM exits from its original GC_PENDING + // state after provisioning. This takes some time until the VM boots starts guest tools and reports success. if !customizationPending { - // Not using maxRetryTimeout here because it would force for maxRetryTimeout to be quite long + // Not using maxRetryTimeout for timeout here because it would force for maxRetryTimeout to be quite long // time by default as it takes some time (around 150s during testing) for Photon OS to boot // first time and get rid of "GC_PENDING" state err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) @@ -124,21 +128,28 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso return err } } - customizationStatus, err := vm.GetGuestCustomizationStatus() if err != nil { return fmt.Errorf("unable to get VM customization status: %s", err) } - // At the stage where "GC_PENDING" should not be set. The state should be something else or this // is an error if !customizationPending && customizationStatus == "GC_PENDING" { return fmt.Errorf("customizationStatus should not be in pending state for vm %s", vm.VM.Name) } + // Customization status of "GC_PENDING" is expected now and it is an error if something else is set if customizationPending && customizationStatus != "GC_PENDING" { - return fmt.Errorf("customizationStatus should not be in pending state for vm %s", vm.VM.Name) + return fmt.Errorf("customizationStatus should be 'GC_PENDING'instead of '%s' for vm %s", + vm.VM.Name, customizationStatus) + } + + if customizationPending && customizationStatus == "GC_PENDING" { + err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) + if err != nil { + return fmt.Errorf("timed out waiting for VM %s to leave 'GC_PENDING' state: %s", vm.VM.Name, err ) + } } return nil From f97e1518837421827835fb2a41bc4bf719a3f355 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 12 Aug 2019 14:20:04 +0300 Subject: [PATCH 07/31] Update website --- website/docs/r/vapp_vm.html.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index 2d45b5c98..9121249d3 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -181,4 +181,5 @@ example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_n * `force` (Optional) This field works as a flag and triggers force customization when `true` during an update (`terraform apply`) every time. It never complains about a change in statefile. It can be used when guest customization -is needed after a NIC change and then set back to `false`. **Note** this setting will cause a VM reboot +is needed after a NIC change and then set back to `false`. **Note** this setting will cause a VM reboot and will not +work when `power_on` field is set to `false`. From 64b4aba223a6a192dacae862ac37e6d42b3d7309 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 12 Aug 2019 14:25:49 +0300 Subject: [PATCH 08/31] make fmt --- vcd/resource_vcd_vapp_vm.go | 8 ++++---- vcd/resource_vcd_vapp_vm_customization_test.go | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 4a06b7b52..8d184911a 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -563,7 +563,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } // Check if the user request for forced reconfiguration of VM - customizationNeeded :=isForcedConfiguration(d.Get("customization")) + customizationNeeded := isForcedConfiguration(d.Get("customization")) if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || d.HasChange("expose_hardware_virtualization") || d.HasChange("network") || customizationNeeded { @@ -676,7 +676,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } - if vmStatus != "POWERED_ON" && !customizationNeeded { + if vmStatus != "POWERED_ON" && !customizationNeeded { task, err := vm.PowerOn() if err != nil { return fmt.Errorf("error powering on: %s", err) @@ -1163,9 +1163,9 @@ func isForcedConfiguration(customizationBlock interface{}) bool { forceCust, ok := fc["force"] forceCustBool := forceCust.(bool) - if !ok || !forceCustBool { + if !ok || !forceCustBool { return false } return true -} \ No newline at end of file +} diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index e57434ccf..f9433c078 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -71,7 +71,6 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "2"), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "true"), ), @@ -138,7 +137,6 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso return fmt.Errorf("customizationStatus should not be in pending state for vm %s", vm.VM.Name) } - // Customization status of "GC_PENDING" is expected now and it is an error if something else is set if customizationPending && customizationStatus != "GC_PENDING" { return fmt.Errorf("customizationStatus should be 'GC_PENDING'instead of '%s' for vm %s", @@ -148,7 +146,7 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso if customizationPending && customizationStatus == "GC_PENDING" { err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) if err != nil { - return fmt.Errorf("timed out waiting for VM %s to leave 'GC_PENDING' state: %s", vm.VM.Name, err ) + return fmt.Errorf("timed out waiting for VM %s to leave 'GC_PENDING' state: %s", vm.VM.Name, err) } } From ae976868623a667fad9862b5bd957240b094d3c5 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 12 Aug 2019 14:29:47 +0300 Subject: [PATCH 09/31] CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7fba78d..bf17cff35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ IMPROVEMENTS: * `vcd_org` Add import capability * `resource/catalog_item` added catalog item metadata support [#28*] * `resource/catalog_media` added catalog media item metadata support [#28*] +* `vcd_vapp_vm` supports update for `network` block [#310] +* `vcd_vapp_vm` allows to force guest customization [#310] BUG FIXES: * Change default value for `vcd_org.deployed_vm_quota` and `vcd_org.stored_vm_quota`. It was incorrectly set at `-1` instead of `0`. From b0ae2aae54933129b64d4a7c42aba2830a61d6d6 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 12 Aug 2019 15:42:54 +0300 Subject: [PATCH 10/31] Pull in govcd --- go.mod | 2 +- go.sum | 4 +- .../vmware/go-vcloud-director/v2/govcd/api.go | 3 +- .../go-vcloud-director/v2/govcd/extension.go | 2 +- .../v2/govcd/lbserverpool.go | 2 +- .../v2/govcd/lbservicemonitor.go | 2 +- .../go-vcloud-director/v2/govcd/media.go | 25 +- .../go-vcloud-director/v2/govcd/metadata.go | 111 ++++++++- .../vmware/go-vcloud-director/v2/govcd/org.go | 21 +- .../go-vcloud-director/v2/govcd/system.go | 232 +++++++++++++++++- .../go-vcloud-director/v2/govcd/user.go | 76 ++++-- .../v2/govcd/vapptemplate.go | 20 ++ .../vmware/go-vcloud-director/v2/govcd/vdc.go | 2 +- vendor/modules.txt | 2 +- 14 files changed, 460 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index d5c03c300..d30af569e 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce diff --git a/go.sum b/go.sum index 7e2cfb462..108020f95 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e h1:XiBlVwXQrYGkwFes3ihVSStf42FvkGWm1Ln10eQGFJo= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce h1:CMyPgThYrsFBGOLalMUebx41/oADG9NwaEDGhCXoStM= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go index ef5a7fb9e..91afc8b7f 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go @@ -42,7 +42,8 @@ type Client struct { // if err == ErrorEntityNotFound { // // do what is needed in case of not found // } -var ErrorEntityNotFound = fmt.Errorf("entity not found") +var errorEntityNotFoundMessage = "[ENF] entity not found" +var ErrorEntityNotFound = fmt.Errorf(errorEntityNotFoundMessage) // Triggers for debugging functions that show requests and responses var debugShowRequestEnabled = os.Getenv("GOVCD_SHOW_REQ") != "" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/extension.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/extension.go index 0c21cd6ec..1995ebed4 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/extension.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/extension.go @@ -9,7 +9,7 @@ import ( "net/http" ) -// DEPRECATED please use GetExternalNetwork function instead +// Deprecated: please use GetExternalNetwork function instead func GetExternalNetworkByName(vcdClient *VCDClient, networkName string) (*types.ExternalNetworkReference, error) { extNetworkRefs := &types.ExternalNetworkReferences{} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbserverpool.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbserverpool.go index d17a635fd..cf2c7b335 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbserverpool.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbserverpool.go @@ -80,7 +80,7 @@ func (egw *EdgeGateway) getLbServerPool(lbPoolConfig *types.LbPool) (*types.LbPo if lbPoolConfig.Name != "" && pool.Name == lbPoolConfig.Name { // We found it by name. Let's verify if search ID was specified and it matches the lookup object if lbPoolConfig.ID != "" && pool.ID != lbPoolConfig.ID { - return nil, fmt.Errorf("load balancer server pool was found by name (%s), but it's ID (%s) does not match specified ID (%s)", + return nil, fmt.Errorf("load balancer server pool was found by name (%s), but its ID (%s) does not match specified ID (%s)", pool.Name, pool.ID, lbPoolConfig.ID) } return pool, nil diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go index f7c97089a..72dbc4699 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go @@ -82,7 +82,7 @@ func (egw *EdgeGateway) getLbServiceMonitor(lbMonitorConfig *types.LbMonitor) (* if lbMonitorConfig.Name != "" && monitor.Name == lbMonitorConfig.Name { // We found it by name. Let's verify if search ID was specified and it matches the lookup object if lbMonitorConfig.ID != "" && monitor.ID != lbMonitorConfig.ID { - return nil, fmt.Errorf("load balancer monitor was found by name (%s), but it's ID (%s) does not match specified ID (%s)", + return nil, fmt.Errorf("load balancer monitor was found by name (%s), but its ID (%s) does not match specified ID (%s)", monitor.Name, monitor.ID, lbMonitorConfig.ID) } return monitor, nil diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go index 5480c282a..ba3daf1aa 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go @@ -21,13 +21,13 @@ import ( type MediaItem struct { MediaItem *types.MediaRecordType - client *Client + vdc *Vdc } -func NewMediaItem(cli *Client) *MediaItem { +func NewMediaItem(vdc *Vdc) *MediaItem { return &MediaItem{ MediaItem: new(types.MediaRecordType), - client: cli, + vdc: vdc, } } @@ -313,7 +313,7 @@ func (mediaItem *MediaItem) Delete() (Task, error) { util.Logger.Printf("[TRACE] Deleting media item: %#v", mediaItem.MediaItem.Name) // Return the task - return mediaItem.client.ExecuteTaskRequest(mediaItem.MediaItem.HREF, http.MethodDelete, + return mediaItem.vdc.client.ExecuteTaskRequest(mediaItem.MediaItem.HREF, http.MethodDelete, "", "error deleting Media item: %s", nil) } @@ -337,3 +337,20 @@ func FindMediaAsCatalogItem(org *Org, catalogName, mediaName string) (CatalogIte } return media, nil } + +// Refresh refreshes the media item information by href +func (mediaItem *MediaItem) Refresh() error { + + if mediaItem.MediaItem == nil { + return fmt.Errorf("cannot refresh, Object is empty") + } + + if mediaItem.MediaItem.Name == "nil" { + return fmt.Errorf("cannot refresh, Name is empty") + } + + latestMediaItem, err := mediaItem.vdc.FindMediaImage(mediaItem.MediaItem.Name) + *mediaItem = latestMediaItem + + return err +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata.go index 5c26a3332..ffb4d5739 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata.go @@ -5,6 +5,7 @@ package govcd import ( + "fmt" "github.com/vmware/go-vcloud-director/v2/types/v56" "net/http" "net/url" @@ -24,7 +25,7 @@ func (vm *VM) DeleteMetadata(key string) (Task, error) { } // AddMetadata() function calls private function addMetadata() with vm.client and vm.VM.HREF -// which adds metadata key, value pair provided as input to VM. +// which adds metadata key/value pair provided as input to VM. func (vm *VM) AddMetadata(key string, value string) (Task, error) { return addMetadata(vm.client, key, value, vm.VM.HREF) } @@ -54,7 +55,7 @@ func (vdc *Vdc) DeleteMetadata(key string) (Vdc, error) { return *vdc, nil } -// AddMetadata() function adds metadata key, value pair provided as input to VDC. +// AddMetadata() function adds metadata key/value pair provided as input to VDC. func (vdc *Vdc) AddMetadata(key string, value string) (Vdc, error) { task, err := addMetadata(vdc.client, key, value, getAdminVdcURL(vdc.Vdc.HREF)) if err != nil { @@ -74,7 +75,7 @@ func (vdc *Vdc) AddMetadata(key string, value string) (Vdc, error) { return *vdc, nil } -// AddMetadata() function adds metadata key, value pair provided as input to VDC. +// AddMetadata() function adds metadata key/value pair provided as input to VDC. // and returns task func (vdc *Vdc) AddMetadataAsync(key string, value string) (Task, error) { return addMetadata(vdc.client, key, value, getAdminVdcURL(vdc.Vdc.HREF)) @@ -123,7 +124,7 @@ func deleteMetadata(client *Client, key string, requestUri string) (Task, error) } // AddMetadata() function calls private function addMetadata() with vapp.client and vapp.VApp.HREF -// which adds metadata key, value pair provided as input. +// which adds metadata key/value pair provided as input func (vapp *VApp) AddMetadata(key string, value string) (Task, error) { return addMetadata(vapp.client, key, value, vapp.VApp.HREF) } @@ -147,3 +148,105 @@ func addMetadata(client *Client, key string, value string, requestUri string) (T return client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut, types.MimeMetaDataValue, "error adding metadata: %s", newMetadata) } + +// GetMetadata() function calls private function getMetadata() with catalogItem.client and catalogItem.CatalogItem.HREF +// which returns a *types.Metadata struct for provided catalog item input. +func (vAppTemplate *VAppTemplate) GetMetadata() (*types.Metadata, error) { + return getMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF) +} + +// AddMetadata() function adds metadata key/value pair provided as input and returned update VAppTemplate +func (vAppTemplate *VAppTemplate) AddMetadata(key string, value string) (*VAppTemplate, error) { + task, err := vAppTemplate.AddMetadataAsync(key, value) + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error completing add metadata for vApp template task: %#v", err) + } + + err = vAppTemplate.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing vApp template: %#v", err) + } + + return vAppTemplate, nil +} + +// AddMetadataAsync() function calls private function addMetadata() with vAppTemplate.client and vAppTemplate.VAppTemplate.HREF +// which adds metadata key/value pair provided as input. +func (vAppTemplate *VAppTemplate) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadata(vAppTemplate.client, key, value, vAppTemplate.VAppTemplate.HREF) +} + +// DeleteMetadata() function calls deletes metadata depending on key provided as input from media item. +func (vAppTemplate *VAppTemplate) DeleteMetadata(key string) error { + task, err := vAppTemplate.DeleteMetadataAsync(key) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error completing delete metadata for vApp template task: %#v", err) + } + + return nil +} + +// DeleteMetadataAsync() function calls private function deleteMetadata() with vAppTemplate.client and vAppTemplate.VAppTemplate.HREF +// which deletes metadata depending on key provided as input from catalog item. +func (vAppTemplate *VAppTemplate) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(vAppTemplate.client, key, vAppTemplate.VAppTemplate.HREF) +} + +// GetMetadata() function calls private function getMetadata() with mediaItem.client and mediaItem.MediaItem.HREF +// which returns a *types.Metadata struct for provided media item input. +func (mediaItem *MediaItem) GetMetadata() (*types.Metadata, error) { + return getMetadata(mediaItem.vdc.client, mediaItem.MediaItem.HREF) +} + +// AddMetadata() function adds metadata key/value pair provided as input. +func (mediaItem *MediaItem) AddMetadata(key string, value string) (*MediaItem, error) { + task, err := mediaItem.AddMetadataAsync(key, value) + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error completing add metadata for media item task: %s", err) + } + + err = mediaItem.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing media item: %s", err) + } + + return mediaItem, nil +} + +// AddMetadataAsync() function calls private function addMetadata() with mediaItem.client and mediaItem.MediaItem.HREF +// which adds metadata key/value pair provided as input. +func (mediaItem *MediaItem) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadata(mediaItem.vdc.client, key, value, mediaItem.MediaItem.HREF) +} + +// DeleteMetadata() function calls deletes metadata depending on key provided as input from media item. +func (mediaItem *MediaItem) DeleteMetadata(key string) error { + task, err := mediaItem.DeleteMetadataAsync(key) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error completing delete metadata for media item task: %s", err) + } + + return nil +} + +// DeleteMetadataAsync() function calls private function deleteMetadata() with mediaItem.client and mediaItem.MediaItem.HREF +// which deletes metadata depending on key provided as input from media item. +func (mediaItem *MediaItem) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(mediaItem.vdc.client, key, mediaItem.MediaItem.HREF) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go index 564321995..a65f1ec63 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go @@ -417,7 +417,7 @@ func (adminOrg *AdminOrg) Delete(force bool, recursive bool) error { // Disable org err := adminOrg.Disable() if err != nil { - return fmt.Errorf("error disabling Org %s: %s", adminOrg.AdminOrg.ID, err) + return fmt.Errorf("error disabling Org %s: %s", adminOrg.AdminOrg.Name, err) } // Get admin HREF orgHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) @@ -428,11 +428,16 @@ func (adminOrg *AdminOrg) Delete(force bool, recursive bool) error { "force": strconv.FormatBool(force), "recursive": strconv.FormatBool(recursive), }, http.MethodDelete, *orgHREF, nil) - _, err = checkResp(adminOrg.client.Http.Do(req)) + resp, err := checkResp(adminOrg.client.Http.Do(req)) if err != nil { return fmt.Errorf("error deleting Org %s: %s", adminOrg.AdminOrg.ID, err) } - return nil + + task := NewTask(adminOrg.client) + if err = decodeBody(resp, task.Task); err != nil { + return fmt.Errorf("error decoding task response: %s", err) + } + return task.WaitTaskCompletion() } // Disables the org. Returns an error if the call to vCD fails. @@ -457,9 +462,19 @@ func (adminOrg *AdminOrg) Update() (Task, error) { Name: adminOrg.AdminOrg.Name, IsEnabled: adminOrg.AdminOrg.IsEnabled, FullName: adminOrg.AdminOrg.FullName, + Description: adminOrg.AdminOrg.Description, OrgSettings: adminOrg.AdminOrg.OrgSettings, } + /**/ + // Same workaround used in Org creation, where OrgGeneralSettings properties + // are not set unless UseServerBootSequence is also set + if vcomp.OrgSettings.OrgGeneralSettings != nil { + vcomp.OrgSettings.OrgGeneralSettings.UseServerBootSequence = true + } + + /**/ + // Return the task return adminOrg.client.ExecuteTaskRequest(adminOrg.AdminOrg.HREF, http.MethodPut, "application/vnd.vmware.admin.organization+xml", "error updating Org: %s", vcomp) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go index 2078013cd..fc97d5168 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go @@ -30,7 +30,7 @@ type EdgeGatewayCreation struct { DistributedRoutingEnabled bool // If advanced networking enabled, also enable distributed routing } -// Creates an Organization based on settings, network, and org name. +// Creates an Admin Organization based on settings, description, and org name. // The Organization created will have these settings specified in the // settings parameter. The settings variable is defined in types.go. // Method will fail unless user has an admin token. @@ -51,6 +51,14 @@ func CreateOrg(vcdClient *VCDClient, name string, fullName string, description s OrgSettings: settings, } + // There is a bug in the settings of CanPublishCatalogs. + // If UseServerBootSequence is not set, CanPublishCatalogs is always false + // regardless of the value passed during creation. + if settings != nil { + if settings.OrgGeneralSettings != nil { + settings.OrgGeneralSettings.UseServerBootSequence = true + } + } orgCreateHREF := vcdClient.Client.VCDHREF orgCreateHREF.Path += "/admin/orgs" @@ -69,7 +77,7 @@ func getBareEntityUuid(entityId string) (string, error) { // Regular expression to match an ID: // 3 strings (alphanumeric + "-") separated by a colon (:) // 1 group of 8 hexadecimal digits - // 3 groups of 4 hexadecimal + // 3 groups of 4 hexadecimal digits // 1 group of 12 hexadecimal digits reGetID := regexp.MustCompile(`^[\w-]+:[\w-]+:[\w-]+:([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$`) matchList := reGetID.FindAllStringSubmatch(entityId, -1) @@ -84,6 +92,24 @@ func getBareEntityUuid(entityId string) (string, error) { return matchList[0][1], nil } +// Returns the UUID part of an HREF +// Similar to getBareEntityUuid, but tailored to HREF +func getUuidFromHref(href string) (string, error) { + // Regular expression to match an ID: + // 1 string starting by 'https://' and ending with a '/', + // followed by + // 1 group of 8 hexadecimal digits + // 3 groups of 4 hexadecimal digits + // 1 group of 12 hexadecimal digits + reGetID := regexp.MustCompile(`^https://.+/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$`) + matchList := reGetID.FindAllStringSubmatch(href, -1) + + if len(matchList) == 0 || len(matchList[0]) < 2 { + return "", fmt.Errorf("error extracting UUID from '%s'", href) + } + return matchList[0][1], nil +} + // CreateEdgeGatewayAsync creates an edge gateway using a simplified configuration structure // https://code.vmware.com/apis/442/vcloud-director/doc/doc/operations/POST-CreateEdgeGateway.html func CreateEdgeGatewayAsync(vcdClient *VCDClient, egwc EdgeGatewayCreation) (Task, error) { @@ -176,7 +202,7 @@ func CreateAndConfigureEdgeGatewayAsync(vcdClient *VCDClient, orgName, vdcName, if egwConfiguration.Name != egwName { return Task{}, fmt.Errorf("name mismatch: '%s' used as parameter but '%s' in the configuration structure", egwName, egwConfiguration.Name) } - adminOrg, err := GetAdminOrgByName(vcdClient, orgName) + adminOrg, err := vcdClient.GetAdminOrgByName(orgName) if err != nil { return Task{}, err } @@ -245,7 +271,7 @@ func createEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation, egwConfig } // The edge gateway is created. Now we retrieve it from the server - org, err := GetAdminOrgByName(vcdClient, egwc.OrgName) + org, err := vcdClient.GetAdminOrgByName(egwc.OrgName) if err != nil { return EdgeGateway{}, err } @@ -274,6 +300,7 @@ func CreateEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation) (EdgeGate // organization object. If no valid org is found, it returns an empty // org and no error. Otherwise it returns an error and an empty // Org object +// Deprecated: Use vcdClient.GetOrgByName instead func GetOrgByName(vcdClient *VCDClient, orgName string) (Org, error) { orgUrl, err := getOrgHREF(vcdClient, orgName) if err != nil { @@ -296,6 +323,7 @@ func GetOrgByName(vcdClient *VCDClient, orgName string) (Org, error) { // org and no error. Otherwise returns an empty AdminOrg // and an error. // API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/GET-Organization-AdminView.html +// Deprecated: Use vcdClient.GetAdminOrgByName instead func GetAdminOrgByName(vcdClient *VCDClient, orgName string) (AdminOrg, error) { orgUrl, err := getOrgHREF(vcdClient, orgName) if err != nil { @@ -337,6 +365,37 @@ func getOrgHREF(vcdClient *VCDClient, orgName string) (string, error) { return "", fmt.Errorf("couldn't find org with name: %s. Please check Org name as it is case sensitive", orgName) } +// Returns the HREF of the org from the org ID +func getOrgHREFById(vcdClient *VCDClient, orgId string) (string, error) { + orgListHREF := vcdClient.Client.VCDHREF + orgListHREF.Path += "/org" + + orgList := new(types.OrgList) + + _, err := vcdClient.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet, + "", "error retrieving org list: %s", nil, orgList) + if err != nil { + return "", err + } + + orgUuid, err := getBareEntityUuid(orgId) + if err != nil { + return "", err + } + // Look for org UUID within OrgList + for _, org := range orgList.Org { + // ID in orgList is usually empty. We extract the UUID from HREF to make the comparison + uuidFromHref, err := getUuidFromHref(org.HREF) + if err != nil { + return "", err + } + if uuidFromHref == orgUuid { + return org.HREF, nil + } + } + return "", fmt.Errorf("couldn't find org with ID: %s", orgId) +} + // Find a list of Virtual Centers matching the filter parameter. // Filter constructing guide: https://pubs.vmware.com/vcloud-api-1-5/wwhelp/wwhimpl/js/html/wwhelp.htm#href=api_prog/GUID-CDF04296-5EB5-47E1-9BEC-228837C584CE.html // Possible parameters are any attribute from QueryResultVirtualCenterRecordType struct @@ -489,7 +548,7 @@ func QueryNetworkPoolByName(vcdCli *VCDClient, name string) ([]*types.QueryResul return results.Results.NetworkPoolRecord, nil } -// QueryNetworkPoolByName finds a provider VDC by name +// QueryProviderVdcByName finds a provider VDC by name func QueryProviderVdcByName(vcdCli *VCDClient, name string) ([]*types.QueryResultVMWProviderVdcRecordType, error) { results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ "type": "providerVdc", @@ -502,6 +561,42 @@ func QueryProviderVdcByName(vcdCli *VCDClient, name string) ([]*types.QueryResul return results.Results.VMWProviderVdcRecord, nil } +// QueryProviderVdcs gets the list of available provider VDCs +func (vcdClient *VCDClient) QueryProviderVdcs() ([]*types.QueryResultVMWProviderVdcRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "providerVdc", + }) + if err != nil { + return nil, err + } + + return results.Results.VMWProviderVdcRecord, nil +} + +// QueryNetworkPools gets the list of network pools +func (vcdClient *VCDClient) QueryNetworkPools() ([]*types.QueryResultNetworkPoolRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "networkPool", + }) + if err != nil { + return nil, err + } + + return results.Results.NetworkPoolRecord, nil +} + +// QueryProviderVdcStorageProfiles gets the list of provider VDC storage profiles +func (vcdClient *VCDClient) QueryProviderVdcStorageProfiles() ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "providerVdcStorageProfile", + }) + if err != nil { + return nil, err + } + + return results.Results.ProviderVdcStorageProfileRecord, nil +} + // GetNetworkPoolByHREF functions fetches an network pool using VDC client and network pool href func GetNetworkPoolByHREF(client *VCDClient, href string) (*types.VMWNetworkPool, error) { util.Logger.Printf("[TRACE] Get network pool by HREF: %s\n", href) @@ -528,3 +623,130 @@ func QueryOrgVdcNetworkByName(vcdCli *VCDClient, name string) ([]*types.QueryRes return results.Results.OrgVdcNetworkRecord, nil } + +// GetOrgByName finds an Organization by name +// On success, returns a pointer to the Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetOrgByName(orgName string) (*Org, error) { + orgUrl, err := getOrgHREF(vcdClient, orgName) + if err != nil { + // Since this operation is a lookup from a list, we return the standard ErrorEntityNotFound + return nil, ErrorEntityNotFound + } + org := NewOrg(&vcdClient.Client) + + _, err = vcdClient.Client.ExecuteRequest(orgUrl, http.MethodGet, + "", "error retrieving org list: %s", nil, org.Org) + if err != nil { + return nil, err + } + + return org, nil +} + +// GetOrgById finds an Organization by ID +// On success, returns a pointer to the Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetOrgById(orgId string) (*Org, error) { + orgUrl, err := getOrgHREFById(vcdClient, orgId) + if err != nil { + // Since this operation is a lookup from a list, we return the standard ErrorEntityNotFound + return nil, ErrorEntityNotFound + } + org := NewOrg(&vcdClient.Client) + + _, err = vcdClient.Client.ExecuteRequest(orgUrl, http.MethodGet, + "", "error retrieving org list: %s", nil, org.Org) + if err != nil { + return nil, err + } + + return org, nil +} + +// GetOrgByNameOrId finds an Organization by name or ID +// On success, returns a pointer to the Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetOrgByNameOrId(identifier string) (*Org, error) { + var byNameErr, byIdErr error + var org *Org + org, byIdErr = vcdClient.GetOrgById(identifier) + if byIdErr == nil { + // Found by ID + return org, nil + } + if IsNotFound(byIdErr) { + // Not found by ID, try by name + org, byNameErr = vcdClient.GetOrgByName(identifier) + return org, byNameErr + } else { + // On any other error, we return it + return nil, byIdErr + } +} + +// GetAdminOrgByName finds an Admin Organization by name +// On success, returns a pointer to the Admin Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetAdminOrgByName(orgName string) (*AdminOrg, error) { + orgUrl, err := getOrgHREF(vcdClient, orgName) + if err != nil { + return nil, ErrorEntityNotFound + } + orgHREF := vcdClient.Client.VCDHREF + orgHREF.Path += "/admin/org/" + strings.Split(orgUrl, "/api/org/")[1] + + adminOrg := NewAdminOrg(&vcdClient.Client) + + _, err = vcdClient.Client.ExecuteRequest(orgHREF.String(), http.MethodGet, + "", "error retrieving org: %s", nil, adminOrg.AdminOrg) + if err != nil { + return nil, err + } + + return adminOrg, nil +} + +// GetAdminOrgById finds an Admin Organization by ID +// On success, returns a pointer to the Admin Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetAdminOrgById(orgId string) (*AdminOrg, error) { + orgUrl, err := getOrgHREFById(vcdClient, orgId) + if err != nil { + return nil, ErrorEntityNotFound + } + orgHREF := vcdClient.Client.VCDHREF + orgHREF.Path += "/admin/org/" + strings.Split(orgUrl, "/api/org/")[1] + + adminOrg := NewAdminOrg(&vcdClient.Client) + + _, err = vcdClient.Client.ExecuteRequest(orgHREF.String(), http.MethodGet, + "", "error retrieving org: %s", nil, adminOrg.AdminOrg) + if err != nil { + return nil, err + } + + return adminOrg, nil +} + +// GetAdminOrgByNameOrId finds an Admin Organization by name or ID +// On success, returns a pointer to the Admin Org structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetAdminOrgByNameOrId(identifier string) (*AdminOrg, error) { + var byNameErr, byIdErr error + var adminOrg *AdminOrg + + adminOrg, byIdErr = vcdClient.GetAdminOrgById(identifier) + if byIdErr == nil { + // Found by ID + return adminOrg, nil + } + if IsNotFound(byIdErr) { + // Not found by ID, try by name + adminOrg, byNameErr = vcdClient.GetAdminOrgByName(identifier) + return adminOrg, byNameErr + } else { + // On any other error, we return it + return nil, byIdErr + } +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go index 8a86e807f..06e0bf25a 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go @@ -71,9 +71,33 @@ func NewUser(cli *Client, org *AdminOrg) *OrgUser { } } -// FetchUserByHref returns a user by its HREF, without need for -// searching in the adminOrg user list +// FetchUserByHref returns a user by its HREF +// Deprecated: use GetUserByHref instead func (adminOrg *AdminOrg) FetchUserByHref(href string) (*OrgUser, error) { + return adminOrg.GetUserByHref(href) +} + +// FetchUserByName returns a user by its Name +// Deprecated: use GetUserByName instead +func (adminOrg *AdminOrg) FetchUserByName(name string, refresh bool) (*OrgUser, error) { + return adminOrg.GetUserByName(name, refresh) +} + +// FetchUserById returns a user by its ID +// Deprecated: use GetUserById instead +func (adminOrg *AdminOrg) FetchUserById(id string, refresh bool) (*OrgUser, error) { + return adminOrg.GetUserById(id, refresh) +} + +// FetchUserById returns a user by its Name or ID +// Deprecated: use GetUserByNameOrId instead +func (adminOrg *AdminOrg) FetchUserByNameOrId(identifier string, refresh bool) (*OrgUser, error) { + return adminOrg.GetUserByNameOrId(identifier, refresh) +} + +// GetUserByHref returns a user by its HREF, without need for +// searching in the adminOrg user list +func (adminOrg *AdminOrg) GetUserByHref(href string) (*OrgUser, error) { orgUser := NewUser(adminOrg.client, adminOrg) _, err := adminOrg.client.ExecuteRequest(href, http.MethodGet, @@ -85,13 +109,13 @@ func (adminOrg *AdminOrg) FetchUserByHref(href string) (*OrgUser, error) { return orgUser, nil } -// FetchUserByName retrieves a user within an admin organization by name +// GetUserByName retrieves a user within an admin organization by name // Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound // If argument refresh is true, the AdminOrg will be refreshed before searching. // This is usually done after creating, modifying, or deleting users. // If it is false, it will search within the data already in memory (useful when // looping through the users and we know that no changes have occurred in the meantime) -func (adminOrg *AdminOrg) FetchUserByName(name string, refresh bool) (*OrgUser, error) { +func (adminOrg *AdminOrg) GetUserByName(name string, refresh bool) (*OrgUser, error) { if refresh { err := adminOrg.Refresh() if err != nil { @@ -101,19 +125,19 @@ func (adminOrg *AdminOrg) FetchUserByName(name string, refresh bool) (*OrgUser, for _, user := range adminOrg.AdminOrg.Users.User { if user.Name == name { - return adminOrg.FetchUserByHref(user.HREF) + return adminOrg.GetUserByHref(user.HREF) } } return nil, ErrorEntityNotFound } -// FetchUserById retrieves a user within an admin organization by ID +// GetUserById retrieves a user within an admin organization by ID // Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound // If argument refresh is true, the AdminOrg will be refreshed before searching. // This is usually done after creating, modifying, or deleting users. // If it is false, it will search within the data already in memory (useful when // looping through the users and we know that no changes have occurred in the meantime) -func (adminOrg *AdminOrg) FetchUserById(id string, refresh bool) (*OrgUser, error) { +func (adminOrg *AdminOrg) GetUserById(id string, refresh bool) (*OrgUser, error) { if refresh { err := adminOrg.Refresh() if err != nil { @@ -123,31 +147,45 @@ func (adminOrg *AdminOrg) FetchUserById(id string, refresh bool) (*OrgUser, erro for _, user := range adminOrg.AdminOrg.Users.User { if user.ID == id { - return adminOrg.FetchUserByHref(user.HREF) + return adminOrg.GetUserByHref(user.HREF) } } return nil, ErrorEntityNotFound } -// FetchUserByNameOrId retrieves a user within an admin organization +// GetUserByNameOrId retrieves a user within an admin organization // by either name or ID // Returns a valid user if it exists. If it doesn't, returns nil and ErrorEntityNotFound // If argument refresh is true, the AdminOrg will be refreshed before searching. // This is usually done after creating, modifying, or deleting users. // If it is false, it will search within the data already in memory (useful when // looping through the users and we know that no changes have occurred in the meantime) -func (adminOrg *AdminOrg) FetchUserByNameOrId(identifier string, refresh bool) (*OrgUser, error) { +func (adminOrg *AdminOrg) GetUserByNameOrId(identifier string, refresh bool) (*OrgUser, error) { + var byNameErr, byIdErr error // First look by ID - orgUser, err := adminOrg.FetchUserByName(identifier, true) - // if it fails, look by name - if IsNotFound(err) { - orgUser, err = adminOrg.FetchUserById(identifier, false) + orgUser, byIdErr := adminOrg.GetUserByName(identifier, true) + if byIdErr == nil { + // found by ID + return orgUser, nil + } + // If not found by ID, look by name + if IsNotFound(byIdErr) { + orgUser, byNameErr = adminOrg.GetUserById(identifier, false) + return orgUser, byNameErr + } else { + // On any other error, we return it + return nil, byIdErr } - return orgUser, err } // GetRole finds a role within the organization +// Deprecated: use GetRoleReference func (adminOrg *AdminOrg) GetRole(roleName string) (*types.Reference, error) { + return adminOrg.GetRoleReference(roleName) +} + +// GetRoleReference finds a role within the organization +func (adminOrg *AdminOrg) GetRoleReference(roleName string) (*types.Reference, error) { // There is no need to refresh the AdminOrg, until we implement CRUD for roles for _, role := range adminOrg.AdminOrg.RoleReferences.RoleReference { @@ -183,7 +221,7 @@ func retrieveUserWithTimeout(adminOrg *AdminOrg, userName string) (*OrgUser, err var newUser *OrgUser var err error for elapsed < maxOperationTimeout { - newUser, err = adminOrg.FetchUserByName(userName, true) + newUser, err = adminOrg.GetUserByName(userName, true) if err == nil { break } @@ -257,7 +295,7 @@ func (adminOrg *AdminOrg) CreateUserSimple(userData OrgUserConfiguration) (*OrgU if userData.RoleName == "" { return nil, fmt.Errorf("role is mandatory to create a user") } - role, err := adminOrg.GetRole(userData.RoleName) + role, err := adminOrg.GetRoleReference(userData.RoleName) if err != nil { return nil, fmt.Errorf("error finding a role named %s", userData.RoleName) } @@ -354,7 +392,7 @@ func (user *OrgUser) UpdateSimple(userData OrgUserConfiguration) error { user.User.IsLocked = userData.IsLocked if userData.RoleName != "" && user.User.Role != nil && user.User.Role.Name != userData.RoleName { - newRole, err := user.AdminOrg.GetRole(userData.RoleName) + newRole, err := user.AdminOrg.GetRoleReference(userData.RoleName) if err != nil { return err } @@ -451,7 +489,7 @@ func (user *OrgUser) ChangeRole(roleName string) error { return fmt.Errorf("new role is the same as current role") } - newRole, err := user.AdminOrg.GetRole(roleName) + newRole, err := user.AdminOrg.GetRoleReference(roleName) if err != nil { return err } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go index 825cfc645..541f9bdf8 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go @@ -49,3 +49,23 @@ func (vdc *Vdc) InstantiateVAppTemplate(template *types.InstantiateVAppTemplateP } return nil } + +// Refresh refreshes the vApp template item information by href +func (vAppTemplate *VAppTemplate) Refresh() error { + + if vAppTemplate.VAppTemplate == nil { + return fmt.Errorf("cannot refresh, Object is empty") + } + + url := vAppTemplate.VAppTemplate.HREF + if url == "nil" { + return fmt.Errorf("cannot refresh, HREF is empty") + } + + vAppTemplate.VAppTemplate = &types.VAppTemplate{} + + _, err := vAppTemplate.client.ExecuteRequest(url, http.MethodGet, + "", "error retrieving vApp template item: %s", nil, vAppTemplate.VAppTemplate) + + return err +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go index ef3ab3da7..1b25d2a99 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go @@ -562,7 +562,7 @@ func (vdc *Vdc) FindMediaImage(mediaName string) (MediaItem, error) { return MediaItem{}, err } - newMediaItem := NewMediaItem(vdc.client) + newMediaItem := NewMediaItem(vdc) if len(mediaResults) == 1 { newMediaItem.MediaItem = mediaResults[0] diff --git a/vendor/modules.txt b/vendor/modules.txt index edd5b78f4..927248926 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -216,7 +216,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812111525-4de0a214832e +# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util From 2d4d76287c7d8b45510e0f7f3a0e3fe82be2f10e Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 14 Aug 2019 15:48:27 +0300 Subject: [PATCH 11/31] bump govcd --- go.mod | 2 +- go.sum | 4 +-- .../vmware/go-vcloud-director/v2/govcd/vm.go | 36 ++++++++++++++++--- .../v2/types/v56/constants.go | 9 +++++ vendor/modules.txt | 2 +- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index d30af569e..c06bdbc95 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 diff --git a/go.sum b/go.sum index 108020f95..c5e873165 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce h1:CMyPgThYrsFBGOLalMUebx41/oADG9NwaEDGhCXoStM= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 h1:PlM3cSVtmTtQmAaIHYI+TtRpNmeu05O/O9oij0x1svk= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index 81f0fd492..c6735905b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -49,6 +49,15 @@ func (vm *VM) GetStatus() (string, error) { return types.VAppStatuses[vm.VM.Status], nil } +// IsDeployed checks if the VM is deployed or not +func (vm *VM) IsDeployed() (bool, error) { + err := vm.Refresh() + if err != nil { + return false, fmt.Errorf("error refreshing VM: %v", err) + } + return vm.VM.Deployed, nil +} + func (vm *VM) Refresh() error { if vm.VM.HREF == "" { @@ -76,7 +85,7 @@ func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, er networkConnectionSection := &types.NetworkConnectionSection{} if vm.VM.HREF == "" { - return networkConnectionSection, fmt.Errorf("cannot refresh, Object is empty") + return networkConnectionSection, fmt.Errorf("cannot retrieve network when VM HREF is unset") } _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodGet, @@ -90,7 +99,7 @@ func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, er // Runs synchronously, VM is ready for another operation after this function returns. func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSection) error { if vm.VM.HREF == "" { - return fmt.Errorf("cannot refresh, Object is empty") + return fmt.Errorf("cannot update network connection when VM HREF is unset") } // Retrieve current network configuration so that we are not altering any other internal fields @@ -140,8 +149,19 @@ func (vm *VM) PowerOn() (Task, error) { // PowerOnAndForceCustomization is a synchronous function which is equivalent to the functionality // one has in UI. It triggers customization which may be useful in some cases (like altering NICs) // -// The VM _must_ be Undeployed for this action to actually work. +// The VM _must_ be un-deployed for this action to actually work. func (vm *VM) PowerOnAndForceCustomization() error { + // PowerOnAndForceCustomization only works if the VM was previously un-deployed + vmIsDeployed, err := vm.IsDeployed() + if err != nil { + return fmt.Errorf("unable to check if VM %s is un-deployed forcing customization: %s", + vm.VM.Name, err) + } + + if vmIsDeployed { + return fmt.Errorf("VM %s must be undeployed before forcing customization", vm.VM.Name) + } + apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/action/deploy" @@ -361,11 +381,13 @@ func (vm *VM) RunCustomizationScript(computername, script string) (Task, error) return vm.Customize(computername, script, false) } +// GetGuestCustomizationStatus retrieves guest customization status. +// It can be one of "GC_PENDING", "REBOOT_PENDING", "GC_FAILED", "POST_GC_PENDING", "GC_COMPLETE" func (vm *VM) GetGuestCustomizationStatus() (string, error) { guestCustomizationStatus := &types.GuestCustomizationStatusSection{} if vm.VM.HREF == "" { - return "", fmt.Errorf("cannot load guest customization, VM HREF is empty") + return "", fmt.Errorf("cannot retrieve guest customization, VM HREF is empty") } _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestcustomizationstatus", http.MethodGet, @@ -377,7 +399,13 @@ func (vm *VM) GetGuestCustomizationStatus() (string, error) { // BlockWhileGuestCustomizationStatus blocks until the customization status of VM exits unwantedStatus. // It sleeps 3 seconds between iterations and times out after timeOutAfterSeconds of seconds. +// +// timeOutAfterSeconds must be more than 4 and less than 2 hours (60s*120) func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutAfterSeconds int) error { + if timeOutAfterSeconds < 5 || timeOutAfterSeconds > 60*120 { + return fmt.Errorf("timeOutAfterSeconds must be in range 4 github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190812122912-df0cdfbbffce +# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util From 82416f462008fbf0cffe7c7c60be76adde0bb7ba Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 14 Aug 2019 18:14:10 +0300 Subject: [PATCH 12/31] bump govcd --- go.mod | 2 +- go.sum | 4 +- .../v2/govcd/admincatalog.go | 86 ++ .../go-vcloud-director/v2/govcd/adminorg.go | 744 ++++++++++++++++++ .../go-vcloud-director/v2/govcd/adminvdc.go | 84 ++ .../go-vcloud-director/v2/govcd/catalog.go | 150 ++-- .../go-vcloud-director/v2/govcd/entity.go | 45 ++ .../vmware/go-vcloud-director/v2/govcd/org.go | 561 +++---------- .../go-vcloud-director/v2/govcd/system.go | 138 +++- .../go-vcloud-director/v2/govcd/user.go | 20 +- .../vmware/go-vcloud-director/v2/govcd/vdc.go | 13 - vendor/modules.txt | 2 +- 12 files changed, 1266 insertions(+), 583 deletions(-) create mode 100644 vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go create mode 100644 vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go create mode 100644 vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go create mode 100644 vendor/github.com/vmware/go-vcloud-director/v2/govcd/entity.go diff --git a/go.mod b/go.mod index 97b0255be..ed639e8b4 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb diff --git a/go.sum b/go.sum index 83f386d24..2ac0ef10d 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 h1:PlM3cSVtmTtQmAaIHYI+TtRpNmeu05O/O9oij0x1svk= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb h1:mwKcO3Bj5wVEJ+X513CK+ggqWKhFtCwvt/2+BJLKkH4= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go new file mode 100644 index 000000000..8a63835fe --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go @@ -0,0 +1,86 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// AdminCatalog is a admin view of a vCloud Director Catalog +// To be able to get an AdminCatalog representation, users must have +// admin credentials to the System org. AdminCatalog is used +// for creating, updating, and deleting a Catalog. +// Definition: https://code.vmware.com/apis/220/vcloud#/doc/doc/types/AdminCatalogType.html +type AdminCatalog struct { + AdminCatalog *types.AdminCatalog + client *Client +} + +func NewAdminCatalog(client *Client) *AdminCatalog { + return &AdminCatalog{ + AdminCatalog: new(types.AdminCatalog), + client: client, + } +} + +// Deletes the Catalog, returning an error if the vCD call fails. +// Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html +func (adminCatalog *AdminCatalog) Delete(force, recursive bool) error { + catalog := NewCatalog(adminCatalog.client) + catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + return catalog.Delete(force, recursive) +} + +// Updates the Catalog definition from current Catalog struct contents. +// Any differences that may be legally applied will be updated. +// Returns an error if the call to vCD fails. Update automatically performs +// a refresh with the admin catalog it gets back from the rest api +// Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Catalog.html +func (adminCatalog *AdminCatalog) Update() error { + reqCatalog := &types.Catalog{ + Name: adminCatalog.AdminCatalog.Catalog.Name, + Description: adminCatalog.AdminCatalog.Description, + } + vcomp := &types.AdminCatalog{ + Xmlns: types.XMLNamespaceVCloud, + Catalog: *reqCatalog, + IsPublished: adminCatalog.AdminCatalog.IsPublished, + } + catalog := &types.AdminCatalog{} + _, err := adminCatalog.client.ExecuteRequest(adminCatalog.AdminCatalog.HREF, http.MethodPut, + "application/vnd.vmware.admin.catalog+xml", "error updating catalog: %s", vcomp, catalog) + adminCatalog.AdminCatalog = catalog + return err +} + +// Uploads an ova file to a catalog. This method only uploads bits to vCD spool area. +// Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to +// remove vCD catalog item which waits for files to be uploaded. Files from ova are extracted to system +// temp folder "govcd+random number" and left for inspection on error. +func (adminCatalog *AdminCatalog) UploadOvf(ovaFileName, itemName, description string, uploadPieceSize int64) (UploadTask, error) { + catalog := NewCatalog(adminCatalog.client) + catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + return catalog.UploadOvf(ovaFileName, itemName, description, uploadPieceSize) +} + +func (adminCatalog *AdminCatalog) Refresh() error { + if *adminCatalog == (AdminCatalog{}) || adminCatalog.AdminCatalog.HREF == "" { + return fmt.Errorf("cannot refresh, Object is empty or HREF is empty") + } + + refreshedCatalog := &types.AdminCatalog{} + + _, err := adminCatalog.client.ExecuteRequest(adminCatalog.AdminCatalog.HREF, http.MethodGet, + "", "error refreshing VDC: %s", nil, refreshedCatalog) + if err != nil { + return err + } + adminCatalog.AdminCatalog = refreshedCatalog + + return nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go new file mode 100644 index 000000000..67e64b1d8 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go @@ -0,0 +1,744 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// AdminOrg gives an admin representation of an org. +// Administrators can delete and update orgs with an admin org object. +// AdminOrg includes all members of the Org element, and adds several +// elements that can be viewed and modified only by system administrators. +// Definition: https://code.vmware.com/apis/220/vcloud#/doc/doc/types/AdminOrgType.html +type AdminOrg struct { + AdminOrg *types.AdminOrg + client *Client +} + +func NewAdminOrg(cli *Client) *AdminOrg { + return &AdminOrg{ + AdminOrg: new(types.AdminOrg), + client: cli, + } +} + +// GetAdminVdcByName function uses a valid VDC name and returns a admin VDC object. +// If no VDC is found, then it returns an empty VDC and no error. +// Otherwise it returns an empty VDC and an error. +// Deprecated: Use adminOrg.GetAdminVDCByName +func (adminOrg *AdminOrg) GetAdminVdcByName(vdcname string) (AdminVdc, error) { + for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { + if vdcs.Name == vdcname { + adminVdc := NewAdminVdc(adminOrg.client) + _, err := adminOrg.client.ExecuteRequest(vdcs.HREF, http.MethodGet, + "", "error getting vdc: %s", nil, adminVdc.AdminVdc) + return *adminVdc, err + } + } + return AdminVdc{}, nil +} + +// CreateCatalog creates a catalog with given name and description under the +// the given organization. Returns an AdminCatalog that contains a creation +// task. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html +func (adminOrg *AdminOrg) CreateCatalog(name, description string) (AdminCatalog, error) { + return CreateCatalog(adminOrg.client, adminOrg.AdminOrg.Link, name, description) +} + +// If user specifies valid vdc name then this returns a vdc object. +// If no vdc is found, then it returns an empty vdc and no error. +// Otherwise it returns an empty vdc and an error. This function +// allows users to use an AdminOrg to fetch a vdc as well. +// Deprecated: Use adminOrg.GetVDCByName instead +func (adminOrg *AdminOrg) GetVdcByName(vdcname string) (Vdc, error) { + for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { + if vdcs.Name == vdcname { + splitByAdminHREF := strings.Split(vdcs.HREF, "/api/admin") + + // admin user and normal user will have different urls + var vdcHREF string + if len(splitByAdminHREF) == 1 { + vdcHREF = vdcs.HREF + } else { + vdcHREF = splitByAdminHREF[0] + "/api" + splitByAdminHREF[1] + } + + vdc := NewVdc(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(vdcHREF, http.MethodGet, + "", "error getting vdc: %s", nil, vdc.Vdc) + + return *vdc, err + } + } + return Vdc{}, nil +} + +// CreateVdc creates a VDC with the given params under the given organization. +// Returns an AdminVdc. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-VdcConfiguration.html +func (adminOrg *AdminOrg) CreateVdc(vdcConfiguration *types.VdcConfiguration) (Task, error) { + err := validateVdcConfiguration(vdcConfiguration) + if err != nil { + return Task{}, err + } + + vdcCreateHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) + if err != nil { + return Task{}, fmt.Errorf("error parsing admin org url: %s", err) + } + vdcCreateHREF.Path += "/vdcsparams" + + adminVdc := NewAdminVdc(adminOrg.client) + + _, err = adminOrg.client.ExecuteRequest(vdcCreateHREF.String(), http.MethodPost, + "application/vnd.vmware.admin.createVdcParams+xml", "error retrieving vdc: %s", vdcConfiguration, adminVdc.AdminVdc) + if err != nil { + return Task{}, err + } + + // Return the task + task := NewTask(adminOrg.client) + task.Task = adminVdc.AdminVdc.Tasks.Task[0] + return *task, nil +} + +// Creates the vdc and waits for the asynchronous task to complete. +func (adminOrg *AdminOrg) CreateVdcWait(vdcDefinition *types.VdcConfiguration) error { + task, err := adminOrg.CreateVdc(vdcDefinition) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("couldn't finish creating vdc %#v", err) + } + return nil +} + +// Deletes the org, returning an error if the vCD call fails. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Organization.html +func (adminOrg *AdminOrg) Delete(force bool, recursive bool) error { + if force && recursive { + //undeploys vapps + err := adminOrg.undeployAllVApps() + if err != nil { + return fmt.Errorf("error could not undeploy: %#v", err) + } + //removes vapps + err = adminOrg.removeAllVApps() + if err != nil { + return fmt.Errorf("error could not remove vapp: %#v", err) + } + //removes catalogs + err = adminOrg.removeCatalogs() + if err != nil { + return fmt.Errorf("error could not remove all catalogs: %#v", err) + } + //removes networks + err = adminOrg.removeAllOrgNetworks() + if err != nil { + return fmt.Errorf("error could not remove all networks: %#v", err) + } + //removes org vdcs + err = adminOrg.removeAllOrgVDCs() + if err != nil { + return fmt.Errorf("error could not remove all vdcs: %#v", err) + } + } + // Disable org + err := adminOrg.Disable() + if err != nil { + return fmt.Errorf("error disabling Org %s: %s", adminOrg.AdminOrg.Name, err) + } + // Get admin HREF + orgHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) + if err != nil { + return fmt.Errorf("error getting AdminOrg HREF %s : %v", adminOrg.AdminOrg.HREF, err) + } + req := adminOrg.client.NewRequest(map[string]string{ + "force": strconv.FormatBool(force), + "recursive": strconv.FormatBool(recursive), + }, http.MethodDelete, *orgHREF, nil) + resp, err := checkResp(adminOrg.client.Http.Do(req)) + if err != nil { + return fmt.Errorf("error deleting Org %s: %s", adminOrg.AdminOrg.ID, err) + } + + task := NewTask(adminOrg.client) + if err = decodeBody(resp, task.Task); err != nil { + return fmt.Errorf("error decoding task response: %s", err) + } + return task.WaitTaskCompletion() +} + +// Disables the org. Returns an error if the call to vCD fails. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-DisableOrg.html +func (adminOrg *AdminOrg) Disable() error { + orgHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) + if err != nil { + return fmt.Errorf("error getting AdminOrg HREF %s : %v", adminOrg.AdminOrg.HREF, err) + } + orgHREF.Path += "/action/disable" + + return adminOrg.client.ExecuteRequestWithoutResponse(orgHREF.String(), http.MethodPost, "", "error disabling organization: %s", nil) +} + +// Updates the Org definition from current org struct contents. +// Any differences that may be legally applied will be updated. +// Returns an error if the call to vCD fails. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Organization.html +func (adminOrg *AdminOrg) Update() (Task, error) { + vcomp := &types.AdminOrg{ + Xmlns: types.XMLNamespaceVCloud, + Name: adminOrg.AdminOrg.Name, + IsEnabled: adminOrg.AdminOrg.IsEnabled, + FullName: adminOrg.AdminOrg.FullName, + Description: adminOrg.AdminOrg.Description, + OrgSettings: adminOrg.AdminOrg.OrgSettings, + } + + // Same workaround used in Org creation, where OrgGeneralSettings properties + // are not set unless UseServerBootSequence is also set + if vcomp.OrgSettings.OrgGeneralSettings != nil { + vcomp.OrgSettings.OrgGeneralSettings.UseServerBootSequence = true + } + + // Return the task + return adminOrg.client.ExecuteTaskRequest(adminOrg.AdminOrg.HREF, http.MethodPut, + "application/vnd.vmware.admin.organization+xml", "error updating Org: %s", vcomp) +} + +// Undeploys every vapp within an organization +func (adminOrg *AdminOrg) undeployAllVApps() error { + for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { + adminVdcHREF, err := url.Parse(vdcs.HREF) + if err != nil { + return err + } + vdc, err := adminOrg.getVdcByAdminHREF(adminVdcHREF) + if err != nil { + return fmt.Errorf("error retrieving vapp with url: %s and with error %s", adminVdcHREF.Path, err) + } + err = vdc.undeployAllVdcVApps() + if err != nil { + return fmt.Errorf("error deleting vapp: %s", err) + } + } + return nil +} + +// Deletes every vapp within an organization +func (adminOrg *AdminOrg) removeAllVApps() error { + for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { + adminVdcHREF, err := url.Parse(vdcs.HREF) + if err != nil { + return err + } + vdc, err := adminOrg.getVdcByAdminHREF(adminVdcHREF) + if err != nil { + return fmt.Errorf("error retrieving vapp with url: %s and with error %s", adminVdcHREF.Path, err) + } + err = vdc.removeAllVdcVApps() + if err != nil { + return fmt.Errorf("error deleting vapp: %s", err) + } + } + return nil +} + +// Given an adminorg with a valid HREF, the function refetches the adminorg +// and updates the user's adminorg data. Otherwise if the function fails, +// it returns an error. Users should use refresh whenever they have +// a stale org due to the creation/update/deletion of a resource +// within the org or the org itself. +func (adminOrg *AdminOrg) Refresh() error { + if *adminOrg == (AdminOrg{}) { + return fmt.Errorf("cannot refresh, Object is empty") + } + + // Empty struct before a new unmarshal, otherwise we end up with duplicate + // elements in slices. + unmarshalledAdminOrg := &types.AdminOrg{} + + _, err := adminOrg.client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, + "", "error refreshing organization: %s", nil, unmarshalledAdminOrg) + if err != nil { + return err + } + adminOrg.AdminOrg = unmarshalledAdminOrg + + return nil +} + +// Gets a vdc within org associated with an admin vdc url +func (adminOrg *AdminOrg) getVdcByAdminHREF(adminVdcUrl *url.URL) (*Vdc, error) { + // get non admin vdc path + vdcURL := adminOrg.client.VCDHREF + vdcURL.Path += strings.Split(adminVdcUrl.Path, "/api/admin")[1] //gets id + + vdc := NewVdc(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(vdcURL.String(), http.MethodGet, + "", "error retrieving vdc: %s", nil, vdc.Vdc) + + return vdc, err +} + +// Removes all vdcs in a org +func (adminOrg *AdminOrg) removeAllOrgVDCs() error { + for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { + + // Get admin Vdc HREF + adminVdcUrl := adminOrg.client.VCDHREF + adminVdcUrl.Path += "/admin/vdc/" + strings.Split(vdcs.HREF, "/api/vdc/")[1] + "/action/disable" + + req := adminOrg.client.NewRequest(map[string]string{}, http.MethodPost, adminVdcUrl, nil) + _, err := checkResp(adminOrg.client.Http.Do(req)) + if err != nil { + return fmt.Errorf("error disabling vdc: %s", err) + } + // Get admin vdc HREF for normal deletion + adminVdcUrl.Path = strings.Split(adminVdcUrl.Path, "/action/disable")[0] + req = adminOrg.client.NewRequest(map[string]string{ + "recursive": "true", + "force": "true", + }, http.MethodDelete, adminVdcUrl, nil) + resp, err := checkResp(adminOrg.client.Http.Do(req)) + if err != nil { + return fmt.Errorf("error deleting vdc: %s", err) + } + task := NewTask(adminOrg.client) + if err = decodeBody(resp, task.Task); err != nil { + return fmt.Errorf("error decoding task response: %s", err) + } + if task.Task.Status == "error" { + return fmt.Errorf("vdc not properly destroyed") + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("couldn't finish removing vdc %#v", err) + } + + } + + return nil +} + +// Removes All networks in the org +func (adminOrg *AdminOrg) removeAllOrgNetworks() error { + for _, networks := range adminOrg.AdminOrg.Networks.Networks { + // Get Network HREF + networkHREF := adminOrg.client.VCDHREF + networkHREF.Path += "/admin/network/" + strings.Split(networks.HREF, "/api/admin/network/")[1] //gets id + + task, err := adminOrg.client.ExecuteTaskRequest(networkHREF.String(), http.MethodDelete, + "", "error deleting network: %s", nil) + if err != nil { + return err + } + + if task.Task.Status == "error" { + return fmt.Errorf("network not properly destroyed") + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("couldn't finish removing network %#v", err) + } + } + return nil +} + +// Forced removal of all organization catalogs +func (adminOrg *AdminOrg) removeCatalogs() error { + for _, catalogs := range adminOrg.AdminOrg.Catalogs.Catalog { + // Get Catalog HREF + catalogHREF := adminOrg.client.VCDHREF + catalogHREF.Path += "/admin/catalog/" + strings.Split(catalogs.HREF, "/api/admin/catalog/")[1] //gets id + req := adminOrg.client.NewRequest(map[string]string{ + "force": "true", + "recursive": "true", + }, http.MethodDelete, catalogHREF, nil) + _, err := checkResp(adminOrg.client.Http.Do(req)) + if err != nil { + return fmt.Errorf("error deleting catalog: %s, %s", err, catalogHREF.Path) + } + } + return nil + +} + +// Given a valid catalog name, FindAdminCatalog returns an AdminCatalog object. +// If no catalog is found, then returns an empty AdminCatalog and no error. +// Otherwise it returns an error. Function allows user to use an AdminOrg +// to also fetch a Catalog. If user does not have proper credentials to +// perform administrator tasks then function returns an error. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/GET-Catalog-AdminView.html +// Deprecated: Use adminOrg.GetAdminCatalog instead +func (adminOrg *AdminOrg) FindAdminCatalog(catalogName string) (AdminCatalog, error) { + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + // Get Catalog HREF + if catalog.Name == catalogName { + adminCatalog := NewAdminCatalog(adminOrg.client) + _, err := adminOrg.client.ExecuteRequest(catalog.HREF, http.MethodGet, + "", "error retrieving catalog: %s", nil, adminCatalog.AdminCatalog) + // The request was successful + return *adminCatalog, err + } + } + return AdminCatalog{}, nil +} + +// Given a valid catalog name, FindCatalog returns a Catalog object. +// If no catalog is found, then returns an empty catalog and no error. +// Otherwise it returns an error. Function allows user to use an AdminOrg +// to also fetch a Catalog. +// Deprecated: Use adminOrg.GetCatalogByName instead +func (adminOrg *AdminOrg) FindCatalog(catalogName string) (Catalog, error) { + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + // Get Catalog HREF + if catalog.Name == catalogName { + catalogURL := adminOrg.client.VCDHREF + catalogURL.Path += "/catalog/" + strings.Split(catalog.HREF, "/api/admin/catalog/")[1] //gets id + + cat := NewCatalog(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(catalogURL.String(), http.MethodGet, + "", "error retrieving catalog: %s", nil, cat.Catalog) + + // The request was successful + return *cat, err + } + } + return Catalog{}, nil +} + +// GetCatalogByHref finds a Catalog by HREF +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetCatalogByHref(catalogHref string) (*Catalog, error) { + catalogURL := adminOrg.client.VCDHREF + catalogURL.Path += "/catalog/" + strings.Split(catalogHref, "/api/admin/catalog/")[1] //gets id + + cat := NewCatalog(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(catalogURL.String(), http.MethodGet, + "", "error retrieving catalog: %s", nil, cat.Catalog) + + if err != nil { + return nil, err + } + // The request was successful + return cat, nil +} + +// GetCatalogByName finds a Catalog by Name +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetCatalogByName(catalogName string, refresh bool) (*Catalog, error) { + + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + if catalog.Name == catalogName { + return adminOrg.GetCatalogByHref(catalog.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// Extracts an UUID from a string, regardless of surrounding text +// Returns an empty string if no UUID was found +func extractUuid(input string) string { + reGetID := regexp.MustCompile(`([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})`) + matchListId := reGetID.FindAllStringSubmatch(input, -1) + if len(matchListId) > 0 && len(matchListId[0]) > 0 { + return matchListId[0][1] + } + return "" +} + +// equalIds compares two IDs and return true if they are the same +// The comparison happens by extracting the bare UUID from both the +// wanted ID and the found one. +// When the found ID is empty, it used the HREF for such comparison, +// This function is useful when the reference structure in the parent lookup list +// may lack the ID (such as in Org.Links, AdminOrg.Catalogs) or has an ID +// that is only a UUID without prefixes (such as in CatalogItem list) +// +// wantedId is the input string to compare +// foundId is the ID field in the reference record (can be empty) +// foundHref is the HREF field in the reference record (should never be empty) +func equalIds(wantedId, foundId, foundHref string) bool { + + wantedUuid := extractUuid(wantedId) + foundUuid := "" + + if wantedUuid == "" { + return false + } + if foundId != "" { + // In some entities, the ID is a simple UUID without prefix + foundUuid = extractUuid(foundId) + } else { + foundUuid = extractUuid(foundHref) + } + return foundUuid == wantedUuid +} + +// GetCatalogById finds a Catalog by ID +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetCatalogById(catalogId string, refresh bool) (*Catalog, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + if equalIds(catalogId, catalog.ID, catalog.HREF) { + return adminOrg.GetCatalogByHref(catalog.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetCatalogByNameOrId finds a Catalog by name or ID +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return adminOrg.GetCatalogByName(name, refresh) } + getById := func(id string, refresh bool) (interface{}, error) { return adminOrg.GetCatalogById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*Catalog), err +} + +// GetAdminCatalogByHref finds an AdminCatalog by HREF +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminCatalogByHref(catalogHref string) (*AdminCatalog, error) { + adminCatalog := NewAdminCatalog(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(catalogHref, http.MethodGet, + "", "error retrieving catalog: %s", nil, adminCatalog.AdminCatalog) + + if err != nil { + return nil, err + } + + // The request was successful + return adminCatalog, nil +} + +// GetCatalogByName finds an AdminCatalog by Name +// On success, returns a pointer to the AdminCatalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminCatalogByName(catalogName string, refresh bool) (*AdminCatalog, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + // Get Catalog HREF + if catalog.Name == catalogName { + return adminOrg.GetAdminCatalogByHref(catalog.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetCatalogById finds an AdminCatalog by ID +// On success, returns a pointer to the AdminCatalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminCatalogById(catalogId string, refresh bool) (*AdminCatalog, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { + // Get Catalog HREF + if equalIds(catalogId, catalog.ID, catalog.HREF) { + return adminOrg.GetAdminCatalogByHref(catalog.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetAdminCatalogByNameOrId finds an AdminCatalog by name or ID +// On success, returns a pointer to the AdminCatalog structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminCatalogByNameOrId(identifier string, refresh bool) (*AdminCatalog, error) { + getByName := func(name string, refresh bool) (interface{}, error) { + return adminOrg.GetAdminCatalogByName(name, refresh) + } + getById := func(id string, refresh bool) (interface{}, error) { + return adminOrg.GetAdminCatalogById(id, refresh) + } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*AdminCatalog), err +} + +// GetVDCByHref retrieves a VDC using a direct call with the HREF +func (adminOrg *AdminOrg) GetVDCByHref(vdcHref string) (*Vdc, error) { + splitByAdminHREF := strings.Split(vdcHref, "/api/admin") + + // admin user and normal user will have different urls + var vdcHREF string + if len(splitByAdminHREF) == 1 { + vdcHREF = vdcHref + } else { + vdcHREF = splitByAdminHREF[0] + "/api" + splitByAdminHREF[1] + } + + vdc := NewVdc(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(vdcHREF, http.MethodGet, + "", "error getting vdc: %s", nil, vdc.Vdc) + + if err != nil { + return nil, err + } + + return vdc, nil +} + +// GetVDCByName finds a VDC by Name +// On success, returns a pointer to the Vdc structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetVDCByName(vdcName string, refresh bool) (*Vdc, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, vdc := range adminOrg.AdminOrg.Vdcs.Vdcs { + if vdc.Name == vdcName { + return adminOrg.GetVDCByHref(vdc.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetVDCById finds a VDC by ID +// On success, returns a pointer to the Vdc structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetVDCById(vdcId string, refresh bool) (*Vdc, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, vdc := range adminOrg.AdminOrg.Vdcs.Vdcs { + if equalIds(vdcId, vdc.ID, vdc.HREF) { + return adminOrg.GetVDCByHref(vdc.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetVDCByNameOrId finds a VDC by name or ID +// On success, returns a pointer to the VDC structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetVDCByNameOrId(identifier string, refresh bool) (*Vdc, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return adminOrg.GetVDCByName(name, refresh) } + getById := func(id string, refresh bool) (interface{}, error) { return adminOrg.GetVDCById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*Vdc), err +} + +// GetVDCByHref retrieves a VDC using a direct call with the HREF +func (adminOrg *AdminOrg) GetAdminVDCByHref(vdcHref string) (*AdminVdc, error) { + + adminVdc := NewAdminVdc(adminOrg.client) + + _, err := adminOrg.client.ExecuteRequest(vdcHref, http.MethodGet, + "", "error getting vdc: %s", nil, adminVdc.AdminVdc) + + if err != nil { + return nil, err + } + return adminVdc, nil +} + +// GetAdminVDCByName finds an Admin VDC by Name +// On success, returns a pointer to the AdminVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminVDCByName(vdcName string, refresh bool) (*AdminVdc, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, vdc := range adminOrg.AdminOrg.Vdcs.Vdcs { + if vdc.Name == vdcName { + return adminOrg.GetAdminVDCByHref(vdc.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetAdminVDCById finds an Admin VDC by ID +// On success, returns a pointer to the AdminVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminVDCById(vdcId string, refresh bool) (*AdminVdc, error) { + if refresh { + err := adminOrg.Refresh() + if err != nil { + return nil, err + } + } + for _, vdc := range adminOrg.AdminOrg.Vdcs.Vdcs { + if equalIds(vdcId, vdc.ID, vdc.HREF) { + return adminOrg.GetAdminVDCByHref(vdc.HREF) + } + } + return nil, ErrorEntityNotFound +} + +// GetAdminVDCByNameOrId finds an Admin VDC by Name Or ID +// On success, returns a pointer to the AdminVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (adminOrg *AdminOrg) GetAdminVDCByNameOrId(identifier string, refresh bool) (*AdminVdc, error) { + getByName := func(name string, refresh bool) (interface{}, error) { + return adminOrg.GetAdminVDCByName(name, refresh) + } + getById := func(id string, refresh bool) (interface{}, error) { return adminOrg.GetAdminVDCById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*AdminVdc), err +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go new file mode 100644 index 000000000..0c0bc4d24 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go @@ -0,0 +1,84 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +type AdminVdc struct { + AdminVdc *types.AdminVdc + client *Client +} + +func NewAdminVdc(cli *Client) *AdminVdc { + return &AdminVdc{ + AdminVdc: new(types.AdminVdc), + client: cli, + } +} + +// Given an adminVdc with a valid HREF, the function refresh the adminVdc +// and updates the adminVdc data. Returns an error on failure +// Users should use refresh whenever they suspect +// a stale VDC due to the creation/update/deletion of a resource +// within the the VDC itself. +func (adminVdc *AdminVdc) Refresh() error { + if *adminVdc == (AdminVdc{}) || adminVdc.AdminVdc.HREF == "" { + return fmt.Errorf("cannot refresh, Object is empty or HREF is empty") + } + + // Empty struct before a new unmarshal, otherwise we end up with duplicate + // elements in slices. + unmarshalledAdminVdc := &types.AdminVdc{} + + _, err := adminVdc.client.ExecuteRequest(adminVdc.AdminVdc.HREF, http.MethodGet, + "", "error refreshing VDC: %s", nil, unmarshalledAdminVdc) + if err != nil { + return err + } + adminVdc.AdminVdc = unmarshalledAdminVdc + + return nil +} + +// UpdateAsync updates VDC from current VDC struct contents. +// Any differences that may be legally applied will be updated. +// Returns an error if the call to vCD fails. +// API Documentation: https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/operations/PUT-Vdc.html +func (adminVdc *AdminVdc) UpdateAsync() (Task, error) { + + adminVdc.AdminVdc.Xmlns = types.XMLNamespaceVCloud + + // Return the task + return adminVdc.client.ExecuteTaskRequest(adminVdc.AdminVdc.HREF, http.MethodPut, + types.MimeAdminVDC, "error updating VDC: %s", adminVdc.AdminVdc) +} + +// Update function updates an Admin VDC from current VDC struct contents. +// Any differences that may be legally applied will be updated. +// Returns an empty AdminVdc struct and error if the call to vCD fails. +// API Documentation: https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/operations/PUT-Vdc.html +func (adminVdc *AdminVdc) Update() (AdminVdc, error) { + task, err := adminVdc.UpdateAsync() + if err != nil { + return AdminVdc{}, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return AdminVdc{}, err + } + + err = adminVdc.Refresh() + if err != nil { + return AdminVdc{}, err + } + + return *adminVdc, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go index d9fe804c2..41ccd8ef6 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go @@ -28,20 +28,6 @@ const ( defaultPieceSize int64 = 1024 * 1024 ) -type CatalogOperations interface { - FindCatalogItem(catalogItem string) (CatalogItem, error) -} - -// AdminCatalog is a admin view of a vCloud Director Catalog -// To be able to get an AdminCatalog representation, users must have -// admin credentials to the System org. AdminCatalog is used -// for creating, updating, and deleting a Catalog. -// Definition: https://code.vmware.com/apis/220/vcloud#/doc/doc/types/AdminCatalogType.html -type AdminCatalog struct { - AdminCatalog *types.AdminCatalog - client *Client -} - type Catalog struct { Catalog *types.Catalog client *Client @@ -54,13 +40,6 @@ func NewCatalog(client *Client) *Catalog { } } -func NewAdminCatalog(client *Client) *AdminCatalog { - return &AdminCatalog{ - AdminCatalog: new(types.AdminCatalog), - client: client, - } -} - // Deletes the Catalog, returning an error if the vCD call fails. // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html func (catalog *Catalog) Delete(force, recursive bool) error { @@ -89,36 +68,6 @@ func (catalog *Catalog) Delete(force, recursive bool) error { return nil } -// Deletes the Catalog, returning an error if the vCD call fails. -// Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html -func (adminCatalog *AdminCatalog) Delete(force, recursive bool) error { - catalog := NewCatalog(adminCatalog.client) - catalog.Catalog = &adminCatalog.AdminCatalog.Catalog - return catalog.Delete(force, recursive) -} - -// Updates the Catalog definition from current Catalog struct contents. -// Any differences that may be legally applied will be updated. -// Returns an error if the call to vCD fails. Update automatically performs -// a refresh with the admin catalog it gets back from the rest api -// Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Catalog.html -func (adminCatalog *AdminCatalog) Update() error { - reqCatalog := &types.Catalog{ - Name: adminCatalog.AdminCatalog.Catalog.Name, - Description: adminCatalog.AdminCatalog.Description, - } - vcomp := &types.AdminCatalog{ - Xmlns: types.XMLNamespaceVCloud, - Catalog: *reqCatalog, - IsPublished: adminCatalog.AdminCatalog.IsPublished, - } - catalog := &types.AdminCatalog{} - _, err := adminCatalog.client.ExecuteRequest(adminCatalog.AdminCatalog.HREF, http.MethodPut, - "application/vnd.vmware.admin.catalog+xml", "error updating catalog: %s", vcomp, catalog) - adminCatalog.AdminCatalog = catalog - return err -} - // Envelope is a ovf description root element. File contains information for vmdk files. // Namespace: http://schemas.dmtf.org/ovf/envelope/1 // Description: Envelope is a ovf description root element. File contains information for vmdk files.. @@ -135,6 +84,7 @@ type Envelope struct { // then the function returns a CatalogItem. If the item does not // exist, then it returns an empty CatalogItem. If the call fails // at any point, it returns an error. +// Deprecated: use GetCatalogItemByName instead func (cat *Catalog) FindCatalogItem(catalogItemName string) (CatalogItem, error) { for _, catalogItems := range cat.Catalog.CatalogItems { for _, catalogItem := range catalogItems.CatalogItem { @@ -152,16 +102,6 @@ func (cat *Catalog) FindCatalogItem(catalogItemName string) (CatalogItem, error) return CatalogItem{}, nil } -// Uploads an ova file to a catalog. This method only uploads bits to vCD spool area. -// Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to -// remove vCD catalog item which waits for files to be uploaded. Files from ova are extracted to system -// temp folder "govcd+random number" and left for inspection on error. -func (adminCatalog *AdminCatalog) UploadOvf(ovaFileName, itemName, description string, uploadPieceSize int64) (UploadTask, error) { - catalog := NewCatalog(adminCatalog.client) - catalog.Catalog = &adminCatalog.AdminCatalog.Catalog - return catalog.UploadOvf(ovaFileName, itemName, description, uploadPieceSize) -} - // Uploads an ova file to a catalog. This method only uploads bits to vCD spool area. // Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to // remove vCD catalog item which waits for files to be uploaded. Files from ova are extracted to system @@ -415,7 +355,7 @@ func queryVappTemplate(client *Client, vappTemplateUrl *url.URL, newItemName str } for _, task := range vappTemplateParsed.Tasks.Task { - if "error" == task.Status && newItemName == task.Owner.Name { + if task.Status == "error" && newItemName == task.Owner.Name { util.Logger.Printf("[Error] %#v", task.Error) return vappTemplateParsed, fmt.Errorf("error in vcd returned error code: %d, error: %s and message: %s ", task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message) } @@ -718,3 +658,89 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin return executeUpload(cat.client, createdMedia, mediaFilePath, mediaName, fileSize, uploadPieceSize) } + +// Refresh gets a fresh copy of the catalog from vCD +func (cat *Catalog) Refresh() error { + if cat == nil || *cat == (Catalog{}) || cat.Catalog.HREF == "" { + return fmt.Errorf("cannot refresh, Object is empty or HREF is empty") + } + + refreshedCatalog := &types.Catalog{} + + _, err := cat.client.ExecuteRequest(cat.Catalog.HREF, http.MethodGet, + "", "error refreshing VDC: %s", nil, refreshedCatalog) + if err != nil { + return err + } + cat.Catalog = refreshedCatalog + + return nil +} + +// GetCatalogItemByHref finds a CatalogItem by HREF +// On success, returns a pointer to the CatalogItem structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetCatalogItemByHref(catalogItemHref string) (*CatalogItem, error) { + + catItem := NewCatalogItem(cat.client) + + _, err := cat.client.ExecuteRequest(catalogItemHref, http.MethodGet, + "", "error retrieving catalog: %s", nil, catItem.CatalogItem) + if err != nil { + return nil, err + } + return catItem, nil +} + +// GetCatalogItemByName finds a CatalogItem by Name +// On success, returns a pointer to the CatalogItem structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetCatalogItemByName(catalogItemName string, refresh bool) (*CatalogItem, error) { + if refresh { + err := cat.Refresh() + if err != nil { + return nil, err + } + } + for _, catalogItems := range cat.Catalog.CatalogItems { + for _, catalogItem := range catalogItems.CatalogItem { + if catalogItem.Name == catalogItemName && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" { + return cat.GetCatalogItemByHref(catalogItem.HREF) + } + } + } + return nil, ErrorEntityNotFound +} + +// GetCatalogItemById finds a Catalog Item by ID +// On success, returns a pointer to the CatalogItem structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetCatalogItemById(catalogItemId string, refresh bool) (*CatalogItem, error) { + if refresh { + err := cat.Refresh() + if err != nil { + return nil, err + } + } + for _, catalogItems := range cat.Catalog.CatalogItems { + for _, catalogItem := range catalogItems.CatalogItem { + if equalIds(catalogItemId, catalogItem.ID, catalogItem.HREF) && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" { + return cat.GetCatalogItemByHref(catalogItem.HREF) + } + } + } + return nil, ErrorEntityNotFound +} + +// GetCatalogItemByNameOrId finds a Catalog Item by Name or ID +// On success, returns a pointer to the CatalogItem structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetCatalogItemByNameOrId(identifier string, refresh bool) (*CatalogItem, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return cat.GetCatalogItemByName(name, refresh) } + getById := func(id string, refresh bool) (interface{}, error) { return cat.GetCatalogItemById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*CatalogItem), err +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/entity.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/entity.go new file mode 100644 index 000000000..e05f70974 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/entity.go @@ -0,0 +1,45 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +type genericGetter func(string, bool) (interface{}, error) + +// getEntityByNameOrId finds a generic entity by Name Or ID +// On success, returns an empty interface representing a pointer to the structure and a nil error +// On failure, returns a nil pointer and an error +// Example usage: +// +// func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { +// getByName := func(name string, refresh bool) (interface{}, error) { +// return org.GetCatalogByName(name, refresh) +// } +// getById := func(id string, refresh bool) (interface{}, error) { +// return org.GetCatalogById(id, refresh) +// } +// entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) +// if entity != nil { +// return nil, err +// } +// return entity.(*Catalog), err +// } +func getEntityByNameOrId(getByName, getById genericGetter, identifier string, refresh bool) (interface{}, error) { + + var byNameErr, byIdErr error + var entity interface{} + + entity, byIdErr = getById(identifier, refresh) + if byIdErr == nil { + // Found by ID + return entity, nil + } + if IsNotFound(byIdErr) { + // Not found by ID, try by name + entity, byNameErr = getByName(identifier, false) + return entity, byNameErr + } else { + // On any other error, we return it + return nil, byIdErr + } +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go index a65f1ec63..b95e65870 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go @@ -8,21 +8,11 @@ import ( "errors" "fmt" "net/http" - "net/url" - "strconv" - "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" ) -// Interface for methods in common for Org and AdminOrg -type OrgOperations interface { - FindCatalog(catalog string) (Catalog, error) - GetVdcByName(vdcname string) (Vdc, error) - Refresh() error -} - type Org struct { Org *types.Org client *Client @@ -63,6 +53,7 @@ func (org *Org) Refresh() error { // Given a valid catalog name, FindCatalog returns a Catalog object. // If no catalog is found, then returns an empty catalog and no error. // Otherwise it returns an error. +// Deprecated: use org.GetCatalogByName instead func (org *Org) FindCatalog(catalogName string) (Catalog, error) { for _, link := range org.Org.Link { @@ -83,6 +74,7 @@ func (org *Org) FindCatalog(catalogName string) (Catalog, error) { // If user specifies valid vdc name then this returns a vdc object. // If no vdc is found, then it returns an empty vdc and no error. // Otherwise it returns an empty vdc and an error. +// Deprecated: use org.GetVDCByName instead func (org *Org) GetVdcByName(vdcname string) (Vdc, error) { for _, link := range org.Org.Link { if link.Name == vdcname { @@ -97,147 +89,6 @@ func (org *Org) GetVdcByName(vdcname string) (Vdc, error) { return Vdc{}, nil } -// Given an adminVdc with a valid HREF, the function refresh the adminVdc -// and updates the adminVdc data. Returns an error on failure -// Users should use refresh whenever they suspect -// a stale VDC due to the creation/update/deletion of a resource -// within the the VDC itself. -func (adminVdc *AdminVdc) Refresh() error { - if *adminVdc == (AdminVdc{}) || adminVdc.AdminVdc.HREF == "" { - return fmt.Errorf("cannot refresh, Object is empty or HREF is empty") - } - - // Empty struct before a new unmarshal, otherwise we end up with duplicate - // elements in slices. - unmarshalledAdminVdc := &types.AdminVdc{} - - _, err := adminVdc.client.ExecuteRequest(adminVdc.AdminVdc.HREF, http.MethodGet, - "", "error refreshing VDC: %s", nil, unmarshalledAdminVdc) - if err != nil { - return err - } - adminVdc.AdminVdc = unmarshalledAdminVdc - - return nil -} - -// GetAdminVdcByName function uses a valid VDC name and returns a admin VDC object. -// If no VDC is found, then it returns an empty VDC and no error. -// Otherwise it returns an empty VDC and an error. -func (adminOrg *AdminOrg) GetAdminVdcByName(vdcname string) (AdminVdc, error) { - for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { - if vdcs.Name == vdcname { - - adminVdc := NewAdminVdc(adminOrg.client) - - _, err := adminOrg.client.ExecuteRequest(vdcs.HREF, http.MethodGet, - "", "error getting vdc: %s", nil, adminVdc.AdminVdc) - - return *adminVdc, err - } - } - return AdminVdc{}, nil -} - -// UpdateAsync updates VDC from current VDC struct contents. -// Any differences that may be legally applied will be updated. -// Returns an error if the call to vCD fails. -// API Documentation: https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/operations/PUT-Vdc.html -func (adminVdc *AdminVdc) UpdateAsync() (Task, error) { - - adminVdc.AdminVdc.Xmlns = types.XMLNamespaceVCloud - - // Return the task - return adminVdc.client.ExecuteTaskRequest(adminVdc.AdminVdc.HREF, http.MethodPut, - types.MimeAdminVDC, "error updating VDC: %s", adminVdc.AdminVdc) -} - -// Update function updates an Admin VDC from current VDC struct contents. -// Any differences that may be legally applied will be updated. -// Returns an empty AdminVdc struct and error if the call to vCD fails. -// API Documentation: https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/operations/PUT-Vdc.html -func (adminVdc *AdminVdc) Update() (AdminVdc, error) { - task, err := adminVdc.UpdateAsync() - if err != nil { - return AdminVdc{}, err - } - - err = task.WaitTaskCompletion() - if err != nil { - return AdminVdc{}, err - } - - err = adminVdc.Refresh() - if err != nil { - return AdminVdc{}, err - } - - return *adminVdc, nil -} - -// AdminOrg gives an admin representation of an org. -// Administrators can delete and update orgs with an admin org object. -// AdminOrg includes all members of the Org element, and adds several -// elements that can be viewed and modified only by system administrators. -// Definition: https://code.vmware.com/apis/220/vcloud#/doc/doc/types/AdminOrgType.html -type AdminOrg struct { - AdminOrg *types.AdminOrg - client *Client -} - -func NewAdminOrg(cli *Client) *AdminOrg { - return &AdminOrg{ - AdminOrg: new(types.AdminOrg), - client: cli, - } -} - -// Given an adminorg with a valid HREF, the function refetches the adminorg -// and updates the user's adminorg data. Otherwise if the function fails, -// it returns an error. Users should use refresh whenever they have -// a stale org due to the creation/update/deletion of a resource -// within the org or the org itself. -func (adminOrg *AdminOrg) Refresh() error { - if *adminOrg == (AdminOrg{}) { - return fmt.Errorf("cannot refresh, Object is empty") - } - - // Empty struct before a new unmarshal, otherwise we end up with duplicate - // elements in slices. - unmarshalledAdminOrg := &types.AdminOrg{} - - _, err := adminOrg.client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, - "", "error refreshing organization: %s", nil, unmarshalledAdminOrg) - if err != nil { - return err - } - adminOrg.AdminOrg = unmarshalledAdminOrg - - return nil -} - -// CreateCatalog creates a catalog with given name and description under the -// the given organization. Returns an AdminCatalog that contains a creation -// task. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html -func (adminOrg *AdminOrg) CreateCatalog(name, description string) (AdminCatalog, error) { - return CreateCatalog(adminOrg.client, adminOrg.AdminOrg.Link, name, description) -} - -// CreateCatalog creates a catalog with given name and description under the -// the given organization. Returns an Catalog that contains a creation -// task. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html -func (org *Org) CreateCatalog(name, description string) (Catalog, error) { - catalog := NewCatalog(org.client) - adminCatalog, err := CreateCatalog(org.client, org.Org.Link, name, description) - if err != nil { - return Catalog{}, err - } - catalog.Catalog = &adminCatalog.AdminCatalog.Catalog - return *catalog, nil -} - func CreateCatalog(client *Client, links types.LinkList, Name, Description string) (AdminCatalog, error) { reqCatalog := &types.Catalog{ Name: Name, @@ -268,32 +119,18 @@ func CreateCatalog(client *Client, links types.LinkList, Name, Description strin return *catalog, err } -// If user specifies valid vdc name then this returns a vdc object. -// If no vdc is found, then it returns an empty vdc and no error. -// Otherwise it returns an empty vdc and an error. This function -// allows users to use an AdminOrg to fetch a vdc as well. -func (adminOrg *AdminOrg) GetVdcByName(vdcname string) (Vdc, error) { - for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { - if vdcs.Name == vdcname { - splitByAdminHREF := strings.Split(vdcs.HREF, "/api/admin") - - // admin user and normal user will have different urls - var vdcHREF string - if len(splitByAdminHREF) == 1 { - vdcHREF = vdcs.HREF - } else { - vdcHREF = splitByAdminHREF[0] + "/api" + splitByAdminHREF[1] - } - - vdc := NewVdc(adminOrg.client) - - _, err := adminOrg.client.ExecuteRequest(vdcHREF, http.MethodGet, - "", "error getting vdc: %s", nil, vdc.Vdc) - - return *vdc, err - } +// CreateCatalog creates a catalog with given name and description under +// the given organization. Returns an Catalog that contains a creation +// task. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html +func (org *Org) CreateCatalog(name, description string) (Catalog, error) { + catalog := NewCatalog(org.client) + adminCatalog, err := CreateCatalog(org.client, org.Org.Link, name, description) + if err != nil { + return Catalog{}, err } - return Vdc{}, nil + catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + return *catalog, nil } func validateVdcConfiguration(vdcDefinition *types.VdcConfiguration) error { @@ -342,321 +179,131 @@ func validateVdcConfiguration(vdcDefinition *types.VdcConfiguration) error { return nil } -// CreateVdc creates a VDC with the given params under the given organization. -// Returns an AdminVdc. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-VdcConfiguration.html -func (org *AdminOrg) CreateVdc(vdcConfiguration *types.VdcConfiguration) (Task, error) { - err := validateVdcConfiguration(vdcConfiguration) - if err != nil { - return Task{}, err - } - - vdcCreateHREF, err := url.ParseRequestURI(org.AdminOrg.HREF) - if err != nil { - return Task{}, fmt.Errorf("error parsing admin org url: %s", err) - } - vdcCreateHREF.Path += "/vdcsparams" - - adminVdc := NewAdminVdc(org.client) - - _, err = org.client.ExecuteRequest(vdcCreateHREF.String(), http.MethodPost, - "application/vnd.vmware.admin.createVdcParams+xml", "error retrieving vdc: %s", vdcConfiguration, adminVdc.AdminVdc) - if err != nil { - return Task{}, err - } - - // Return the task - task := NewTask(org.client) - task.Task = adminVdc.AdminVdc.Tasks.Task[0] - return *task, nil -} +// GetCatalogByHref finds a Catalog by HREF +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetCatalogByHref(catalogHref string) (*Catalog, error) { + cat := NewCatalog(org.client) -// Creates the vdc and waits for the asynchronous task to complete. -func (org *AdminOrg) CreateVdcWait(vdcDefinition *types.VdcConfiguration) error { - task, err := org.CreateVdc(vdcDefinition) + _, err := org.client.ExecuteRequest(catalogHref, http.MethodGet, + "", "error retrieving catalog: %s", nil, cat.Catalog) if err != nil { - return err + return nil, err } - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf("couldn't finish creating vdc %#v", err) - } - return nil + // The request was successful + return cat, nil } -// Deletes the org, returning an error if the vCD call fails. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Organization.html -func (adminOrg *AdminOrg) Delete(force bool, recursive bool) error { - if force && recursive { - //undeploys vapps - err := adminOrg.undeployAllVApps() - if err != nil { - return fmt.Errorf("error could not undeploy: %#v", err) - } - //removes vapps - err = adminOrg.removeAllVApps() +// GetCatalogByName finds a Catalog by Name +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetCatalogByName(catalogName string, refresh bool) (*Catalog, error) { + if refresh { + err := org.Refresh() if err != nil { - return fmt.Errorf("error could not remove vapp: %#v", err) + return nil, err } - //removes catalogs - err = adminOrg.removeCatalogs() - if err != nil { - return fmt.Errorf("error could not remove all catalogs: %#v", err) - } - //removes networks - err = adminOrg.removeAllOrgNetworks() - if err != nil { - return fmt.Errorf("error could not remove all networks: %#v", err) - } - //removes org vdcs - err = adminOrg.removeAllOrgVDCs() - if err != nil { - return fmt.Errorf("error could not remove all vdcs: %#v", err) - } - } - // Disable org - err := adminOrg.Disable() - if err != nil { - return fmt.Errorf("error disabling Org %s: %s", adminOrg.AdminOrg.Name, err) - } - // Get admin HREF - orgHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) - if err != nil { - return fmt.Errorf("error getting AdminOrg HREF %s : %v", adminOrg.AdminOrg.HREF, err) - } - req := adminOrg.client.NewRequest(map[string]string{ - "force": strconv.FormatBool(force), - "recursive": strconv.FormatBool(recursive), - }, http.MethodDelete, *orgHREF, nil) - resp, err := checkResp(adminOrg.client.Http.Do(req)) - if err != nil { - return fmt.Errorf("error deleting Org %s: %s", adminOrg.AdminOrg.ID, err) } - - task := NewTask(adminOrg.client) - if err = decodeBody(resp, task.Task); err != nil { - return fmt.Errorf("error decoding task response: %s", err) - } - return task.WaitTaskCompletion() -} - -// Disables the org. Returns an error if the call to vCD fails. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-DisableOrg.html -func (adminOrg *AdminOrg) Disable() error { - orgHREF, err := url.ParseRequestURI(adminOrg.AdminOrg.HREF) - if err != nil { - return fmt.Errorf("error getting AdminOrg HREF %s : %v", adminOrg.AdminOrg.HREF, err) - } - orgHREF.Path += "/action/disable" - - return adminOrg.client.ExecuteRequestWithoutResponse(orgHREF.String(), http.MethodPost, "", "error disabling organization: %s", nil) -} - -// Updates the Org definition from current org struct contents. -// Any differences that may be legally applied will be updated. -// Returns an error if the call to vCD fails. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Organization.html -func (adminOrg *AdminOrg) Update() (Task, error) { - vcomp := &types.AdminOrg{ - Xmlns: types.XMLNamespaceVCloud, - Name: adminOrg.AdminOrg.Name, - IsEnabled: adminOrg.AdminOrg.IsEnabled, - FullName: adminOrg.AdminOrg.FullName, - Description: adminOrg.AdminOrg.Description, - OrgSettings: adminOrg.AdminOrg.OrgSettings, - } - - /**/ - // Same workaround used in Org creation, where OrgGeneralSettings properties - // are not set unless UseServerBootSequence is also set - if vcomp.OrgSettings.OrgGeneralSettings != nil { - vcomp.OrgSettings.OrgGeneralSettings.UseServerBootSequence = true - } - - /**/ - - // Return the task - return adminOrg.client.ExecuteTaskRequest(adminOrg.AdminOrg.HREF, http.MethodPut, - "application/vnd.vmware.admin.organization+xml", "error updating Org: %s", vcomp) -} - -// Undeploys every vapp within an organization -func (adminOrg *AdminOrg) undeployAllVApps() error { - for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { - adminVdcHREF, err := url.Parse(vdcs.HREF) - if err != nil { - return err - } - vdc, err := adminOrg.getVdcByAdminHREF(adminVdcHREF) - if err != nil { - return fmt.Errorf("error retrieving vapp with url: %s and with error %s", adminVdcHREF.Path, err) - } - err = vdc.undeployAllVdcVApps() - if err != nil { - return fmt.Errorf("error deleting vapp: %s", err) + for _, catalog := range org.Org.Link { + // Get Catalog HREF + if catalog.Name == catalogName && catalog.Type == types.MimeCatalog { + return org.GetCatalogByHref(catalog.HREF) } } - return nil + return nil, ErrorEntityNotFound } -// Deletes every vapp within an organization -func (adminOrg *AdminOrg) removeAllVApps() error { - for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { - adminVdcHREF, err := url.Parse(vdcs.HREF) - if err != nil { - return err - } - vdc, err := adminOrg.getVdcByAdminHREF(adminVdcHREF) +// GetCatalogById finds a Catalog by ID +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetCatalogById(catalogId string, refresh bool) (*Catalog, error) { + if refresh { + err := org.Refresh() if err != nil { - return fmt.Errorf("error retrieving vapp with url: %s and with error %s", adminVdcHREF.Path, err) + return nil, err } - err = vdc.removeAllVdcVApps() - if err != nil { - return fmt.Errorf("error deleting vapp: %s", err) + } + for _, catalog := range org.Org.Link { + // Get Catalog HREF + if equalIds(catalogId, catalog.ID, catalog.HREF) { + return org.GetCatalogByHref(catalog.HREF) } } - return nil + return nil, ErrorEntityNotFound } -// Gets a vdc within org associated with an admin vdc url -func (adminOrg *AdminOrg) getVdcByAdminHREF(adminVdcUrl *url.URL) (*Vdc, error) { - // get non admin vdc path - vdcURL := adminOrg.client.VCDHREF - vdcURL.Path += strings.Split(adminVdcUrl.Path, "/api/admin")[1] //gets id - - vdc := NewVdc(adminOrg.client) - - _, err := adminOrg.client.ExecuteRequest(vdcURL.String(), http.MethodGet, - "", "error retrieving vdc: %s", nil, vdc.Vdc) - - return vdc, err +// GetCatalogByNameOrId finds a Catalog by name or ID +// On success, returns a pointer to the Catalog structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return org.GetCatalogByName(name, refresh) } + getById := func(id string, refresh bool) (interface{}, error) { return org.GetCatalogById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*Catalog), err } -// Removes all vdcs in a org -func (adminOrg *AdminOrg) removeAllOrgVDCs() error { - for _, vdcs := range adminOrg.AdminOrg.Vdcs.Vdcs { - - // Get admin Vdc HREF - adminVdcUrl := adminOrg.client.VCDHREF - adminVdcUrl.Path += "/admin/vdc/" + strings.Split(vdcs.HREF, "/api/vdc/")[1] + "/action/disable" - - req := adminOrg.client.NewRequest(map[string]string{}, http.MethodPost, adminVdcUrl, nil) - _, err := checkResp(adminOrg.client.Http.Do(req)) - if err != nil { - return fmt.Errorf("error disabling vdc: %s", err) - } - // Get admin vdc HREF for normal deletion - adminVdcUrl.Path = strings.Split(adminVdcUrl.Path, "/action/disable")[0] - req = adminOrg.client.NewRequest(map[string]string{ - "recursive": "true", - "force": "true", - }, http.MethodDelete, adminVdcUrl, nil) - resp, err := checkResp(adminOrg.client.Http.Do(req)) - if err != nil { - return fmt.Errorf("error deleting vdc: %s", err) - } - task := NewTask(adminOrg.client) - if err = decodeBody(resp, task.Task); err != nil { - return fmt.Errorf("error decoding task response: %s", err) - } - if task.Task.Status == "error" { - return fmt.Errorf("vdc not properly destroyed") - } - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf("couldn't finish removing vdc %#v", err) - } - +// GetVDCByHref finds a VDC by HREF +// On success, returns a pointer to the VDC structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetVDCByHref(vdcHref string) (*Vdc, error) { + vdc := NewVdc(org.client) + _, err := org.client.ExecuteRequest(vdcHref, http.MethodGet, + "", "error retrieving VDC: %s", nil, vdc.Vdc) + if err != nil { + return nil, err } - - return nil + // The request was successful + return vdc, nil } -// Removes All networks in the org -func (adminOrg *AdminOrg) removeAllOrgNetworks() error { - for _, networks := range adminOrg.AdminOrg.Networks.Networks { - // Get Network HREF - networkHREF := adminOrg.client.VCDHREF - networkHREF.Path += "/admin/network/" + strings.Split(networks.HREF, "/api/admin/network/")[1] //gets id - - task, err := adminOrg.client.ExecuteTaskRequest(networkHREF.String(), http.MethodDelete, - "", "error deleting network: %s", nil) +// GetVDCByName finds a VDC by Name +// On success, returns a pointer to the VDC structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetVDCByName(vdcName string, refresh bool) (*Vdc, error) { + if refresh { + err := org.Refresh() if err != nil { - return err + return nil, err } - - if task.Task.Status == "error" { - return fmt.Errorf("network not properly destroyed") - } - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf("couldn't finish removing network %#v", err) + } + for _, link := range org.Org.Link { + if link.Name == vdcName && link.Type == types.MimeVDC { + return org.GetVDCByHref(link.HREF) } } - return nil + return nil, ErrorEntityNotFound } -// Forced removal of all organization catalogs -func (adminOrg *AdminOrg) removeCatalogs() error { - for _, catalogs := range adminOrg.AdminOrg.Catalogs.Catalog { - // Get Catalog HREF - catalogHREF := adminOrg.client.VCDHREF - catalogHREF.Path += "/admin/catalog/" + strings.Split(catalogs.HREF, "/api/admin/catalog/")[1] //gets id - req := adminOrg.client.NewRequest(map[string]string{ - "force": "true", - "recursive": "true", - }, http.MethodDelete, catalogHREF, nil) - _, err := checkResp(adminOrg.client.Http.Do(req)) +// GetVDCById finds a VDC by ID +// On success, returns a pointer to the VDC structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetVDCById(vdcId string, refresh bool) (*Vdc, error) { + if refresh { + err := org.Refresh() if err != nil { - return fmt.Errorf("error deleting catalog: %s, %s", err, catalogHREF.Path) + return nil, err } } - return nil - -} - -// Given a valid catalog name, FindCatalog returns an AdminCatalog object. -// If no catalog is found, then returns an empty AdminCatalog and no error. -// Otherwise it returns an error. Function allows user to use an AdminOrg -// to also fetch a Catalog. If user does not have proper credentials to -// perform administrator tasks then function returns an error. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/GET-Catalog-AdminView.html -func (adminOrg *AdminOrg) FindAdminCatalog(catalogName string) (AdminCatalog, error) { - for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { - // Get Catalog HREF - if catalog.Name == catalogName { - - adminCatalog := NewAdminCatalog(adminOrg.client) - - _, err := adminOrg.client.ExecuteRequest(catalog.HREF, http.MethodGet, - "", "error retrieving catalog: %s", nil, adminCatalog.AdminCatalog) - - // The request was successful - return *adminCatalog, err + for _, link := range org.Org.Link { + if equalIds(vdcId, link.ID, link.HREF) { + return org.GetVDCByHref(link.HREF) } } - return AdminCatalog{}, nil + return nil, ErrorEntityNotFound } -// Given a valid catalog name, FindCatalog returns a Catalog object. -// If no catalog is found, then returns an empty catalog and no error. -// Otherwise it returns an error. Function allows user to use an AdminOrg -// to also fetch a Catalog. -func (adminOrg *AdminOrg) FindCatalog(catalogName string) (Catalog, error) { - for _, catalog := range adminOrg.AdminOrg.Catalogs.Catalog { - // Get Catalog HREF - if catalog.Name == catalogName { - catalogURL := adminOrg.client.VCDHREF - catalogURL.Path += "/catalog/" + strings.Split(catalog.HREF, "/api/admin/catalog/")[1] //gets id - - cat := NewCatalog(adminOrg.client) - - _, err := adminOrg.client.ExecuteRequest(catalogURL.String(), http.MethodGet, - "", "error retrieving catalog: %s", nil, cat.Catalog) - - // The request was successful - return *cat, err - } - } - return Catalog{}, nil +// GetVDCByNameOrId finds a VDC by name or ID +// On success, returns a pointer to the VDC structure and a nil error +// On failure, returns a nil pointer and an error +func (org *Org) GetVDCByNameOrId(identifier string, refresh bool) (*Vdc, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return org.GetVDCByName(name, refresh) } + getById := func(id string, refresh bool) (interface{}, error) { return org.GetVDCById(id, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*Vdc), err } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go index fc97d5168..f061e7024 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go @@ -155,7 +155,7 @@ func CreateEdgeGatewayAsync(vcdClient *VCDClient, egwc EdgeGatewayCreation) (Tas } // Add external networks inside the configuration structure for _, extNetName := range egwc.ExternalNetworks { - extNet, err := GetExternalNetwork(vcdClient, extNetName) + extNet, err := vcdClient.GetExternalNetworkByName(extNetName) if err != nil { return Task{}, err } @@ -206,7 +206,7 @@ func CreateAndConfigureEdgeGatewayAsync(vcdClient *VCDClient, orgName, vdcName, if err != nil { return Task{}, err } - vdc, err := adminOrg.GetVdcByName(vdcName) + vdc, err := adminOrg.GetVDCByName(vdcName, false) if err != nil { return Task{}, err } @@ -275,7 +275,7 @@ func createEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation, egwConfig if err != nil { return EdgeGateway{}, err } - vdc, err := org.GetVdcByName(egwc.VdcName) + vdc, err := org.GetVDCByName(egwc.VdcName, false) if err != nil { return EdgeGateway{}, err } @@ -437,6 +437,7 @@ func QueryPortGroups(vcdCli *VCDClient, filter string) ([]*types.PortGroupRecord // GetExternalNetwork returns an ExternalNetwork reference if user the network name matches an existing one. // If no valid external network is found, it returns an empty ExternalNetwork reference and an error +// Deprecated: use vcdClient.GetExternalNetworkByName instead func GetExternalNetwork(vcdClient *VCDClient, networkName string) (*ExternalNetwork, error) { if !vcdClient.Client.IsSysAdmin { @@ -476,6 +477,94 @@ func GetExternalNetwork(vcdClient *VCDClient, networkName string) (*ExternalNetw } +// GetExternalNetworks returns a list of available external networks +func (vcdClient *VCDClient) GetExternalNetworks() (*types.ExternalNetworkReferences, error) { + + if !vcdClient.Client.IsSysAdmin { + return nil, fmt.Errorf("functionality requires system administrator privileges") + } + + extNetworkHREF, err := getExternalNetworkHref(&vcdClient.Client) + if err != nil { + return nil, err + } + + extNetworkRefs := &types.ExternalNetworkReferences{} + _, err = vcdClient.Client.ExecuteRequest(extNetworkHREF, http.MethodGet, + types.MimeNetworkConnectionSection, "error retrieving external networks: %s", nil, extNetworkRefs) + if err != nil { + return nil, err + } + + return extNetworkRefs, nil +} + +// GetExternalNetworkByName returns an ExternalNetwork reference if the network name matches an existing one. +// If no valid external network is found, it returns an empty ExternalNetwork reference and an error +func (vcdClient *VCDClient) GetExternalNetworkByName(networkName string) (*ExternalNetwork, error) { + + extNetworkRefs, err := vcdClient.GetExternalNetworks() + + if err != nil { + return nil, err + } + + externalNetwork := NewExternalNetwork(&vcdClient.Client) + + for _, netRef := range extNetworkRefs.ExternalNetworkReference { + if netRef.Name == networkName { + externalNetwork.ExternalNetwork.HREF = netRef.HREF + err = externalNetwork.Refresh() + if err != nil { + return nil, err + } + return externalNetwork, nil + } + } + + return nil, ErrorEntityNotFound +} + +// GetExternalNetworkById returns an ExternalNetwork reference if the network ID matches an existing one. +// If no valid external network is found, it returns an empty ExternalNetwork reference and an error +func (vcdClient *VCDClient) GetExternalNetworkById(id string) (*ExternalNetwork, error) { + + extNetworkRefs, err := vcdClient.GetExternalNetworks() + + if err != nil { + return nil, err + } + + externalNetwork := NewExternalNetwork(&vcdClient.Client) + + for _, netRef := range extNetworkRefs.ExternalNetworkReference { + // ExternalNetworkReference items don't have ID + // We compare using the UUID from HREF + if equalIds(id, "", netRef.HREF) { + externalNetwork.ExternalNetwork.HREF = netRef.HREF + err = externalNetwork.Refresh() + if err != nil { + return nil, err + } + return externalNetwork, nil + } + } + + return nil, ErrorEntityNotFound +} + +// GetExternalNetworkByNameOrId returns an ExternalNetwork reference if either the network name or ID matches an existing one. +// If no valid external network is found, it returns an empty ExternalNetwork reference and an error +func (vcdClient *VCDClient) GetExternalNetworkByNameOrId(identifier string) (*ExternalNetwork, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return vcdClient.GetExternalNetworkByName(name) } + getById := func(id string, refresh bool) (interface{}, error) { return vcdClient.GetExternalNetworkById(id) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, false) + if entity == nil { + return nil, err + } + return entity.(*ExternalNetwork), err +} + // CreateExternalNetwork allows create external network and returns Task or error. // types.ExternalNetwork struct is general and used for various types of networks. But for external network // fence mode is always isolated, isInherited is false, parentNetwork is empty. @@ -604,7 +693,7 @@ func GetNetworkPoolByHREF(client *VCDClient, href string) (*types.VMWNetworkPool networkPool := &types.VMWNetworkPool{} _, err := client.Client.ExecuteRequest(href, http.MethodGet, - "", "error fetching network ppol: %s", nil, networkPool) + "", "error fetching network pool: %s", nil, networkPool) // Return the disk return networkPool, err @@ -668,21 +757,13 @@ func (vcdClient *VCDClient) GetOrgById(orgId string) (*Org, error) { // On success, returns a pointer to the Org structure and a nil error // On failure, returns a nil pointer and an error func (vcdClient *VCDClient) GetOrgByNameOrId(identifier string) (*Org, error) { - var byNameErr, byIdErr error - var org *Org - org, byIdErr = vcdClient.GetOrgById(identifier) - if byIdErr == nil { - // Found by ID - return org, nil - } - if IsNotFound(byIdErr) { - // Not found by ID, try by name - org, byNameErr = vcdClient.GetOrgByName(identifier) - return org, byNameErr - } else { - // On any other error, we return it - return nil, byIdErr + getByName := func(name string, refresh bool) (interface{}, error) { return vcdClient.GetOrgByName(name) } + getById := func(id string, refresh bool) (interface{}, error) { return vcdClient.GetOrgById(id) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, false) + if entity == nil { + return nil, err } + return entity.(*Org), err } // GetAdminOrgByName finds an Admin Organization by name @@ -733,20 +814,11 @@ func (vcdClient *VCDClient) GetAdminOrgById(orgId string) (*AdminOrg, error) { // On success, returns a pointer to the Admin Org structure and a nil error // On failure, returns a nil pointer and an error func (vcdClient *VCDClient) GetAdminOrgByNameOrId(identifier string) (*AdminOrg, error) { - var byNameErr, byIdErr error - var adminOrg *AdminOrg - - adminOrg, byIdErr = vcdClient.GetAdminOrgById(identifier) - if byIdErr == nil { - // Found by ID - return adminOrg, nil - } - if IsNotFound(byIdErr) { - // Not found by ID, try by name - adminOrg, byNameErr = vcdClient.GetAdminOrgByName(identifier) - return adminOrg, byNameErr - } else { - // On any other error, we return it - return nil, byIdErr + getByName := func(name string, refresh bool) (interface{}, error) { return vcdClient.GetAdminOrgByName(name) } + getById := func(id string, refresh bool) (interface{}, error) { return vcdClient.GetAdminOrgById(id) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, false) + if entity == nil { + return nil, err } + return entity.(*AdminOrg), err } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go index 06e0bf25a..f7bbb4cab 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/user.go @@ -161,21 +161,13 @@ func (adminOrg *AdminOrg) GetUserById(id string, refresh bool) (*OrgUser, error) // If it is false, it will search within the data already in memory (useful when // looping through the users and we know that no changes have occurred in the meantime) func (adminOrg *AdminOrg) GetUserByNameOrId(identifier string, refresh bool) (*OrgUser, error) { - var byNameErr, byIdErr error - // First look by ID - orgUser, byIdErr := adminOrg.GetUserByName(identifier, true) - if byIdErr == nil { - // found by ID - return orgUser, nil - } - // If not found by ID, look by name - if IsNotFound(byIdErr) { - orgUser, byNameErr = adminOrg.GetUserById(identifier, false) - return orgUser, byNameErr - } else { - // On any other error, we return it - return nil, byIdErr + getByName := func(name string, refresh bool) (interface{}, error) { return adminOrg.GetUserByName(name, refresh) } + getById := func(name string, refresh bool) (interface{}, error) { return adminOrg.GetUserById(name, refresh) } + entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err } + return entity.(*OrgUser), err } // GetRole finds a role within the organization diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go index 1b25d2a99..37401afd5 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go @@ -30,19 +30,6 @@ func NewVdc(cli *Client) *Vdc { } } -type AdminVdc struct { - AdminVdc *types.AdminVdc - client *Client - VApp *types.VApp -} - -func NewAdminVdc(cli *Client) *AdminVdc { - return &AdminVdc{ - AdminVdc: new(types.AdminVdc), - client: cli, - } -} - // Gets a vapp with a specific url vappHREF func (vdc *Vdc) getVdcVAppbyHREF(vappHREF *url.URL) (*VApp, error) { vapp := NewVApp(vdc.client) diff --git a/vendor/modules.txt b/vendor/modules.txt index 3e91ae116..8bffbb251 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -221,7 +221,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814110727-962885792c76 +# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util From 1e6ee4d38d2003aa50f240f817c3be8cda26b21d Mon Sep 17 00:00:00 2001 From: Dainius S Date: Fri, 16 Aug 2019 09:04:10 +0300 Subject: [PATCH 13/31] Add function for handling maxRetryTimeout minimum value --- vcd/provider_test.go | 11 +++++++++++ vcd/resource_vcd_vapp_vm_customization_test.go | 7 ++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/vcd/provider_test.go b/vcd/provider_test.go index 958cd0620..418e9deb6 100644 --- a/vcd/provider_test.go +++ b/vcd/provider_test.go @@ -71,3 +71,14 @@ func createTemporaryVCDConnection() *VCDClient { } return conn } + +// minIfLess returns: +// `min` if `value` is less than min +// `value` if `value` > `min` +func minIfLess(min, value int) int { + if value < min { + return min + } + + return value +} diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index f9433c078..261aefc3a 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -4,10 +4,11 @@ package vcd import ( "fmt" + "testing" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "github.com/vmware/go-vcloud-director/v2/govcd" - "testing" ) // TestAccVcdVAppVmCustomization tests that setting attribute customizaton.force to `true` triggers VM customization @@ -122,7 +123,7 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso // Not using maxRetryTimeout for timeout here because it would force for maxRetryTimeout to be quite long // time by default as it takes some time (around 150s during testing) for Photon OS to boot // first time and get rid of "GC_PENDING" state - err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) + err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", minIfLess(300, conn.Client.MaxRetryTimeout)) if err != nil { return err } @@ -144,7 +145,7 @@ func testAccCheckVcdVMCustomization(node string, customizationPending bool) reso } if customizationPending && customizationStatus == "GC_PENDING" { - err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", 300) + err = vm.BlockWhileGuestCustomizationStatus("GC_PENDING", minIfLess(300, conn.Client.MaxRetryTimeout)) if err != nil { return fmt.Errorf("timed out waiting for VM %s to leave 'GC_PENDING' state: %s", vm.VM.Name, err) } From 061dfcb06bb5a59dba71f15adc8c484caed87f93 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Fri, 16 Aug 2019 09:09:19 +0300 Subject: [PATCH 14/31] cleanup changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa15d700..709665642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ IMPROVEMENTS: * `vcd_org` Add import capability * `resource/catalog_item` added catalog item metadata support [#298] * `resource/catalog_media` added catalog media item metadata support [#298] -* `vcd_vapp_vm` supports update for `network` block [#310] -* `vcd_vapp_vm` allows to force guest customization [#310] +* `resource/vcd_vapp_vm` supports update for `network` block [#310] +* `resource/vcd_vapp_vm` allows to force guest customization [#310] * Upgrade Terraform SDK dependency to 0.12.6 [#302] BUG FIXES: From 3871106c27aa5e1ef34ab8be16f1b750d0a34dc5 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Fri, 16 Aug 2019 15:42:32 +0300 Subject: [PATCH 15/31] Hack - WIP --- vcd/resource_vcd_vapp_vm.go | 78 ++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 8d184911a..41e9bcc06 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -562,11 +562,8 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } - // Check if the user request for forced reconfiguration of VM - customizationNeeded := isForcedConfiguration(d.Get("customization")) - if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || - d.HasChange("expose_hardware_virtualization") || d.HasChange("network") || customizationNeeded { + d.HasChange("expose_hardware_virtualization") || d.HasChange("network") { if vmStatusBeforeUpdate != "POWERED_OFF" { task, err := vm.PowerOff() if err != nil { @@ -651,39 +648,64 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er // If the VM was powered off during update but it has to be powered off if d.Get("power_on").(bool) { + // Check if the user requested for forced customization of VM + customizationNeeded := isForcedConfiguration(d.Get("customization")) vmStatus, err := vm.GetStatus() if err != nil { return fmt.Errorf("error getting VM status before ensuring it is powered on: %s", err) } + log.Printf("[DEBUG] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) - log.Printf("[TRACE] Powering on VM %s after update. Previous state %s", vm.VM.Name, vmStatus) - - if vmStatus != "POWERED_ON" && customizationNeeded { - - // The VM must be un-deployed for customization to actually work. The option "Power off" in GUI - // actually does un-deploy as well. - task, err := vm.Undeploy() + // Simply power on if customization is not requested + if !customizationNeeded && vmStatus != "POWERED_ON" { + task, err := vm.PowerOn() if err != nil { - return fmt.Errorf("error triggering undeploy for VM %s: %s", vm.VM.Name, err) + return fmt.Errorf("error powering on: %s", err) } err = task.WaitTaskCompletion() if err != nil { - return fmt.Errorf("error waiting for undeploy task for VM %s: %s", vm.VM.Name, err) - } - err = vm.PowerOnAndForceCustomization() - if err != nil { - return fmt.Errorf("failed powering on with customization: %s", err) + return fmt.Errorf(errorCompletingTask, err) } } - if vmStatus != "POWERED_ON" && !customizationNeeded { - task, err := vm.PowerOn() + // When customization is requested VM must be un-deployed before starting it + if customizationNeeded { + log.Printf("[TRACE] forced customization for VM %s was requested. Current state %s", + vm.VM.Name, vmStatus) + + // Check if VM is deployed + isDeployed, err := vm.IsDeployed() if err != nil { - return fmt.Errorf("error powering on: %s", err) + return fmt.Errorf("unable to check if VM is deployed: %s", err) } - err = task.WaitTaskCompletion() + + log.Printf("[TRACE] VM %s deployment state: %t", vm.VM.Name, isDeployed) + + if vmStatus == "POWERED_OFF" { + t, _ := vm.PowerOn() + _ = t.WaitTaskCompletion() + } + + vmStatus, _ = vm.GetStatus() + + // if isDeployed { + if vmStatus != "POWERED_OFF" { + log.Printf("[TRACE] VM %s is in state %s. Un-deploying", vm.VM.Name, vmStatus) + task, err := vm.Undeploy() + if err != nil { + return fmt.Errorf("error triggering undeploy for VM %s: %s", vm.VM.Name, err) + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for undeploy task for VM %s: %s", vm.VM.Name, err) + } + } + // } + + log.Printf("[TRACE] Powering on VM %s with forced customization", vm.VM.Name) + err = vm.PowerOnAndForceCustomization() if err != nil { - return fmt.Errorf(errorCompletingTask, err) + return fmt.Errorf("failed powering on with customization: %s", err) } } } @@ -892,7 +914,7 @@ func resourceVcdVAppVmDelete(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error getting VM status: %#v", err) } - log.Printf("[TRACE] VM Status:: %s", status) + log.Printf("[TRACE] VM Status: %s", status) if status != "POWERED_OFF" { log.Printf("[TRACE] Undeploying VM: %s", vm.VM.Name) task, err := vm.Undeploy() @@ -927,6 +949,16 @@ func resourceVcdVAppVmDelete(d *schema.ResourceData, meta interface{}) error { } log.Printf("[TRACE] Removing VM: %s", vm.VM.Name) + + // status, err = vm.GetStatus() + // if err != nil { + // return fmt.Errorf("error getting VM status: %#v", err) + // } + // log.Printf("[TRACE] VM Status before removal: %s", status) + + // t, _ := vm.PowerOff() + // _ = t.WaitTaskCompletion() + err = vapp.RemoveVM(vm) if err != nil { return fmt.Errorf("error deleting: %#v", err) From 311c072eb44f2a0861d4bf6bfa3ef3d7ae4934d7 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 19 Aug 2019 11:11:00 +0300 Subject: [PATCH 16/31] Fix in place --- go.mod | 2 +- go.sum | 4 +- vcd/resource_vcd_vapp_vm.go | 46 +++++++++++-------- ...resource_vcd_vapp_vm_customization_test.go | 42 ++++++++++++++++- .../vmware/go-vcloud-director/v2/govcd/vm.go | 2 +- .../v2/types/v56/constants.go | 2 +- vendor/modules.txt | 2 +- 7 files changed, 73 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index ed639e8b4..01070c922 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 ) -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb +replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190816092212-45ef284f6029 diff --git a/go.sum b/go.sum index 2ac0ef10d..5cdbdc5ee 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb h1:mwKcO3Bj5wVEJ+X513CK+ggqWKhFtCwvt/2+BJLKkH4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190816092212-45ef284f6029 h1:KzobNk8fjbklHWpnv1NyZbPCiXT2AYlUSPPqr9ic9Jc= +github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190816092212-45ef284f6029/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 41e9bcc06..bf73a081d 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -527,6 +527,9 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error getting VM status before update: %#v", err) } + // Check if the user requested for forced customization of VM + customizationNeeded := isForcedConfiguration(d.Get("customization")) + // VM does not have to be in POWERED_OFF state for metadata operations if d.HasChange("metadata") { oldRaw, newRaw := d.GetChange("metadata") @@ -564,7 +567,15 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || d.HasChange("expose_hardware_virtualization") || d.HasChange("network") { - if vmStatusBeforeUpdate != "POWERED_OFF" { + + log.Printf("[TRACE] VM %s has changes: memory(%t), cpus(%t), cpu_cores(%t), power_on(%t), disk(%t), expose_hardware_virtualization(%t), network(%t)", + vm.VM.Name, d.HasChange("memory"), d.HasChange("cpus"), d.HasChange("cpu_cores"), d.HasChange("power_on"), d.HasChange("disk"), + d.HasChange("expose_hardware_virtualization"), d.HasChange("network")) + + // If customization is not requested then a simple shutdown is enough + if vmStatusBeforeUpdate != "POWERED_OFF" && !customizationNeeded { + log.Printf("[DEBUG] Powering off VM %s for offline update. Previous state %s", + vm.VM.Name, vmStatusBeforeUpdate) task, err := vm.PowerOff() if err != nil { return fmt.Errorf("error Powering Off: %#v", err) @@ -575,6 +586,20 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } + // If customization was requested then a shutdown with undeploy is needed + if vmStatusBeforeUpdate != "POWERED_OFF" && customizationNeeded { + log.Printf("[DEBUG] Un-deploying VM %s for offline update. Previous state %s", + vm.VM.Name, vmStatusBeforeUpdate) + task, err := vm.Undeploy() + if err != nil { + return fmt.Errorf("error triggering undeploy for VM %s: %s", vm.VM.Name, err) + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for undeploy task for VM %s: %s", vm.VM.Name, err) + } + } + // detaching independent disks - only possible when VM power off if d.HasChange("disk") { err = attachDetachDisks(d, vm, vdc) @@ -648,8 +673,6 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er // If the VM was powered off during update but it has to be powered off if d.Get("power_on").(bool) { - // Check if the user requested for forced customization of VM - customizationNeeded := isForcedConfiguration(d.Get("customization")) vmStatus, err := vm.GetStatus() if err != nil { return fmt.Errorf("error getting VM status before ensuring it is powered on: %s", err) @@ -673,22 +696,6 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er log.Printf("[TRACE] forced customization for VM %s was requested. Current state %s", vm.VM.Name, vmStatus) - // Check if VM is deployed - isDeployed, err := vm.IsDeployed() - if err != nil { - return fmt.Errorf("unable to check if VM is deployed: %s", err) - } - - log.Printf("[TRACE] VM %s deployment state: %t", vm.VM.Name, isDeployed) - - if vmStatus == "POWERED_OFF" { - t, _ := vm.PowerOn() - _ = t.WaitTaskCompletion() - } - - vmStatus, _ = vm.GetStatus() - - // if isDeployed { if vmStatus != "POWERED_OFF" { log.Printf("[TRACE] VM %s is in state %s. Un-deploying", vm.VM.Name, vmStatus) task, err := vm.Undeploy() @@ -700,7 +707,6 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error waiting for undeploy task for VM %s: %s", vm.VM.Name, err) } } - // } log.Printf("[TRACE] Powering on VM %s with forced customization", vm.VM.Name) err = vm.PowerOnAndForceCustomization() diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index 261aefc3a..e735db7e8 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -40,6 +40,9 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { params["FuncName"] = t.Name() + "-step1" configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmCustomizationStep1, params) + params["FuncName"] = t.Name() + "-step2" + configTextVMUpdateStep2 := templateFill(testAccCheckVcdVAppVmCustomizationStep2, params) + if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -63,7 +66,7 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "0"), ), }, - // Step 1 - Change network configuration and force customization + // Step 1 - Update - change network configuration and force customization resource.TestStep{ Config: configTextVMUpdateStep1, Check: resource.ComposeAggregateTestCheckFunc( @@ -76,6 +79,18 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "true"), ), }, + // Step 2 - Create new VM and force customization initially + resource.TestStep{ + Config: configTextVMUpdateStep2, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm2", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "network.#", "1"), + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "customization.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "customization.0.force", "true"), + ), + }, }, }) } @@ -232,3 +247,28 @@ resource "vcd_vapp_vm" "test-vm" { } } ` + +const testAccCheckVcdVAppVmCustomizationStep2 = testAccCheckVcdVAppVmCustomizationShared + ` +resource "vcd_vapp_vm" "test-vm2" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = "${vcd_vapp.test-vapp.name}" + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + + network { + type = "vapp" + name = "${vcd_vapp_network.vappNet.name}" + ip_allocation_mode = "POOL" + } + + customization { + force = true + } +} +` diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index c6735905b..c3debd13c 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -467,7 +467,7 @@ func (vm *VM) Undeploy() (Task, error) { // Return the task return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, - types.MimeUndeployVappParams, "error undeploy vApp: %s", vu) + types.MimeUndeployVappParams, "error undeploy VM: %s", vu) } // Attach or detach an independent disk diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go index a524aec93..3eda85af2 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go @@ -161,7 +161,7 @@ const ( LbVirtualServerPath = "/loadbalancer/config/virtualservers/" ) -// Guest customization statues. These are all known possible statuses +// Guest customization statuses. These are all known possible statuses const ( GuestCustStatusPending = "GC_PENDING" GuestCustStatusPostPending = "POST_GC_PENDING" diff --git a/vendor/modules.txt b/vendor/modules.txt index 8bffbb251..f0a601549 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -221,7 +221,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190814151258-f90d248e74eb +# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha-2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190816092212-45ef284f6029 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util From 2e5e5b7d6ee4aee9cb9d126e67eb650982f74adc Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 19 Aug 2019 11:14:20 +0300 Subject: [PATCH 17/31] Remove commented block --- vcd/resource_vcd_vapp_vm.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index bf73a081d..63ff2f641 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -956,15 +956,6 @@ func resourceVcdVAppVmDelete(d *schema.ResourceData, meta interface{}) error { log.Printf("[TRACE] Removing VM: %s", vm.VM.Name) - // status, err = vm.GetStatus() - // if err != nil { - // return fmt.Errorf("error getting VM status: %#v", err) - // } - // log.Printf("[TRACE] VM Status before removal: %s", status) - - // t, _ := vm.PowerOff() - // _ = t.WaitTaskCompletion() - err = vapp.RemoveVM(vm) if err != nil { return fmt.Errorf("error deleting: %#v", err) From 85757ad92bf31b355381e08dad8899f19168b9ff Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 19 Aug 2019 11:40:34 +0300 Subject: [PATCH 18/31] Tune docs --- website/docs/r/vapp_vm.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index 9121249d3..3ed4e1101 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -182,4 +182,4 @@ example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_n * `force` (Optional) This field works as a flag and triggers force customization when `true` during an update (`terraform apply`) every time. It never complains about a change in statefile. It can be used when guest customization is needed after a NIC change and then set back to `false`. **Note** this setting will cause a VM reboot and will not -work when `power_on` field is set to `false`. +have effect when `power_on` field is set to `false`. From 066aa7f6530f3eb628b165ce50f3aeae087db12e Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 19 Aug 2019 16:37:57 +0300 Subject: [PATCH 19/31] Update description --- website/docs/r/vapp_vm.html.markdown | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index 3ed4e1101..937728a55 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -106,6 +106,9 @@ resource "vcd_vapp_vm" "web2" { ``` + +## Example Usage with forced customization + ## Argument Reference The following arguments are supported: @@ -179,7 +182,8 @@ example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_n ## Customization -* `force` (Optional) This field works as a flag and triggers force customization when `true` during an update -(`terraform apply`) every time. It never complains about a change in statefile. It can be used when guest customization -is needed after a NIC change and then set back to `false`. **Note** this setting will cause a VM reboot and will not -have effect when `power_on` field is set to `false`. +* `force` (Optional) **Warning.** `true` value will cause the VM to reboot on every `apply` operation. +This field works as a flag and triggers force customization when `true` during an update +(`terraform apply`) every time. It never complains about a change in statefile. Can be used when guest customization +is needed after VM configuration (e.g. NIC change, customization options change, etc.) and then set back to `false`. +**Note.** It will not have effect when `power_on` field is set to `false`. From f52752f72340f5119a5dc212330135c7c7f803a2 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Mon, 19 Aug 2019 18:04:50 +0300 Subject: [PATCH 20/31] WIP docs --- website/docs/r/vapp_vm.html.markdown | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index 937728a55..3ac810c6d 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -109,6 +109,59 @@ resource "vcd_vapp_vm" "web2" { ## Example Usage with forced customization +Step 1 - setup VM +``` +resource "vcd_vapp_vm" "web2" { + vapp_name = "${vcd_vapp.web.name}" + name = "web2" + catalog_name = "Boxes" + template_name = "lampstack-1.10.1-ubuntu-10.04" + memory = 2048 + cpus = 1 + + network { + type = "org" + name = "net" + ip = "10.10.104.162" + ip_allocation_mode = "MANUAL" + is_primary = true + } + +} +``` + +Step 2 - change network configuration and force customization +``` +resource "vcd_vapp_vm" "web2" { + vapp_name = "${vcd_vapp.web.name}" + name = "web2" + catalog_name = "Boxes" + template_name = "lampstack-1.10.1-ubuntu-10.04" + memory = 2048 + cpus = 1 + + network { + type = "org" + name = "net" + ip = "10.10.104.162" + ip_allocation_mode = "MANUAL" + is_primary = true + } + + network { + type = "org" + name = "net" + ip = "10.10.108.162" + ip_allocation_mode = "MANUAL" + } + + customization { + force = true + } + +} +``` + ## Argument Reference The following arguments are supported: From 8e7525e181e5fda66dec6c6b46ce746fb4e0227c Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 21 Aug 2019 14:26:33 +0300 Subject: [PATCH 21/31] Add forced customization demo steps --- website/docs/r/vapp_vm.html.markdown | 47 +++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index 3ac810c6d..b1d3b938a 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -107,10 +107,11 @@ resource "vcd_vapp_vm" "web2" { ``` -## Example Usage with forced customization +## Example forced customization workflow -Step 1 - setup VM -``` +Step 1 - Setup VM: + +```hcl resource "vcd_vapp_vm" "web2" { vapp_name = "${vcd_vapp.web.name}" name = "web2" @@ -124,41 +125,43 @@ resource "vcd_vapp_vm" "web2" { name = "net" ip = "10.10.104.162" ip_allocation_mode = "MANUAL" - is_primary = true } - } ``` -Step 2 - change network configuration and force customization -``` -resource "vcd_vapp_vm" "web2" { - vapp_name = "${vcd_vapp.web.name}" - name = "web2" - catalog_name = "Boxes" - template_name = "lampstack-1.10.1-ubuntu-10.04" - memory = 2048 - cpus = 1 +Step 2 - Change VM configuration and force customization (VM will be rebooted during +`terraform apply`): +```hcl +resource "vcd_vapp_vm" "web2" { +... network { type = "org" name = "net" - ip = "10.10.104.162" - ip_allocation_mode = "MANUAL" - is_primary = true + ip_allocation_mode = "DHCP" } + customization { + force = true + } +} +``` + +Step 3 - Once customization is done, set the force customization flag to false (or remove it) to +prevent forcing customization on every `terraform apply` command: + +```hcl +resource "vcd_vapp_vm" "web2" { +... network { type = "org" name = "net" - ip = "10.10.108.162" - ip_allocation_mode = "MANUAL" + ip_allocation_mode = "DHCP" } customization { - force = true + force = false } - } ``` @@ -239,4 +242,4 @@ example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_n This field works as a flag and triggers force customization when `true` during an update (`terraform apply`) every time. It never complains about a change in statefile. Can be used when guest customization is needed after VM configuration (e.g. NIC change, customization options change, etc.) and then set back to `false`. -**Note.** It will not have effect when `power_on` field is set to `false`. +**Note.** It will not have effect when `power_on` field is set to `false`. See [example workflow above](#example-forced-customization-workflow). From db66f6a588fa0920be14b912801a84e700e76e6f Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 21 Aug 2019 15:00:27 +0300 Subject: [PATCH 22/31] Fix for latest govcd master --- vcd/resource_vcd_external_network.go | 4 +--- vcd/resource_vcd_vapp_vm.go | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/vcd/resource_vcd_external_network.go b/vcd/resource_vcd_external_network.go index 63e058ce1..8192505c1 100644 --- a/vcd/resource_vcd_external_network.go +++ b/vcd/resource_vcd_external_network.go @@ -206,9 +206,7 @@ func resourceVcdExternalNetworkDelete(d *schema.ResourceData, meta interface{}) // any cast operations or default values should be done here so that the create method is simple func getExternalNetworkInput(d *schema.ResourceData, vcdClient *VCDClient) (*types.ExternalNetwork, error) { params := &types.ExternalNetwork{ - Name: d.Get("name").(string), - Xmlns: types.XMLNamespaceExtension, - XmlnsVCloud: types.XMLNamespaceVCloud, + Name: d.Get("name").(string), Configuration: &types.NetworkConfiguration{ Xmlns: types.XMLNamespaceVCloud, RetainNetInfoAcrossDeployments: d.Get("retain_net_info_across_deployments").(bool), diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index c1150ce8f..d21fb336a 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -305,10 +305,10 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { // TODO v3.0 remove else branch once 'network_name', 'vapp_network_name', 'ip' are deprecated networkConnectionSection := types.NetworkConnectionSection{} if len(d.Get("network").([]interface{})) > 0 { - networkConnectionSection, err = networksToConfig(d.Get("network").([]interface{}), *vdc, vapp, vcdClient) + networkConnectionSection, err = networksToConfig(d.Get("network").([]interface{}), vdc, vapp, vcdClient) } else { networkConnectionSection, err = deprecatedNetworksToConfig(d.Get("network_name").(string), - d.Get("vapp_network_name").(string), d.Get("ip").(string), *vdc, vapp, vcdClient) + d.Get("vapp_network_name").(string), d.Get("ip").(string), vdc, vapp, vcdClient) } if err != nil { return fmt.Errorf("unable to process network configuration: %s", err) @@ -362,7 +362,7 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { // TODO do not trigger resourceVcdVAppVmUpdate from create. These must be separate actions. err = resourceVcdVAppVmUpdateExecute(d, meta) if err != nil { - errAttachedDisk := updateStateOfAttachedDisks(d, vm, *vdc) + errAttachedDisk := updateStateOfAttachedDisks(d, vm, vdc) if errAttachedDisk != nil { d.Set("disk", nil) return fmt.Errorf("error reading attached disks : %#v and internal error : %#v", errAttachedDisk, err) @@ -374,7 +374,7 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { // Adds existing org VDC network to VM network configuration // Returns configured OrgVDCNetwork for Vm, networkName, error if any occur -func addVdcNetwork(networkNameToAdd string, vdc govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (*types.OrgVDCNetwork, error) { +func addVdcNetwork(networkNameToAdd string, vdc *govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (*types.OrgVDCNetwork, error) { if networkNameToAdd == "" { return &types.OrgVDCNetwork{}, fmt.Errorf("'network_name' must be valid when adding VM to raw vApp") } @@ -602,9 +602,9 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er // detaching independent disks - only possible when VM power off if d.HasChange("disk") { - err = attachDetachDisks(d, vm, *vdc) + err = attachDetachDisks(d, vm, vdc) if err != nil { - errAttachedDisk := updateStateOfAttachedDisks(d, vm, *vdc) + errAttachedDisk := updateStateOfAttachedDisks(d, vm, vdc) if errAttachedDisk != nil { d.Set("disk", nil) return fmt.Errorf("error reading attached disks : %#v and internal error : %#v", errAttachedDisk, err) @@ -720,7 +720,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } // updates attached disks to latest state. Removed not needed and add new ones -func attachDetachDisks(d *schema.ResourceData, vm govcd.VM, vdc govcd.Vdc) error { +func attachDetachDisks(d *schema.ResourceData, vm govcd.VM, vdc *govcd.Vdc) error { oldValues, newValues := d.GetChange("disk") attachDisks := newValues.(*schema.Set).Difference(oldValues.(*schema.Set)) @@ -839,7 +839,7 @@ func resourceVcdVAppVmRead(d *schema.ResourceData, meta interface{}) error { d.Set("href", vm.VM.HREF) d.Set("expose_hardware_virtualization", vm.VM.NestedHypervisorEnabled) - err = updateStateOfAttachedDisks(d, vm, *vdc) + err = updateStateOfAttachedDisks(d, vm, vdc) if err != nil { d.Set("disk", nil) return fmt.Errorf("error reading attached disks : %#v", err) @@ -848,7 +848,7 @@ func resourceVcdVAppVmRead(d *schema.ResourceData, meta interface{}) error { return nil } -func updateStateOfAttachedDisks(d *schema.ResourceData, vm govcd.VM, vdc govcd.Vdc) error { +func updateStateOfAttachedDisks(d *schema.ResourceData, vm govcd.VM, vdc *govcd.Vdc) error { // Check VM independent disks state diskProperties, err := expandDisksProperties(d.Get("disk")) if err != nil { @@ -982,7 +982,7 @@ func resourceVcdVmIndependentDiskHash(v interface{}) int { // networksToConfig converts terraform schema for 'networks' and converts to types.NetworkConnectionSection // which is used for creating new VM -func networksToConfig(networks []interface{}, vdc govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (types.NetworkConnectionSection, error) { +func networksToConfig(networks []interface{}, vdc *govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (types.NetworkConnectionSection, error) { networkConnectionSection := types.NetworkConnectionSection{} for index, singleNetwork := range networks { nic := singleNetwork.(map[string]interface{}) @@ -1033,7 +1033,7 @@ func networksToConfig(networks []interface{}, vdc govcd.Vdc, vapp govcd.VApp, vc // deprecatedNetworksToConfig converts deprecated network configuration in fields // TODO v3.0 remove this function once 'network_name', 'vapp_network_name', 'ip' are deprecated -func deprecatedNetworksToConfig(network_name, vapp_network_name, ip string, vdc govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (types.NetworkConnectionSection, error) { +func deprecatedNetworksToConfig(network_name, vapp_network_name, ip string, vdc *govcd.Vdc, vapp govcd.VApp, vcdClient *VCDClient) (types.NetworkConnectionSection, error) { if vapp_network_name != "" { isVappNetwork, err := isItVappNetwork(vapp_network_name, vapp) if err != nil { From 16dbffa3daa7ca72fe6561f4141d7837719e9baf Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 21 Aug 2019 15:39:42 +0300 Subject: [PATCH 23/31] Paralellize guest customization tests --- ...resource_vcd_vapp_vm_customization_test.go | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index e735db7e8..6fd315d28 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -11,12 +11,12 @@ import ( "github.com/vmware/go-vcloud-director/v2/govcd" ) -// TestAccVcdVAppVmCustomization tests that setting attribute customizaton.force to `true` triggers VM customization -// and waits until it is completed. +// TestAccVcdVAppVmUpdateCustomization tests that setting attribute customizaton.force to `true` +// during update triggers VM customization and waits until it is completed. // It is important to wait until the operation is completed to test what VM was properly handled before triggering // power on and force customization. (VM must be un-deployed for customization to work, otherwise it would stay in // "GC_PENDING" state for long time) -func TestAccVcdVAppVmCustomization(t *testing.T) { +func TestAccVcdVAppVmUpdateCustomization(t *testing.T) { var ( vapp govcd.VApp vm govcd.VM @@ -35,21 +35,17 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { "Tags": "vapp vm", } - configTextVM := templateFill(testAccCheckVcdVAppVmCustomization, params) + configTextVM := templateFill(testAccCheckVcdVAppVmUpdateCustomization, params) params["FuncName"] = t.Name() + "-step1" - configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmCustomizationStep1, params) - - params["FuncName"] = t.Name() + "-step2" - configTextVMUpdateStep2 := templateFill(testAccCheckVcdVAppVmCustomizationStep2, params) - + configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmUpdateCustomizationStep1, params) if vcdShortTest { t.Skip(acceptanceTestsSkipped) return } debugPrintf("#[DEBUG] CONFIGURATION: %s\n", configTextVM) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), @@ -79,7 +75,47 @@ func TestAccVcdVAppVmCustomization(t *testing.T) { resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "true"), ), }, - // Step 2 - Create new VM and force customization initially + }, + }) +} + +// TestAccVcdVAppVmCreateCustomization tests that setting attribute customizaton.force to `true` +// during create triggers VM customization and waits until it is completed. +// It is important to wait until the operation is completed to test what VM was properly handled before triggering +// power on and force customization. (VM must be un-deployed for customization to work, otherwise it would stay in +// "GC_PENDING" state for long time) +func TestAccVcdVAppVmCreateCustomization(t *testing.T) { + var ( + vapp govcd.VApp + vm govcd.VM + netVappName string = t.Name() + netVmName1 string = t.Name() + "VM" + ) + + var params = StringMap{ + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "Catalog": testSuiteCatalogName, + "CatalogItem": testSuiteCatalogOVAItem, + "VAppName": netVappName, + "VMName": netVmName1, + "Tags": "vapp vm", + } + + configTextVMUpdateStep2 := templateFill(testAccCheckVcdVAppVmCreateCustomization, params) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), + Steps: []resource.TestStep{ + // Step 0 - Create new VM and force customization initially resource.TestStep{ Config: configTextVMUpdateStep2, Check: resource.ComposeAggregateTestCheckFunc( @@ -197,7 +233,7 @@ resource "vcd_vapp_network" "vappNet" { } ` -const testAccCheckVcdVAppVmCustomization = testAccCheckVcdVAppVmCustomizationShared + ` +const testAccCheckVcdVAppVmUpdateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -218,7 +254,7 @@ resource "vcd_vapp_vm" "test-vm" { } ` -const testAccCheckVcdVAppVmCustomizationStep1 = testAccCheckVcdVAppVmCustomizationShared + ` +const testAccCheckVcdVAppVmUpdateCustomizationStep1 = testAccCheckVcdVAppVmCustomizationShared + ` resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -248,7 +284,7 @@ resource "vcd_vapp_vm" "test-vm" { } ` -const testAccCheckVcdVAppVmCustomizationStep2 = testAccCheckVcdVAppVmCustomizationShared + ` +const testAccCheckVcdVAppVmCreateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` resource "vcd_vapp_vm" "test-vm2" { org = "{{.Org}}" vdc = "{{.Vdc}}" From af9fe0fa87dff31aedf066cda5922a2ae0083f0a Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 21 Aug 2019 18:35:12 +0300 Subject: [PATCH 24/31] With parallel tests --- vcd/resource_vcd_edgegateway_test.go | 2 +- vcd/resource_vcd_lb_app_profile_test.go | 2 +- vcd/resource_vcd_lb_app_rule_test.go | 2 +- vcd/resource_vcd_lb_server_pool_test.go | 2 +- vcd/resource_vcd_lb_service_monitor_test.go | 2 +- vcd/resource_vcd_lb_virtual_server_test.go | 2 +- vcd/resource_vcd_vapp_vm_hw_virtualization_test.go | 2 +- vcd/resource_vcd_vapp_vm_singlenetwork_test.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/vcd/resource_vcd_edgegateway_test.go b/vcd/resource_vcd_edgegateway_test.go index 39e92f0b5..0b30ec006 100644 --- a/vcd/resource_vcd_edgegateway_test.go +++ b/vcd/resource_vcd_edgegateway_test.go @@ -51,7 +51,7 @@ func TestAccVcdEdgeGatewayBasic(t *testing.T) { return } debugPrintf("#[DEBUG] CONFIGURATION: %s", configText) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVcdEdgeGatewayDestroyBasic, diff --git a/vcd/resource_vcd_lb_app_profile_test.go b/vcd/resource_vcd_lb_app_profile_test.go index 615143a05..bb91909f1 100644 --- a/vcd/resource_vcd_lb_app_profile_test.go +++ b/vcd/resource_vcd_lb_app_profile_test.go @@ -56,7 +56,7 @@ func TestAccVcdLBAppProfile(t *testing.T) { return } - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdLBAppProfileDestroy(params["AppProfileName"].(string)), diff --git a/vcd/resource_vcd_lb_app_rule_test.go b/vcd/resource_vcd_lb_app_rule_test.go index 61f53e26a..1650b48b2 100644 --- a/vcd/resource_vcd_lb_app_rule_test.go +++ b/vcd/resource_vcd_lb_app_rule_test.go @@ -63,7 +63,7 @@ acl other_page2 url_beg / other2 redirect location https://www.other2.com/ ifoth return } - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdLBAppRuleDestroy(params["AppRuleName"].(string)), diff --git a/vcd/resource_vcd_lb_server_pool_test.go b/vcd/resource_vcd_lb_server_pool_test.go index b97ab84a6..fa335cfaa 100644 --- a/vcd/resource_vcd_lb_server_pool_test.go +++ b/vcd/resource_vcd_lb_server_pool_test.go @@ -43,7 +43,7 @@ func TestAccVcdLbServerPool(t *testing.T) { return } - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdLbServerPoolDestroy(params["ServerPoolName"].(string)), diff --git a/vcd/resource_vcd_lb_service_monitor_test.go b/vcd/resource_vcd_lb_service_monitor_test.go index b31988453..e272f0a89 100644 --- a/vcd/resource_vcd_lb_service_monitor_test.go +++ b/vcd/resource_vcd_lb_service_monitor_test.go @@ -47,7 +47,7 @@ func TestAccVcdLbServiceMonitor(t *testing.T) { t.Skip(t.Name() + "requires advanced edge gateway to work") } - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdLbServiceMonitorDestroy(params["ServiceMonitorName"].(string)), diff --git a/vcd/resource_vcd_lb_virtual_server_test.go b/vcd/resource_vcd_lb_virtual_server_test.go index 629c5fe56..9bbb50d33 100644 --- a/vcd/resource_vcd_lb_virtual_server_test.go +++ b/vcd/resource_vcd_lb_virtual_server_test.go @@ -38,7 +38,7 @@ func TestAccVcdLbVirtualServer(t *testing.T) { return } - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdLbVirtualServerDestroy(params["VirtualServerName"].(string)), diff --git a/vcd/resource_vcd_vapp_vm_hw_virtualization_test.go b/vcd/resource_vcd_vapp_vm_hw_virtualization_test.go index 19e3d20c4..5876928e1 100644 --- a/vcd/resource_vcd_vapp_vm_hw_virtualization_test.go +++ b/vcd/resource_vcd_vapp_vm_hw_virtualization_test.go @@ -39,7 +39,7 @@ func TestAccVcdVAppVm_HardwareVirtualization(t *testing.T) { return } debugPrintf("#[DEBUG] CONFIGURATION: %s\n", configTextStep0) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVcdVAppVmDestroy(vappNameHwVirt), diff --git a/vcd/resource_vcd_vapp_vm_singlenetwork_test.go b/vcd/resource_vcd_vapp_vm_singlenetwork_test.go index e3d08f972..9bf15684b 100644 --- a/vcd/resource_vcd_vapp_vm_singlenetwork_test.go +++ b/vcd/resource_vcd_vapp_vm_singlenetwork_test.go @@ -94,7 +94,7 @@ func TestAccVcdVAppVmSingleNIC(t *testing.T) { debugPrintf("#[DEBUG] CONFIGURATION (dhcp): %s\n", configTextStep2) debugPrintf("#[DEBUG] CONFIGURATION (manual IP): %s\n", configTextStep4) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: resource.ComposeAggregateTestCheckFunc( From 1744902f5522551ed7887fad479b54c423c544d1 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Wed, 21 Aug 2019 18:43:33 +0300 Subject: [PATCH 25/31] isForcedConfiguration() -> isForcedCustomization() --- vcd/resource_vcd_vapp_vm.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index d21fb336a..8717dd2ca 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -528,7 +528,7 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } // Check if the user requested for forced customization of VM - customizationNeeded := isForcedConfiguration(d.Get("customization")) + customizationNeeded := isForcedCustomization(d.Get("customization")) // VM does not have to be in POWERED_OFF state for metadata operations if d.HasChange("metadata") { @@ -1178,9 +1178,9 @@ func readNetworks(vm govcd.VM, vapp govcd.VApp) ([]map[string]interface{}, error return nets, nil } -// isForcedConfiguration checks "customization" block in resource and checks if the value of field "force" +// isForcedCustomization checks "customization" block in resource and checks if the value of field "force" // is set to "true". It returns false if the value is not set or is set to false -func isForcedConfiguration(customizationBlock interface{}) bool { +func isForcedCustomization(customizationBlock interface{}) bool { customizationSlice := customizationBlock.([]interface{}) if len(customizationSlice) != 1 { From 9bc4db85c13bf184b304958aeeb84ec71f2f13a7 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 07:43:54 +0300 Subject: [PATCH 26/31] Add 'make seqtestacc' for forcing sequential acceptance tests --- GNUmakefile | 6 +++++- scripts/runtest.sh | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index a3ddb6197..9a876ef34 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -54,6 +54,10 @@ test: testunit testacc: testunit @sh -c "'$(CURDIR)/scripts/runtest.sh' acceptance" +# Runs full acceptance test sequentially (using "-parallel 1" flag for go test) +seqtestacc: testunit + @sh -c "'$(CURDIR)/scripts/runtest.sh' sequential-acceptance" + # Runs the acceptance test with tag 'multiple' testmulti: fmtcheck @sh -c "'$(CURDIR)/scripts/runtest.sh' multiple" @@ -141,5 +145,5 @@ ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) endif @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider-test PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) -.PHONY: build test testacc vet fmt fmtcheck errcheck vendor-check test-compile website website-test +.PHONY: build test seqtestacc testacc vet fmt fmtcheck errcheck vendor-check test-compile website website-test diff --git a/scripts/runtest.sh b/scripts/runtest.sh index c1939b343..03ac97eeb 100755 --- a/scripts/runtest.sh +++ b/scripts/runtest.sh @@ -22,7 +22,7 @@ then VERBOSE=1 fi -accepted="[short acceptance multiple binary binary-prepare catalog gateway vapp vm network extnetwork multinetwork short-provider lb user]" +accepted="[short acceptance sequential-acceptance multiple binary binary-prepare catalog gateway vapp vm network extnetwork multinetwork short-provider lb user]" if [ -z "$wanted" ] then echo "Syntax: test TYPE" @@ -107,6 +107,26 @@ function acceptance_test { fi } +function sequential_acceptance_test { + tags="$1" + parallel="$2" + if [ -z "$tags" ] + then + tags=functional + fi + if [ -n "$VERBOSE" ] + then + echo "# check for config file" + echo "TF_ACC=1 go test -tags '$tags' -v -timeout $timeout ." + fi + + if [ -z "$DRY_RUN" ] + then + check_for_config_file + TF_ACC=1 go test -parallel 1 -tags "$tags" -v -timeout $timeout . + fi +} + function multiple_test { filter=$1 if [ -z "$filter" ] @@ -201,6 +221,9 @@ case $wanted in acceptance) acceptance_test functional ;; + sequential-acceptance) + sequential_acceptance_test functional + ;; multinetwork) multiple_test TestAccVcdVappNetworkMulti ;; From cdb710139a471ceae54f1552efdd8632a8710f50 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 08:36:33 +0300 Subject: [PATCH 27/31] Improve runtest.sh --- scripts/runtest.sh | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/scripts/runtest.sh b/scripts/runtest.sh index 03ac97eeb..89a992a74 100755 --- a/scripts/runtest.sh +++ b/scripts/runtest.sh @@ -89,25 +89,6 @@ function short_test { } function acceptance_test { - tags="$1" - if [ -z "$tags" ] - then - tags=functional - fi - if [ -n "$VERBOSE" ] - then - echo "# check for config file" - echo "TF_ACC=1 go test -tags '$tags' -v -timeout $timeout ." - fi - - if [ -z "$DRY_RUN" ] - then - check_for_config_file - TF_ACC=1 go test -tags "$tags" -v -timeout $timeout . - fi -} - -function sequential_acceptance_test { tags="$1" parallel="$2" if [ -z "$tags" ] @@ -123,7 +104,7 @@ function sequential_acceptance_test { if [ -z "$DRY_RUN" ] then check_for_config_file - TF_ACC=1 go test -parallel 1 -tags "$tags" -v -timeout $timeout . + TF_ACC=1 go test -tags "$tags" $parallel -v -timeout $timeout . fi } @@ -222,7 +203,7 @@ case $wanted in acceptance_test functional ;; sequential-acceptance) - sequential_acceptance_test functional + acceptance_test functional "--parallel=1" ;; multinetwork) multiple_test TestAccVcdVappNetworkMulti From 255479b945e232fd33cea18b5dbe5af4507e9f19 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 09:40:48 +0300 Subject: [PATCH 28/31] Add note to testing.md --- TESTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/TESTING.md b/TESTING.md index ac6b45b87..f9629f52f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -6,6 +6,7 @@ - [Running tests](#running-tests) - [Tests split by feature set](#tests-split-by-feature-set) - [Adding new tests](#adding-new-tests) + - [Parallelism considerations](#parallelism-considerations) - [Binary testing](#binary-testing) - [Custom terraform scripts](#custom-terraform-scripts) - [Environment variables](#environment-variables) @@ -279,6 +280,17 @@ testacc: testunit @sh -c "'$(CURDIR)/scripts/runtest.sh' acceptance" ``` +### Parallelism considerations + +When writing Terraform acceptance tests there are two ways to define tests. Either using +`resource.Test` or `resource.ParallelTest`. The former runs tests sequentially one by one while the +later runs all the tests (which are defined to be run in parallel) instantiated this way in +parallel. This is useful because it can speed up total test execution time. However one must be sure +that the tests defined for parallel run are not clashing with each other. + +By default `make testacc` runs acceptance tests with parallelism enabled (for the tests which are +defined with `resource.ParallelTest`). If there is a need to troubleshoot or simply force the tests +to run sequentially - `make seqtestacc` can be used to achieve it. ## Binary testing From ed6b2be0ab6c4297d5055f9cd8a5a90569c8d5b5 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 09:54:54 +0300 Subject: [PATCH 29/31] Add reboot warning for 'terraform plan' --- vcd/resource_vcd_vapp_vm.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 8717dd2ca..a28ca3b6e 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -206,6 +206,8 @@ func resourceVcdVAppVm() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "force": { + ValidateFunc: noopValueWarningValidator(true, + "Using 'true' value for field 'vcd_vapp_vm.customization.force' will reboot VM on every 'terraform apply' operation"), Type: schema.TypeBool, Optional: true, Default: false, @@ -221,6 +223,18 @@ func resourceVcdVAppVm() *schema.Resource { } } +// noopValueWarningValidator is a no-op validator which only emits warning string when fieldValue +// is set to the specified one +func noopValueWarningValidator(fieldValue interface{}, warningText string) schema.SchemaValidateFunc { + return func(i interface{}, k string) (warnings []string, errors []error) { + if fieldValue == i { + warnings = append(warnings, fmt.Sprintf("%s\n\n", warningText)) + } + + return + } +} + func checkEmptyOrSingleIP() schema.SchemaValidateFunc { return func(i interface{}, k string) (s []string, es []error) { v, ok := i.(string) From 3eb1839b079a144e991d6da1cc0ce91ce781faff Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 11:07:00 +0300 Subject: [PATCH 30/31] Tune documentation --- website/docs/r/vapp_vm.html.markdown | 119 +++++++++++++-------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index b1d3b938a..266a77163 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -106,65 +106,6 @@ resource "vcd_vapp_vm" "web2" { ``` - -## Example forced customization workflow - -Step 1 - Setup VM: - -```hcl -resource "vcd_vapp_vm" "web2" { - vapp_name = "${vcd_vapp.web.name}" - name = "web2" - catalog_name = "Boxes" - template_name = "lampstack-1.10.1-ubuntu-10.04" - memory = 2048 - cpus = 1 - - network { - type = "org" - name = "net" - ip = "10.10.104.162" - ip_allocation_mode = "MANUAL" - } -} -``` - -Step 2 - Change VM configuration and force customization (VM will be rebooted during -`terraform apply`): - -```hcl -resource "vcd_vapp_vm" "web2" { -... - network { - type = "org" - name = "net" - ip_allocation_mode = "DHCP" - } - - customization { - force = true - } -} -``` - -Step 3 - Once customization is done, set the force customization flag to false (or remove it) to -prevent forcing customization on every `terraform apply` command: - -```hcl -resource "vcd_vapp_vm" "web2" { -... - network { - type = "org" - name = "net" - ip_allocation_mode = "DHCP" - } - - customization { - force = false - } -} -``` - ## Argument Reference The following arguments are supported: @@ -242,4 +183,62 @@ example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_n This field works as a flag and triggers force customization when `true` during an update (`terraform apply`) every time. It never complains about a change in statefile. Can be used when guest customization is needed after VM configuration (e.g. NIC change, customization options change, etc.) and then set back to `false`. -**Note.** It will not have effect when `power_on` field is set to `false`. See [example workflow above](#example-forced-customization-workflow). +**Note.** It will not have effect when `power_on` field is set to `false`. See [example workflow below](#example-forced-customization-workflow). + +## Example forced customization workflow + +Step 1 - Setup VM: + +```hcl +resource "vcd_vapp_vm" "web2" { + vapp_name = "${vcd_vapp.web.name}" + name = "web2" + catalog_name = "Boxes" + template_name = "lampstack-1.10.1-ubuntu-10.04" + memory = 2048 + cpus = 1 + + network { + type = "org" + name = "net" + ip = "10.10.104.162" + ip_allocation_mode = "MANUAL" + } +} +``` + +Step 2 - Change VM configuration and force customization (VM will be rebooted during +`terraform apply`): + +```hcl +resource "vcd_vapp_vm" "web2" { +... + network { + type = "org" + name = "net" + ip_allocation_mode = "DHCP" + } + + customization { + force = true + } +} +``` + +Step 3 - Once customization is done, set the force customization flag to false (or remove it) to +prevent forcing customization on every `terraform apply` command: + +```hcl +resource "vcd_vapp_vm" "web2" { +... + network { + type = "org" + name = "net" + ip_allocation_mode = "DHCP" + } + + customization { + force = false + } +} +``` From 72b86f9faa3816b6752156cbfad676af94af16c3 Mon Sep 17 00:00:00 2001 From: Dainius S Date: Thu, 22 Aug 2019 12:22:56 +0300 Subject: [PATCH 31/31] Point correct govcd --- go.mod | 4 +--- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c7bb5d2a8..ba78c7c18 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,5 @@ go 1.12 require ( github.com/hashicorp/terraform v0.12.6 - github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.3 + github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.5 ) - -replace github.com/vmware/go-vcloud-director/v2 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190821113230-7fe7758acbc3 diff --git a/go.sum b/go.sum index abd701801..de9645634 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,6 @@ github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxS github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190821113230-7fe7758acbc3 h1:G4VMptgLCvwtMVvGgx2FSjxr9vcWG+WzrsGP+uUeajw= -github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190821113230-7fe7758acbc3/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= @@ -315,6 +313,8 @@ github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.5 h1:bj07gO1M9wxx0GYxj+xgqR3AprvuzqZNLLvFpOJTULQ= +github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.5/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= diff --git a/vendor/modules.txt b/vendor/modules.txt index 3641076c5..90411c773 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -221,7 +221,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.3 => github.com/Didainius/go-vcloud-director/v2 v2.3.2-0.20190821113230-7fe7758acbc3 +# github.com/vmware/go-vcloud-director/v2 v2.4.0-alpha.5 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util