diff --git a/CHANGELOG.md b/CHANGELOG.md index f88833b49..228164c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ * Added methods `AdminOrg.GetVDCByName` and related `GetVDCById`, `GetVDCByNameOrId` * Added methods `AdminOrg.GetAdminVDCByName` and related `GetAdminVDCById`, `GetAdminVDCByNameOrId` * Added methods `Catalog.Refresh` and `AdminCatalog.Refresh` +* Added method `vm.GetVirtualHardwareSection` to retrieve virtual hardware items [#200](https://github.com/vmware/go-vcloud-director/pull/200) +* Added methods `vm.SetProductSectionList` and `vm.GetProductSectionList` allowing to manipulate VM +guest properties [#235](https://github.com/vmware/go-vcloud-director/pull/235) +* Added methods `vapp.SetProductSectionList` and `vapp.GetProductSectionList` allowing to manipulate +vApp guest properties [#235](https://github.com/vmware/go-vcloud-director/pull/235) IMPROVEMENTS: diff --git a/go.mod b/go.mod index 63d608087..1c75c93e1 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/vmware/go-vcloud-director/v2 require ( github.com/hashicorp/go-version v1.1.0 - github.com/kr/pretty v0.1.0 + github.com/kr/pretty v0.1.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index af2c6a600..91cfce57f 100644 --- a/go.sum +++ b/go.sum @@ -5,7 +5,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/vmware/go-vcloud-director v2.0.0+incompatible h1:3B121XZVdEOxRhv5ARswKVxXt4KznAbun8GoXNbbZWs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/govcd/lb_test.go b/govcd/lb_test.go index bb25b560f..54ca2396a 100644 --- a/govcd/lb_test.go +++ b/govcd/lb_test.go @@ -274,7 +274,7 @@ func checkLb(queryUrl string, expectedResponses []string, maxRetryTimeout int) e for { select { case <-timeoutAfter: - return fmt.Errorf("timed out waiting for all nodes to be up: %s", err) + return fmt.Errorf("timed out waiting for all nodes to be up") case <-tick.C: var resp *http.Response resp, err = httpClient.Get(queryUrl) diff --git a/govcd/productsection.go b/govcd/productsection.go new file mode 100644 index 000000000..b6233089c --- /dev/null +++ b/govcd/productsection.go @@ -0,0 +1,53 @@ +/* + * 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" +) + +// setProductSectionList is a shared function for both vApp and VM +func setProductSectionList(client *Client, href string, productSection *types.ProductSectionList) error { + if href == "" { + return fmt.Errorf("href cannot be empty to set product section") + } + + productSection.Xmlns = types.XMLNamespaceVCloud + productSection.Ovf = types.XMLNamespaceOVF + + task, err := client.ExecuteTaskRequest(href+"/productSections", http.MethodPut, + types.MimeProductSection, "error setting product section: %s", productSection) + + if err != nil { + return fmt.Errorf("unable to set product section: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("task for setting product section failed: %s", err) + } + + return nil +} + +// getProductSectionList is a shared function for both vApp and VM +func getProductSectionList(client *Client, href string) (*types.ProductSectionList, error) { + if href == "" { + return nil, fmt.Errorf("href cannot be empty to get product section") + } + productSection := &types.ProductSectionList{} + + _, err := client.ExecuteRequest(href+"/productSections", http.MethodGet, + types.MimeProductSection, "error retrieving product section : %s", nil, productSection) + + if err != nil { + return nil, fmt.Errorf("unable to retrieve product section: %s", err) + } + + return productSection, nil +} diff --git a/govcd/vapp.go b/govcd/vapp.go index 521563c9d..61e831be2 100644 --- a/govcd/vapp.go +++ b/govcd/vapp.go @@ -537,6 +537,9 @@ func (vapp *VApp) ChangeVMName(name string) (Task, error) { types.MimeVM, "error changing VM name: %s", newName) } +// SetOvf sets guest properties for the first child VM in vApp +// +// Deprecated: Use vm.SetProductSectionList() func (vapp *VApp) SetOvf(parameters map[string]string) (Task, error) { err := vapp.Refresh() if err != nil { @@ -830,7 +833,28 @@ func updateNetworkConfigurations(vapp *VApp, networkConfigurations []types.VAppN types.MimeNetworkConfigSection, "error updating vApp Network: %s", networkConfig) } -// Function RemoveAllNetworks unattach all networks from VAPP +// RemoveAllNetworks detaches all networks from vApp func (vapp *VApp) RemoveAllNetworks() (Task, error) { return updateNetworkConfigurations(vapp, []types.VAppNetworkConfiguration{}) } + +// SetProductSectionList sets product section for a vApp. It allows to change vApp guest properties. +// +// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered +// or returned as set before +func (vapp *VApp) SetProductSectionList(productSection *types.ProductSectionList) (*types.ProductSectionList, error) { + err := setProductSectionList(vapp.client, vapp.VApp.HREF, productSection) + if err != nil { + return nil, fmt.Errorf("unable to set vApp product section: %s", err) + } + + return vapp.GetProductSectionList() +} + +// GetProductSectionList retrieves product section for a vApp. It allows to read vApp guest properties. +// +// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered +// or returned as set before +func (vapp *VApp) GetProductSectionList() (*types.ProductSectionList, error) { + return getProductSectionList(vapp.client, vapp.VApp.HREF) +} diff --git a/govcd/vapp_test.go b/govcd/vapp_test.go index b6921cdf6..67fe0c677 100644 --- a/govcd/vapp_test.go +++ b/govcd/vapp_test.go @@ -680,3 +680,13 @@ func (vcd *TestVCD) Test_RemoveAllNetworks(check *C) { } check.Assert(hasNetworks, Equals, false) } + +// Test_VappSetProductSectionList sets vApp product section, retrieves it and deeply matches if +// properties were properly set using a propertyTester helper. +func (vcd *TestVCD) Test_VappSetProductSectionList(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + vapp := vcd.findFirstVapp() + propertyTester(vcd, check, &vapp) +} diff --git a/govcd/vapp_vm_test.go b/govcd/vapp_vm_test.go new file mode 100644 index 000000000..3800f9d7a --- /dev/null +++ b/govcd/vapp_vm_test.go @@ -0,0 +1,94 @@ +// +build vapp vm functional ALL + +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// guestPropertyGetSetter interface is used for covering tests in both VM and vApp guest property +type productSectionListGetSetter interface { + GetProductSectionList() (*types.ProductSectionList, error) + SetProductSectionList(productSection *types.ProductSectionList) (*types.ProductSectionList, error) +} + +// propertyTester is a guest property setter accepting guestPropertyGetSetter interface for trying +// out settings on all objects implementing such interface +func propertyTester(vcd *TestVCD, check *C, object productSectionListGetSetter) { + productSection := &types.ProductSectionList{ + ProductSection: &types.ProductSection{ + Info: "Custom properties", + Property: []*types.Property{ + &types.Property{ + UserConfigurable: false, + Key: "sys_owner", + Label: "sys_owner_label", + Type: "string", + DefaultValue: "sys_owner_default", + Value: &types.Value{Value: "test"}, + }, + &types.Property{ + UserConfigurable: true, + Key: "asset_tag", + Label: "asset_tag_label", + Type: "string", + DefaultValue: "asset_tag_default", + Value: &types.Value{Value: "xxxyyy"}, + }, + &types.Property{ + UserConfigurable: true, + Key: "guestinfo.config.bootstrap.ip", + Label: "guestinfo.config.bootstrap.ip_label", + Type: "string", + DefaultValue: "default_ip", + Value: &types.Value{Value: "192.168.12.180"}, + }, + }, + }, + } + + productSection.SortByPropertyKeyName() + + gotproductSection, err := object.SetProductSectionList(productSection) + check.Assert(err, IsNil) + gotproductSection.SortByPropertyKeyName() + + getproductSection, err := object.GetProductSectionList() + check.Assert(err, IsNil) + getproductSection.SortByPropertyKeyName() + + // Check that values were set in API + check.Assert(getproductSection, NotNil) + check.Assert(getproductSection.ProductSection, NotNil) + check.Assert(len(getproductSection.ProductSection.Property), Equals, 3) + + check.Assert(getproductSection.ProductSection.Property[0].Key, Equals, "asset_tag") + check.Assert(getproductSection.ProductSection.Property[0].Label, Equals, "asset_tag_label") + check.Assert(getproductSection.ProductSection.Property[0].Type, Equals, "string") + check.Assert(getproductSection.ProductSection.Property[0].Value.Value, Equals, "xxxyyy") + check.Assert(getproductSection.ProductSection.Property[0].DefaultValue, Equals, "asset_tag_default") + check.Assert(getproductSection.ProductSection.Property[0].UserConfigurable, Equals, true) + + check.Assert(getproductSection.ProductSection.Property[1].Key, Equals, "guestinfo.config.bootstrap.ip") + check.Assert(getproductSection.ProductSection.Property[1].Label, Equals, "guestinfo.config.bootstrap.ip_label") + check.Assert(getproductSection.ProductSection.Property[1].Type, Equals, "string") + check.Assert(getproductSection.ProductSection.Property[1].Value.Value, Equals, "192.168.12.180") + check.Assert(getproductSection.ProductSection.Property[1].DefaultValue, Equals, "default_ip") + check.Assert(getproductSection.ProductSection.Property[1].UserConfigurable, Equals, true) + + check.Assert(getproductSection.ProductSection.Property[2].Key, Equals, "sys_owner") + check.Assert(getproductSection.ProductSection.Property[2].Label, Equals, "sys_owner_label") + check.Assert(getproductSection.ProductSection.Property[2].Type, Equals, "string") + check.Assert(getproductSection.ProductSection.Property[2].Value.Value, Equals, "test") + check.Assert(getproductSection.ProductSection.Property[2].DefaultValue, Equals, "sys_owner_default") + check.Assert(getproductSection.ProductSection.Property[2].UserConfigurable, Equals, false) + + // Ensure the object are deeply equal + check.Assert(gotproductSection.ProductSection.Property, DeepEquals, productSection.ProductSection.Property) + check.Assert(getproductSection, DeepEquals, gotproductSection) +} diff --git a/govcd/vm.go b/govcd/vm.go index 28a8b334f..6b90fc7ab 100644 --- a/govcd/vm.go +++ b/govcd/vm.go @@ -777,3 +777,24 @@ func (vm *VM) ToggleHardwareVirtualization(isEnabled bool) (Task, error) { return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, "", errMessage, nil) } + +// SetProductSectionList sets product section for a VM. It allows to change VM guest properties. +// +// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered +// or returned as set before +func (vm *VM) SetProductSectionList(productSection *types.ProductSectionList) (*types.ProductSectionList, error) { + err := setProductSectionList(vm.client, vm.VM.HREF, productSection) + if err != nil { + return nil, fmt.Errorf("unable to set VM product section: %s", err) + } + + return vm.GetProductSectionList() +} + +// GetProductSectionList retrieves product section for a VM. It allows to read VM guest properties. +// +// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered +// or returned as set before +func (vm *VM) GetProductSectionList() (*types.ProductSectionList, error) { + return getProductSectionList(vm.client, vm.VM.HREF) +} diff --git a/govcd/vm_test.go b/govcd/vm_test.go index d0bba2345..096bb5075 100644 --- a/govcd/vm_test.go +++ b/govcd/vm_test.go @@ -973,6 +973,22 @@ func (vcd *TestVCD) Test_BlockWhileGuestCustomizationStatus(check *C) { check.Assert(err, IsNil) } +// Test_VMSetProductSectionList sets product section, retrieves it and deeply matches if properties +// were properly set using a propertyTester helper. +func (vcd *TestVCD) Test_VMSetProductSectionList(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + vapp := vcd.findFirstVapp() + vmType, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + vm, err := vcd.client.Client.FindVMByHREF(vmType.HREF) + check.Assert(err, IsNil) + propertyTester(vcd, check, &vm) +} + // Test gathering VM virtual hardware items func (vcd *TestVCD) Test_GetVirtualHardwareSection(check *C) { itemName := "TestGetVirtualHardwareSection" diff --git a/govcd/vm_unit_test.go b/govcd/vm_unit_test.go index 88f457e95..502649bf8 100644 --- a/govcd/vm_unit_test.go +++ b/govcd/vm_unit_test.go @@ -7,6 +7,7 @@ package govcd import ( + "reflect" "testing" "github.com/vmware/go-vcloud-director/v2/types/v56" @@ -270,3 +271,104 @@ func Test_VMupdateNicParameters_singleNIC(t *testing.T) { } } + +// TestProductSectionList_SortByPropertyKeyName validates that a +// SortByPropertyKeyName() works on ProductSectionList and can handle empty properties as well as +// sort correctly +func TestProductSectionList_SortByPropertyKeyName(t *testing.T) { + sliceProductSection := &types.ProductSectionList{ + ProductSection: &types.ProductSection{}, + } + + emptyProductSection := &types.ProductSectionList{ + ProductSection: &types.ProductSection{ + Info: "Custom properties", + }, + } + + // unordered list for test + sortOrder := &types.ProductSectionList{ + ProductSection: &types.ProductSection{ + Info: "Custom properties", + Property: []*types.Property{ + &types.Property{ + UserConfigurable: false, + Key: "sys_owner", + Label: "sys_owner_label", + Type: "string", + DefaultValue: "sys_owner_default", + Value: &types.Value{Value: "test"}, + }, + &types.Property{ + UserConfigurable: true, + Key: "asset_tag", + Label: "asset_tag_label", + Type: "string", + DefaultValue: "asset_tag_default", + Value: &types.Value{Value: "xxxyyy"}, + }, + &types.Property{ + UserConfigurable: true, + Key: "guestinfo.config.bootstrap.ip", + Label: "guestinfo.config.bootstrap.ip_label", + Type: "string", + DefaultValue: "default_ip", + Value: &types.Value{Value: "192.168.12.180"}, + }, + }, + }, + } + // correct state after ordering + expectedSortedOrder := &types.ProductSectionList{ + ProductSection: &types.ProductSection{ + Info: "Custom properties", + Property: []*types.Property{ + &types.Property{ + UserConfigurable: true, + Key: "asset_tag", + Label: "asset_tag_label", + Type: "string", + DefaultValue: "asset_tag_default", + Value: &types.Value{Value: "xxxyyy"}, + }, + &types.Property{ + UserConfigurable: true, + Key: "guestinfo.config.bootstrap.ip", + Label: "guestinfo.config.bootstrap.ip_label", + Type: "string", + DefaultValue: "default_ip", + Value: &types.Value{Value: "192.168.12.180"}, + }, + &types.Property{ + UserConfigurable: false, + Key: "sys_owner", + Label: "sys_owner_label", + Type: "string", + DefaultValue: "sys_owner_default", + Value: &types.Value{Value: "test"}, + }, + }, + }, + } + + tests := []struct { + name string + setValue *types.ProductSectionList + expectedValue *types.ProductSectionList + }{ + {name: "Empty", setValue: emptyProductSection, expectedValue: emptyProductSection}, + {name: "Slice", setValue: sliceProductSection, expectedValue: sliceProductSection}, + {name: "SortOrder", setValue: sortOrder, expectedValue: expectedSortedOrder}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := tt.setValue + p.SortByPropertyKeyName() + + if !reflect.DeepEqual(p, tt.expectedValue) { + t.Errorf("Objects were not deeply equal: \n%#+v\n, got:\n %#+v\n", tt.expectedValue, p) + } + + }) + } +} diff --git a/types/v56/types.go b/types/v56/types.go index e5488240c..3daa509df 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -7,6 +7,7 @@ package types import ( "encoding/xml" "fmt" + "sort" ) // Maps status Attribute Values for VAppTemplate, VApp, Vm, and Media Objects @@ -1172,6 +1173,15 @@ type ProductSectionList struct { ProductSection *ProductSection `xml:"http://schemas.dmtf.org/ovf/envelope/1 ProductSection,omitempty"` } +// SortByPropertyKeyName allows to sort ProductSectionList property slice by key name as the API is +// does not always return an ordered slice +func (p *ProductSectionList) SortByPropertyKeyName() { + sort.SliceStable(p.ProductSection.Property, func(i, j int) bool { + return p.ProductSection.Property[i].Key < p.ProductSection.Property[j].Key + }) + return +} + type ProductSection struct { Info string `xml:"Info,omitempty"` Property []*Property `xml:"http://schemas.dmtf.org/ovf/envelope/1 Property,omitempty"`