From b06de988e4103c48e3d485ee7cbba9479f2623bf Mon Sep 17 00:00:00 2001 From: Kobi Samoray Date: Tue, 9 Apr 2024 11:06:17 +0300 Subject: [PATCH] Add custom groups support to upgrade run resource Add the logic to create and update custom host groups while running the upgrade process. Signed-off-by: Kobi Samoray --- nsxt/resource_nsxt_edge_transport_node.go | 35 +---- nsxt/resource_nsxt_upgrade_run.go | 181 ++++++++++++++++++++-- nsxt/utils.go | 44 ++++++ website/docs/r/upgrade_run.html.markdown | 9 +- 4 files changed, 226 insertions(+), 43 deletions(-) diff --git a/nsxt/resource_nsxt_edge_transport_node.go b/nsxt/resource_nsxt_edge_transport_node.go index 2c6d5df7b..595ded949 100644 --- a/nsxt/resource_nsxt_edge_transport_node.go +++ b/nsxt/resource_nsxt_edge_transport_node.go @@ -303,23 +303,7 @@ func getEdgeNodeSettingsSchema() *schema.Schema { Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "advanced_configuration": { - Type: schema.TypeList, - Optional: true, - Description: "Advanced configuration", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Required: true, - }, - "value": { - Type: schema.TypeString, - Required: true, - }, - }, - }, - }, + "advanced_configuration": getKeyValuePairListSchema(), "allow_ssh_root_login": { Type: schema.TypeBool, Default: false, @@ -861,13 +845,7 @@ func getEdgeNodeSettingsFromSchema(s interface{}) (*model.EdgeNodeSettings, erro settings := s.([]interface{}) for _, settingIf := range settings { setting := settingIf.(map[string]interface{}) - var advCfg []model.KeyValuePair - for _, aci := range setting["advanced_configuration"].([]interface{}) { - ac := aci.(map[string]interface{}) - key := ac["key"].(string) - val := ac["value"].(string) - advCfg = append(advCfg, model.KeyValuePair{Key: &key, Value: &val}) - } + advCfg := getKeyValuePairListFromSchema(setting["advanced_configuration"]) allowSSHRootLogin := setting["allow_ssh_root_login"].(bool) dnsServers := interface2StringList(setting["dns_servers"].([]interface{})) enableSSH := setting["enable_ssh"].(bool) @@ -1260,14 +1238,7 @@ func resourceNsxtEdgeTransportNodeRead(d *schema.ResourceData, m interface{}) er func setEdgeNodeSettingsInSchema(d *schema.ResourceData, nodeSettings *model.EdgeNodeSettings) error { elem := getElemOrEmptyMapFromSchema(d, "node_settings") - var advCfg []map[string]interface{} - for _, kv := range nodeSettings.AdvancedConfiguration { - e := make(map[string]interface{}) - e["key"] = kv.Key - e["value"] = kv.Value - advCfg = append(advCfg, e) - } - elem["advanced_configuration"] = advCfg + elem["advanced_configuration"] = setKeyValueListForSchema(nodeSettings.AdvancedConfiguration) elem["allow_ssh_root_login"] = nodeSettings.AllowSshRootLogin elem["dns_servers"] = nodeSettings.DnsServers elem["enable_ssh"] = nodeSettings.EnableSsh diff --git a/nsxt/resource_nsxt_upgrade_run.go b/nsxt/resource_nsxt_upgrade_run.go index 68da107a1..215d12d9e 100644 --- a/nsxt/resource_nsxt_upgrade_run.go +++ b/nsxt/resource_nsxt_upgrade_run.go @@ -11,13 +11,18 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/vmware/vsphere-automation-sdk-go/lib/vapi/std/errors" + "github.com/vmware/vsphere-automation-sdk-go/runtime/bindings" "github.com/vmware/vsphere-automation-sdk-go/runtime/protocol/client" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp/nsx" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp/nsx/model" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp/nsx/upgrade" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp/nsx/upgrade/plan" + "golang.org/x/exp/slices" ) +const hostUpgradeUnitDefaultGroup = "Group 1 for ESXI" + // Order matters var upgradeComponentList = []string{ edgeUpgradeGroup, @@ -232,7 +237,8 @@ func getUpgradeGroupSchema(isHostGroup bool) *schema.Schema { "id": { Type: schema.TypeString, Description: "ID of upgrade unit group", - Required: true, + Required: !isHostGroup, + Optional: isHostGroup, }, "enabled": { Type: schema.TypeBool, @@ -255,6 +261,11 @@ func getUpgradeGroupSchema(isHostGroup bool) *schema.Schema { } if isHostGroup { + elemSchema["display_name"] = &schema.Schema{ + Type: schema.TypeString, + Description: "Name of upgrade unit group", + Optional: true, + } elemSchema["upgrade_mode"] = &schema.Schema{ Type: schema.TypeString, Description: "Upgrade mode", @@ -279,6 +290,15 @@ func getUpgradeGroupSchema(isHostGroup bool) *schema.Schema { Optional: true, Default: true, } + elemSchema["hosts"] = &schema.Schema{ + Type: schema.TypeList, + Description: "Hosts to be included in the upgrade group", + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + } } return &schema.Schema{ @@ -340,7 +360,7 @@ func upgradeRunCreateOrUpdate(d *schema.ResourceData, m interface{}) error { upgradeClientSet := newUpgradeClientSet(connector, d) log.Printf("[INFO] Updating UpgradeUnitGroup and UpgradePlanSetting.") - err := prepareUpgrade(upgradeClientSet, d) + err := prepareUpgrade(upgradeClientSet, d, m) if err != nil { return handleCreateError("NsxtUpgradeRun", id, err) } @@ -358,7 +378,7 @@ func upgradeRunCreateOrUpdate(d *schema.ResourceData, m interface{}) error { return resourceNsxtUpgradeRunRead(d, m) } -func prepareUpgrade(upgradeClientSet *upgradeClientSet, d *schema.ResourceData) error { +func prepareUpgrade(upgradeClientSet *upgradeClientSet, d *schema.ResourceData, m interface{}) error { for i := range upgradeComponentList { component := upgradeComponentList[i] // Customize MP upgrade is not allowed @@ -395,7 +415,7 @@ func prepareUpgrade(upgradeClientSet *upgradeClientSet, d *schema.ResourceData) return err } - err = updateUpgradeUnitGroups(upgradeClientSet, d, component) + err = updateUpgradeUnitGroups(upgradeClientSet, d, m, component) if err != nil { return err } @@ -490,7 +510,7 @@ func waitUpgradeForStatus(upgradeClientSet *upgradeClientSet, component *string, return nil } -func updateUpgradeUnitGroups(upgradeClientSet *upgradeClientSet, d *schema.ResourceData, component string) error { +func updateUpgradeUnitGroups(upgradeClientSet *upgradeClientSet, d *schema.ResourceData, m interface{}, component string) error { isBefore := false getReorderAfterReq := func(id string) model.ReorderRequest { return model.ReorderRequest{ @@ -503,9 +523,76 @@ func updateUpgradeUnitGroups(upgradeClientSet *upgradeClientSet, d *schema.Resou for _, groupI := range d.Get(componentToGroupKey[component]).([]interface{}) { group := groupI.(map[string]interface{}) groupID := group["id"].(string) - groupGet, err := upgradeClientSet.GroupClient.Get(groupID, nil) - if err != nil { - return err + var groupGet *model.UpgradeUnitGroup + var err error + isCreate := false + if groupID == "" { + groupName := group["display_name"].(string) + if groupName == "" { + return fmt.Errorf("couldn't find upgrade unit group without id or display_name") + } + // This is a custom group, try to find it by name + groupList, err := upgradeClientSet.GroupClient.List(nil, nil, nil, nil, nil, nil, nil, nil) + if err != nil { + return err + } + for i, group := range groupList.Results { + if *group.DisplayName == groupName { + if groupGet == nil { + groupID = *group.Id + groupGet = &groupList.Results[i] + } else { + return fmt.Errorf("upgrade group name %s is not unique", groupName) + } + } + } + if groupGet == nil { + // This is a new custom group, create an upgrade unit list + isCreate = true + var groupMembers []model.UpgradeUnit + if group["hosts"] != nil { + for _, h := range group["hosts"].([]interface{}) { + hostID := h.(string) + groupMembers = append(groupMembers, model.UpgradeUnit{Id: &hostID}) + } + } + typeHost := "HOST" + groupGet = &model.UpgradeUnitGroup{DisplayName: &groupName, UpgradeUnits: groupMembers, Type_: &typeHost} + } else { + // This custom group might be updated, compare the upgrade unit lists + nsxUnits := getUnitIDsFromUnits(groupGet.UpgradeUnits) + + // Find and remove upgrade units which have been removed from the list. This is done by assigning the + // upgrade unit to its default group + var schemaUnits []string + if group["hosts"] != nil { + schemaUnits = interface2StringList(group["hosts"].([]interface{})) + } + for _, nsxUnit := range nsxUnits { + if !slices.Contains(schemaUnits, nsxUnit) { + groupID, err := getHostDefaultUpgradeGroup(m, nsxUnit) + if isNotFoundError(err) { + return fmt.Errorf("couldn't find default group for host %s as default group was not found", nsxUnit) + } else if err != nil { + return handleDeleteError("Host Upgrade Group Binding", nsxUnit, err) + } + return addHostToGroup(m, groupID, nsxUnit, false) + } + } + // Replace the upgrade unit list + var groupMembers []model.UpgradeUnit + for _, h := range group["hosts"].([]interface{}) { + hostID := h.(string) + groupMembers = append(groupMembers, model.UpgradeUnit{Id: &hostID}) + } + groupGet.UpgradeUnits = groupMembers + } + } else { + group, err := upgradeClientSet.GroupClient.Get(groupID, nil) + if err != nil { + return err + } + groupGet = &group } enabled := group["enabled"].(bool) @@ -551,11 +638,14 @@ func updateUpgradeUnitGroups(upgradeClientSet *upgradeClientSet, d *schema.Resou groupGet.ExtendedConfiguration = extendConfig } - _, err = upgradeClientSet.GroupClient.Update(groupID, groupGet) + if isCreate { + _, err = upgradeClientSet.GroupClient.Create(*groupGet) + } else { + _, err = upgradeClientSet.GroupClient.Update(groupID, *groupGet) + } if err != nil { return err } - if preUpgradeGroupID != "" { err = upgradeClientSet.GroupClient.Reorder(groupID, getReorderAfterReq(preUpgradeGroupID)) if err != nil { @@ -722,3 +812,74 @@ func resourceNsxtUpgradeRunUpdate(d *schema.ResourceData, m interface{}) error { func resourceNsxtUpgradeRunDelete(d *schema.ResourceData, m interface{}) error { return nil } + +func getHostDefaultUpgradeGroup(m interface{}, hostID string) (string, error) { + connector := getPolicyConnector(m) + hostClient := nsx.NewTransportNodesClient(connector) + host, err := hostClient.Get(hostID) + if err != nil { + return "", err + } + converter := bindings.NewTypeConverter() + base, errs := converter.ConvertToGolang(host.NodeDeploymentInfo, model.HostNodeBindingType()) + if errs != nil { + return "", errs[0] + } + node := base.(model.HostNode) + + if node.ComputeCollectionId != nil { + return *node.ComputeCollectionId, nil + } + + // This host is not a part of a compute cluster: + // it should be assigned to the 'Group 1 for ESXI' group (this value is hardcoded in NSX) + groupClient := upgrade.NewUpgradeUnitGroupsClient(connector) + componentType := "HOST" + hostGroups, err := groupClient.List(&componentType, nil, nil, nil, nil, nil, nil, nil) + if err != nil { + return "", err + } + if hostGroups.Results != nil { + for _, group := range hostGroups.Results { + if group.DisplayName != nil && *group.DisplayName == hostUpgradeUnitDefaultGroup { + return *group.Id, nil + } + } + } + + return "", errors.NotFound{} +} + +func addHostToGroup(m interface{}, groupID, hostID string, isCreate bool) error { + connector := getPolicyConnector(m) + client := upgrade.NewUpgradeUnitGroupsClient(connector) + + doUpdate := func() error { + group, err := client.Get(groupID, nil) + if err != nil { + return err + } + + hostIDs := getUnitIDsFromUnits(group.UpgradeUnits) + if slices.Contains(hostIDs, hostID) { + return fmt.Errorf("host %s already exists in group %s", hostID, groupID) + } + group.UpgradeUnits = append(group.UpgradeUnits, model.UpgradeUnit{Id: &hostID}) + _, err = client.Update(groupID, group) + if err != nil { + return err + } + return nil + } + commonProviderConfig := getCommonProviderConfig(m) + return retryUponPreconditionFailed(doUpdate, commonProviderConfig.MaxRetries) +} + +func getUnitIDsFromUnits(units []model.UpgradeUnit) []string { + var unitIDs []string + + for _, unit := range units { + unitIDs = append(unitIDs, *unit.Id) + } + return unitIDs +} diff --git a/nsxt/utils.go b/nsxt/utils.go index 677b947ea..148ef42e3 100644 --- a/nsxt/utils.go +++ b/nsxt/utils.go @@ -819,3 +819,47 @@ func getGMTagsFromSchema(d *schema.ResourceData) []gm_model.Tag { func setGMTagsInSchema(d *schema.ResourceData, tags []gm_model.Tag) { setCustomizedGMTagsInSchema(d, tags, "tag") } + +func getKeyValuePairListSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Advanced configuration", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + } +} + +func getKeyValuePairListFromSchema(kvIList interface{}) []mp_model.KeyValuePair { + var kvList []mp_model.KeyValuePair + if kvIList != nil { + for _, kv := range kvIList.([]interface{}) { + kvMap := kv.(map[string]interface{}) + key := kvMap["key"].(string) + val := kvMap["value"].(string) + kvList = append(kvList, mp_model.KeyValuePair{Key: &key, Value: &val}) + } + } + return kvList +} + +func setKeyValueListForSchema(kvList []mp_model.KeyValuePair) interface{} { + var kvIList []interface{} + for _, ec := range kvList { + kvMap := make(map[string]interface{}) + kvMap["key"] = ec.Key + kvMap["value"] = ec.Value + kvIList = append(kvIList, kvMap) + } + return kvIList +} diff --git a/website/docs/r/upgrade_run.html.markdown b/website/docs/r/upgrade_run.html.markdown index 9b4eec992..a308055c4 100644 --- a/website/docs/r/upgrade_run.html.markdown +++ b/website/docs/r/upgrade_run.html.markdown @@ -42,6 +42,11 @@ resource "nsxt_upgrade_run" "run1" { id = data.nsxt_host_upgrade_group.hg1.id parallel = true } + host_group { + display_name = "TEST123" + parallel = false + hosts = ["2fa96cdc-6b82-4284-a69a-18a21a6b6d0c"] + } edge_upgrade_setting { parallel = true @@ -66,7 +71,8 @@ The following arguments are supported: * `parallel` - (Optional) Upgrade method to specify whether upgrades of UpgradeUnits in this group are performed in parallel or serially. Default: True. * `pause_after_each_upgrade_unit` - (Optional) Flag to indicate whether upgrade should be paused after upgrade of each upgrade-unit. Default: False. * `host_group` - (Optional) HOST component upgrade unit group configurations. Groups will be reordered following the order they present in this field. - * `id` - (Required) ID of the upgrade unit group. + * `id` - (Optional) ID of the upgrade unit group. Should exist only for predefined groups. When creating a custom host group, the value is assigned by NSX. + * `display_name` - (Optional) The display name of the host group. Should be assigned only for custom host groups and must be unique. * `enabled` - (Optional) Flag to indicate whether upgrade of this group is enabled or not. Default: True. * `parallel` - (Optional) Upgrade method to specify whether upgrades of UpgradeUnits in this group are performed in parallel or serially. Default: True. * `pause_after_each_upgrade_unit` - (Optional) Flag to indicate whether upgrade should be paused after upgrade of each upgrade-unit. Default: False. @@ -74,6 +80,7 @@ The following arguments are supported: * `maintenance_mode_config_vsan_mode` - (Optional) Maintenance mode config of vsan mode. Supported values: `evacuate_all_data`, `ensure_object_accessibility`, `no_action`. * `maintenance_mode_config_evacuate_powered_off_vms` - (Optional) Maintenance mode config of whether evacuate powered off vms. * `rebootless_upgrade` - (Optional) Flag to indicate whether to use rebootless upgrade. Default: True. + * `hosts` - (Optional) The list of hosts to be associated with a custom group. * `edge_upgrade_setting` - (Optional) EDGE component upgrade plan setting. * `parallel` - (Optional) Upgrade Method to specify whether upgrades of UpgradeUnitGroups in this component are performed serially or in parallel. Default: True. * `post_upgrade_check` - (Optional) Flag to indicate whether run post upgrade check after upgrade. Default: True.