From 85e22f801db55f11dae4c797ab72a85a02edd97d Mon Sep 17 00:00:00 2001 From: Giuseppe Maxia Date: Tue, 14 May 2024 09:29:18 +0200 Subject: [PATCH] Vm extra configuration (#666) * Add VM methods GetExtraConfig, UpdateExtraConfig, DeleteExtraConfig * Add Changelog entry --------- Signed-off-by: Giuseppe Maxia Co-authored-by: Dainius Serplis --- .changes/v2.25.0/666-features.md | 1 + govcd/vm.go | 111 +++++++++++++++++++++++++++ govcd/vm_test.go | 125 +++++++++++++++++++++++++++++++ types/v56/types.go | 9 +++ types/v56/vm_types.go | 47 ++++++++++++ types/v56/vmmarshal.go | 79 +++++++++++++++++++ 6 files changed, 372 insertions(+) create mode 100644 .changes/v2.25.0/666-features.md create mode 100644 types/v56/vmmarshal.go diff --git a/.changes/v2.25.0/666-features.md b/.changes/v2.25.0/666-features.md new file mode 100644 index 000000000..3b573a924 --- /dev/null +++ b/.changes/v2.25.0/666-features.md @@ -0,0 +1 @@ +* Added `VM` methods `GetExtraConfig`, `UpdateExtraConfig`, `DeleteExtraConfig` to manage VM extra-configuration [GH-666] diff --git a/govcd/vm.go b/govcd/vm.go index 804b392ef..cf4ec478a 100644 --- a/govcd/vm.go +++ b/govcd/vm.go @@ -2105,3 +2105,114 @@ func (vm *VM) ConsolidateDisks() error { } return task.WaitTaskCompletion() } + +// GetExtraConfig retrieves the extra configuration items from a VM +func (vm *VM) GetExtraConfig() ([]*types.ExtraConfigMarshal, error) { + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot update VM spec section, VM HREF is unset") + } + + virtualHardwareSection := &types.ResponseVirtualHardwareSection{} + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodGet, types.MimeVirtualHardwareSection, "error retrieving virtual hardware: %s", nil, virtualHardwareSection) + if err != nil { + return nil, err + } + + convertedExtraConfig := convertExtraConfig(virtualHardwareSection.ExtraConfigs) + + return convertedExtraConfig, nil +} + +// UpdateExtraConfig adds or changes items in the VM Extra Configuration set +// Returns the modified set +// Note: an item with an empty `Value` will be deleted. +func (vm *VM) UpdateExtraConfig(update []*types.ExtraConfigMarshal) ([]*types.ExtraConfigMarshal, error) { + return vm.updateExtraConfig(update, false) +} + +// DeleteExtraConfig removes items from the VM Extra Configuration set +// Returns the modified set +func (vm *VM) DeleteExtraConfig(deleteItems []*types.ExtraConfigMarshal) ([]*types.ExtraConfigMarshal, error) { + return vm.updateExtraConfig(deleteItems, true) +} + +// updateExtraConfig adds, changes, or delete items in the VM Extra Configuration set +func (vm *VM) updateExtraConfig(update []*types.ExtraConfigMarshal, wantDelete bool) ([]*types.ExtraConfigMarshal, error) { + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot update VM spec section, VM HREF is unset") + } + + virtualHardwareSection := &types.ResponseVirtualHardwareSection{} + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodGet, types.MimeVirtualHardwareSection, "error retrieving virtual hardware: %s", nil, virtualHardwareSection) + if err != nil { + return nil, err + } + + var newExtraConfig []*types.ExtraConfigMarshal + + var invalidKeys []string + + if wantDelete { + for _, ec := range update { + newExtraConfig = append(newExtraConfig, &types.ExtraConfigMarshal{Key: ec.Key, Value: ""}) + } + + } else { + for _, ec := range update { + if strings.Contains(ec.Key, " ") { + invalidKeys = append(invalidKeys, ec.Key) + continue + } + newExtraConfig = append(newExtraConfig, ec) + } + if len(invalidKeys) > 0 { + return nil, fmt.Errorf("[vm.UpdateExtraConfig] invalid keys provided: [%s]", strings.Join(invalidKeys, ",")) + } + } + + requestVirtualHardwareSection := &types.RequestVirtualHardwareSection{ + Info: "Virtual hardware requirements", + Ovf: types.XMLNamespaceOVF, + Rasd: types.XMLNamespaceRASD, + Vssd: types.XMLNamespaceVSSD, + Ns4: types.XMLNamespaceVCloud, + Vmw: types.XMLNamespaceVMW, + + Type: virtualHardwareSection.Type, + System: virtualHardwareSection.System, + Item: virtualHardwareSection.Item, + + ExtraConfigs: newExtraConfig, + } + + task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodPut, + types.MimeVirtualHardwareSection, "error updating VM spec section: %s", requestVirtualHardwareSection) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting task: %s", err) + } + + xtraCfg, err := vm.GetExtraConfig() + if err != nil { + return nil, fmt.Errorf("got error while retrieving extra config: %s", err) + } + + return xtraCfg, nil +} + +func convertExtraConfig(source []*types.ExtraConfig) []*types.ExtraConfigMarshal { + resp := make([]*types.ExtraConfigMarshal, len(source)) + for index, field := range source { + resp[index] = &types.ExtraConfigMarshal{ + Key: field.Key, + Value: field.Value, + Required: field.Required, + } + } + + return resp +} diff --git a/govcd/vm_test.go b/govcd/vm_test.go index 23e832f97..0a61b22e8 100644 --- a/govcd/vm_test.go +++ b/govcd/vm_test.go @@ -9,6 +9,7 @@ package govcd import ( "fmt" + "slices" "strings" "time" @@ -2225,3 +2226,127 @@ func (vcd *TestVCD) Test_VmConsolidateDisks(check *C) { err = task.WaitTaskCompletion() check.Assert(err, IsNil) } + +func (vcd *TestVCD) Test_VmExtraConfig(check *C) { + + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipVappTests { + check.Skip("Skipping test because vApp wasn't properly created") + } + + vapp := vcd.findFirstVapp() + if vapp.VApp.Name == "" { + check.Skip("Disabled: No suitable vApp found in vDC") + } + vm, _ := vcd.findFirstVm(vapp) + if vm.Name == "" { + check.Skip("Disabled: No suitable VM found in vDC") + } + + poweredOffVm, err := vcd.client.Client.GetVMByHref(vm.HREF) + check.Assert(err, IsNil) + + newVapp, poweredOnVm := createNsxtVAppAndVm(vcd, check) + + testVmExtraConfig(vcd, "powered OFF VM", poweredOffVm, check, false, false) + testVmExtraConfig(vcd, "formerly powered OFF VM, now powered ON", poweredOffVm, check, true, false) + testVmExtraConfig(vcd, "powered ON VM", poweredOnVm, check, true, false) + testVmExtraConfig(vcd, "formerly powered ON VM, now powered OFF", poweredOnVm, check, false, true) + + // poweredOffVm should be brought back to its original state + task, err := poweredOffVm.PowerOff() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + // Removing the newly created VM and its vApp + task, err = newVapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func testVmExtraConfig(vcd *TestVCD, label string, vm *VM, check *C, wantPowerOn, wantPowerOff bool) { + + fmt.Println(label) + if wantPowerOn { + task, err := vm.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + if wantPowerOff && !wantPowerOn { + task, err := vm.PowerOff() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + printVerbose("vm extra config %# v\n", pretty.Formatter(vm.VM.VirtualHardwareSection.ExtraConfig)) + + configSimilar := types.ExtraConfigMarshal{ + Key: "hpet1.present", + Value: "TRUE", + } + configWithValidKey := types.ExtraConfigMarshal{ + Key: "Norwegian.wood", + Value: "With a little help from my friends", + } + configWithInvalidKey := types.ExtraConfigMarshal{ + Key: "Eleanor Rigby", // invalid key: contains a space + Value: "The long and winding road", + } + + xtraConfig, err := vm.GetExtraConfig() + check.Assert(err, IsNil) + printVerbose("initial values %# v\n", pretty.Formatter(xtraConfig)) + + // Checks that keys containing spaces trigger an error. + invalidUpdatedCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configWithInvalidKey}) + check.Assert(err, NotNil) + check.Assert(invalidUpdatedCfg, IsNil) + check.Assert(strings.Contains(err.Error(), "invalid keys"), Equals, true) + + containsKey := func(items []*types.ExtraConfigMarshal, key string) bool { + return slices.ContainsFunc(items, func(marshal *types.ExtraConfigMarshal) bool { + return marshal.Key == key + }) + } + containsKeyValue := func(items []*types.ExtraConfigMarshal, key, value string) bool { + return slices.ContainsFunc(items, func(marshal *types.ExtraConfigMarshal) bool { + return marshal.Key == key && marshal.Value == value + }) + } + + // Adds two items + updatedCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(updatedCfg, NotNil) + + updatedXtraConfig, err := vm.GetExtraConfig() + check.Assert(err, IsNil) + check.Assert(updatedXtraConfig, NotNil) + printVerbose(" after update %# v\n", pretty.Formatter(updatedXtraConfig)) + + check.Assert(containsKey(updatedXtraConfig, configWithValidKey.Key), Equals, true) + check.Assert(containsKey(updatedXtraConfig, configSimilar.Key), Equals, true) + + // Change the value of an existing key + modifiedValue := "modified value" + configSimilar.Value = modifiedValue + configWithValidKey.Value = modifiedValue + modifiedExtraCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(modifiedExtraCfg, NotNil) + printVerbose(" after modification %# v\n", pretty.Formatter(modifiedExtraCfg)) + check.Assert(containsKeyValue(modifiedExtraCfg, configSimilar.Key, modifiedValue), Equals, true) + check.Assert(containsKeyValue(modifiedExtraCfg, configWithValidKey.Key, modifiedValue), Equals, true) + + // Delete the recently inserted items + afterDeleteXtraConfig, err := vm.DeleteExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(afterDeleteXtraConfig, NotNil) + + printVerbose("after delete %# v\n", pretty.Formatter(afterDeleteXtraConfig)) + + check.Assert(containsKey(afterDeleteXtraConfig, configWithValidKey.Key), Equals, false) + check.Assert(containsKey(afterDeleteXtraConfig, configSimilar.Key), Equals, false) +} diff --git a/types/v56/types.go b/types/v56/types.go index 530daf3a8..7ad3e984c 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -1741,6 +1741,15 @@ type VirtualHardwareSection struct { HREF string `xml:"href,attr,omitempty"` Type string `xml:"type,attr,omitempty"` Item []*VirtualHardwareItem `xml:"Item,omitempty"` + + ExtraConfig []*VmVirtualHardwareSectionExtraConfig `xml:"ExtraConfig,omitempty"` + Link []*Link `xml:"Link,omitempty"` +} + +type VmVirtualHardwareSectionExtraConfig struct { + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` + Required bool `xml:"required,attr"` } // Each ovf:Item parsed from the ovf:VirtualHardwareSection diff --git a/types/v56/vm_types.go b/types/v56/vm_types.go index 2bb1d40f9..be1d18e6f 100644 --- a/types/v56/vm_types.go +++ b/types/v56/vm_types.go @@ -221,3 +221,50 @@ type Adapter struct { Network string `xml:"network,attr"` UnitNumber string `xml:"unitNumber,attr"` } + +// RequestVirtualHardwareSection is used to start a request in VM Extra Configuration set +type RequestVirtualHardwareSection struct { + // Extends OVF Section_Type + XMLName xml.Name `xml:"ovf:VirtualHardwareSection"` + Ovf string `xml:"xmlns:ovf,attr"` + Vssd string `xml:"xmlns:vssd,attr"` + Rasd string `xml:"xmlns:rasd,attr"` + Ns4 string `xml:"xmlns:ns4,attr"` + Vmw string `xml:"xmlns:vmw,attr"` + + Info string `xml:"ovf:Info"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + System []InnerXML `xml:"ovf:System,omitempty"` + Item []InnerXML `xml:"ovf:Item,omitempty"` + + ExtraConfigs []*ExtraConfigMarshal `xml:"vmw:ExtraConfig,omitempty"` +} + +// ResponseVirtualHardwareSection is used to get a response +type ResponseVirtualHardwareSection struct { + // Extends OVF Section_Type + XMLName xml.Name `xml:"VirtualHardwareSection"` + Xmlns string `xml:"vcloud,attr,omitempty"` + Ovf string `xml:"xmlns:ovf,attr"` + Ns4 string `xml:"xmlns:ns4,attr"` + Vssd string `xml:"xmlns:vssd,attr"` + Rasd string `xml:"xmlns:rasd,attr"` + Vmw string `xml:"xmlns:vmw,attr"` + + Info string `xml:"Info"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + + System []InnerXML `xml:"System,omitempty"` + Item []InnerXML `xml:"Item,omitempty"` + + ExtraConfigs []*ExtraConfig `xml:"ExtraConfig,omitempty"` +} + +// ExtraConfig describes an Extra Configuration item +type ExtraConfig struct { + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` + Required bool `xml:"required,attr"` +} diff --git a/types/v56/vmmarshal.go b/types/v56/vmmarshal.go new file mode 100644 index 000000000..32cb18399 --- /dev/null +++ b/types/v56/vmmarshal.go @@ -0,0 +1,79 @@ +package types + +import ( + "encoding/xml" +) + +type ExtraConfigVirtualHardwareSectionMarshal struct { + NS10 string `xml:"xmlns:ns10,attr,omitempty"` + + Info string `xml:"ovf:Info"` + Items []*VirtualHardwareItemMarshal `xml:"ovf:Item,omitempty"` + ExtraConfigs []*ExtraConfigMarshal `xml:"vmw:ExtraConfig,omitempty"` +} +type ExtraConfigMarshal struct { + Key string `xml:"vmw:key,attr"` + Value string `xml:"vmw:value,attr"` + Required bool `xml:"ovf:required,attr"` +} + +type VirtualHardwareItemMarshal struct { + XMLName xml.Name `xml:"ovf:Item"` + Type string `xml:"ns10:type,attr,omitempty"` + Href string `xml:"ns10:href,attr,omitempty"` + + Address *NillableElementMarshal `xml:"rasd:Address"` + AddressOnParent *NillableElementMarshal `xml:"rasd:AddressOnParent"` + AllocationUnits *NillableElementMarshal `xml:"rasd:AllocationUnits"` + AutomaticAllocation *NillableElementMarshal `xml:"rasd:AutomaticAllocation"` + AutomaticDeallocation *NillableElementMarshal `xml:"rasd:AutomaticDeallocation"` + ConfigurationName *NillableElementMarshal `xml:"rasd:ConfigurationName"` + Connection []*VirtualHardwareConnectionMarshal `xml:"rasd:Connection,omitempty"` + ConsumerVisibility *NillableElementMarshal `xml:"rasd:ConsumerVisibility"` + Description *NillableElementMarshal `xml:"rasd:Description"` + ElementName *NillableElementMarshal `xml:"rasd:ElementName,omitempty"` + Generation *NillableElementMarshal `xml:"rasd:Generation"` + HostResource []*VirtualHardwareHostResourceMarshal `xml:"rasd:HostResource,omitempty"` + InstanceID int `xml:"rasd:InstanceID"` + Limit *NillableElementMarshal `xml:"rasd:Limit"` + MappingBehavior *NillableElementMarshal `xml:"rasd:MappingBehavior"` + OtherResourceType *NillableElementMarshal `xml:"rasd:OtherResourceType"` + Parent *NillableElementMarshal `xml:"rasd:Parent"` + PoolID *NillableElementMarshal `xml:"rasd:PoolID"` + Reservation *NillableElementMarshal `xml:"rasd:Reservation"` + ResourceSubType *NillableElementMarshal `xml:"rasd:ResourceSubType"` + ResourceType *NillableElementMarshal `xml:"rasd:ResourceType"` + VirtualQuantity *NillableElementMarshal `xml:"rasd:VirtualQuantity"` + VirtualQuantityUnits *NillableElementMarshal `xml:"rasd:VirtualQuantityUnits"` + Weight *NillableElementMarshal `xml:"rasd:Weight"` + + CoresPerSocket *CoresPerSocketMarshal `xml:"vmw:CoresPerSocket,omitempty"` + Link []*Link `xml:"Link,omitempty"` +} + +type NillableElementMarshal struct { + XmlnsXsi string `xml:"xmlns:xsi,attr,omitempty"` + XsiNil string `xml:"xsi:nil,attr,omitempty"` + Value string `xml:",chardata"` +} + +type CoresPerSocketMarshal struct { + OvfRequired string `xml:"ovf:required,attr,omitempty"` + Value string `xml:",chardata"` +} + +type VirtualHardwareConnectionMarshal struct { + IpAddressingMode string `xml:"ns10:ipAddressingMode,attr,omitempty"` + IPAddress string `xml:"ns10:ipAddress,attr,omitempty"` + PrimaryConnection bool `xml:"ns10:primaryNetworkConnection,attr,omitempty"` + Value string `xml:",chardata"` +} + +type VirtualHardwareHostResourceMarshal struct { + StorageProfile string `xml:"ns10:storageProfileHref,attr,omitempty"` + BusType int `xml:"ns10:busType,attr,omitempty"` + BusSubType string `xml:"ns10:busSubType,attr,omitempty"` + Capacity int `xml:"ns10:capacity,attr,omitempty"` + Iops string `xml:"ns10:iops,attr,omitempty"` + OverrideVmDefault string `xml:"ns10:storageProfileOverrideVmDefault,attr,omitempty"` +}