Skip to content

Commit

Permalink
feat: add support for vlcm (offline software depots, cluster images) (#…
Browse files Browse the repository at this point in the history
…2143)

Signed-off-by: Stoyan Zhelyazkov <[email protected]>

Signed-off-by: Ryan Johnson <[email protected]>

Co-authored-by: Ryan Johnson <[email protected]>
  • Loading branch information
spacegospod and tenthirtyam authored Mar 11, 2024
1 parent 6eec7a9 commit 40e4997
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 0 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## 2.8.0 (Unreleased)

FEATURES:

* `resource/vsphere_offline_software_depot`: Adds resource to the provider for offline software
depots. Support for online depots can be added at a later time. Only depots with source type
"PULL" are supported. This is intentional and aims to discourage the use of the deprecated VUM
functionality. ([#2143](https://github.com/terraform-providers/terraform-provider-vsphere/pull/2143))
* `data/vsphere_host_base_images`: Adds data source to the provider for base images. Declaring this
data source allows users to retrieve the full list of available ESXi versions for their
environment. ([#2143](https://github.com/terraform-providers/terraform-provider-vsphere/pull/2143))
* `resource/vsphere_compute_cluster`: Adds property that serves as an entry point for the vLCM
configuration. Allows selection of a base image and a list of custom components from a depot.
Configuring this property for the first time also enables vLCM on the cluster.
([#2143](https://github.com/terraform-providers/terraform-provider-vsphere/pull/2143))

CHORES:

* `provider`: Updates `vmware/govmomi` to v0.36.0. ([#2149](https://github.com/terraform-providers/terraform-provider-vsphere/pull/2149))
Expand Down
39 changes: 39 additions & 0 deletions vsphere/data_source_vsphere_host_base_images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vsphere

import (
"github.com/vmware/govmomi/vapi/esx/settings/depots"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceVSphereHostBaseImages() *schema.Resource {
return &schema.Resource{
Read: dataSourceVSphereHostBaseImagesRead,
Schema: map[string]*schema.Schema{
"version": {
Type: schema.TypeList,
Computed: true,
Description: "The available ESXi versions.",
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}

func dataSourceVSphereHostBaseImagesRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client).restClient
if images, err := depots.NewManager(client).ListBaseImages(); err != nil {
return err
} else {
versions := make([]string, len(images))
for i, image := range images {
versions[i] = image.Version
}

d.SetId(versions[0])
return d.Set("version", versions)
}
}
8 changes: 8 additions & 0 deletions vsphere/internal/helper/testhelper/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,11 @@ data "vsphere_host" "roothost3" {
}
`, os.Getenv("TF_VSPHERE_VSAN_WITNESS_HOST"))
}

func ConfigDataSoftwareDepot() string {
return fmt.Sprintf(`
resource "vsphere_offline_software_depot" "depot" {
location = "%s"
}
`, os.Getenv("TF_VAR_VSPHERE_SOFTWARE_DEPOT_LOCATION"))
}
2 changes: 2 additions & 0 deletions vsphere/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func Provider() *schema.Provider {
"vsphere_host_port_group": resourceVSphereHostPortGroup(),
"vsphere_host_virtual_switch": resourceVSphereHostVirtualSwitch(),
"vsphere_license": resourceVSphereLicense(),
"vsphere_offline_software_depot": resourceVsphereOfflineSoftwareDepot(),
"vsphere_resource_pool": resourceVSphereResourcePool(),
"vsphere_tag": resourceVSphereTag(),
"vsphere_tag_category": resourceVSphereTagCategory(),
Expand Down Expand Up @@ -158,6 +159,7 @@ func Provider() *schema.Provider {
"vsphere_dynamic": dataSourceVSphereDynamic(),
"vsphere_folder": dataSourceVSphereFolder(),
"vsphere_host": dataSourceVSphereHost(),
"vsphere_host_base_images": dataSourceVSphereHostBaseImages(),
"vsphere_host_pci_device": dataSourceVSphereHostPciDevice(),
"vsphere_host_thumbprint": dataSourceVSphereHostThumbprint(),
"vsphere_host_vgpu_profile": dataSourceVSphereHostVGpuProfile(),
Expand Down
163 changes: 163 additions & 0 deletions vsphere/resource_vsphere_compute_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ package vsphere
import (
"context"
"fmt"
"github.com/vmware/govmomi/vapi/cis/tasks"
"github.com/vmware/govmomi/vapi/esx/settings/clusters"
"github.com/vmware/govmomi/vapi/rest"
"log"
"sort"
"strings"
Expand Down Expand Up @@ -448,6 +451,44 @@ func resourceVSphereComputeCluster() *schema.Resource {
Description: "Advanced configuration options for vSphere HA.",
Elem: &schema.Schema{Type: schema.TypeString},
},
// vLCM
"host_image": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Details about the host image which should be applied to the cluster.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"esx_version": {
Type: schema.TypeString,
Description: "The ESXi version which the image is based on.",
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
"component": {
Type: schema.TypeList,
Description: "List of custom components.",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {
Type: schema.TypeString,
Description: "The identifier for the component.",
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
"version": {
Type: schema.TypeString,
Description: "The version to use.",
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
},
},
},
},
},
},
// Proactive HA
"proactive_ha_enabled": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -691,6 +732,11 @@ func resourceVSphereComputeClusterCreate(d *schema.ResourceData, meta interface{
return err
}

// Apply vLCM settings
if err := resourceVSphereComputeClusterApplyHostImage(d, meta, cluster); err != nil {
return err
}

// All done!
log.Printf("[DEBUG] %s: Create finished successfully", resourceVSphereComputeClusterIDString(d))
return resourceVSphereComputeClusterRead(d, meta)
Expand Down Expand Up @@ -766,6 +812,10 @@ func resourceVSphereComputeClusterUpdate(d *schema.ResourceData, meta interface{
return err
}

if err := resourceVSphereComputeClusterApplyHostImage(d, meta, cluster); err != nil {
return err
}

log.Printf("[DEBUG] %s: Update finished successfully", resourceVSphereComputeClusterIDString(d))
return resourceVSphereComputeClusterRead(d, meta)
}
Expand Down Expand Up @@ -1092,6 +1142,119 @@ func resourceVSphereComputeClusterApplyCustomAttributes(
return attrsProcessor.ProcessDiff(cluster)
}

func resourceVSphereComputeClusterApplyHostImage(
d *schema.ResourceData,
meta interface{},
cluster *object.ClusterComputeResource,
) error {
if !d.HasChange("host_image") {
return nil
}

if d.Get("host_image") == nil {
return fmt.Errorf("disabling vLCM is not allowed")
}

client := meta.(*Client).restClient

m := clusters.NewManager(client)
if vlcmEnabled, err := m.GetSoftwareManagement(d.Id()); err != nil {
return err
} else if !vlcmEnabled.Enabled {
if err := resourceVsphereComputeClusterEnableSoftwareManagement(d, client); err != nil {
return err
}
}

if draftId, err := m.CreateSoftwareDraft(d.Id()); err != nil {
return err
} else {
if err := m.SetSoftwareDraftBaseImage(d.Id(), draftId, d.Get("host_image.0.esx_version").(string)); err != nil {
return err
}

spec := clusters.SoftwareComponentsUpdateSpec{ComponentsToSet: make(map[string]string)}
oldComponents, newComponents := d.GetChange("host_image.0.component")
oldComponentsMap := getComponentsMap(oldComponents.([]interface{}))
newComponentsMap := getComponentsMap(newComponents.([]interface{}))

spec.ComponentsToSet = getComponentsToAdd(oldComponentsMap, newComponentsMap)
componentsToRemove := getComponentsToRemove(oldComponentsMap, newComponentsMap)

if err = m.UpdateSoftwareDraftComponents(d.Id(), draftId, spec); err != nil {
return err
} else if len(componentsToRemove) > 0 {
for _, componentId := range componentsToRemove {
if err := m.RemoveSoftwareDraftComponents(d.Id(), draftId, componentId); err != nil {
return err
}
}
}

if taskId, err := m.CommitSoftwareDraft(d.Id(), draftId, clusters.SettingsClustersSoftwareDraftsCommitSpec{}); err != nil {
return err
} else {
_, err := tasks.NewManager(client).WaitForCompletion(context.Background(), taskId)
return err
}
}
}

func resourceVsphereComputeClusterEnableSoftwareManagement(d *schema.ResourceData, client *rest.Client) error {
m := clusters.NewManager(client)

if draftId, err := m.CreateSoftwareDraft(d.Id()); err != nil {
return err
} else if err := m.SetSoftwareDraftBaseImage(d.Id(), draftId, d.Get("host_image.0.esx_version").(string)); err != nil {
return err
} else if taskId, err := m.CommitSoftwareDraft(d.Id(), draftId, clusters.SettingsClustersSoftwareDraftsCommitSpec{}); err != nil {
return err
} else if _, err := tasks.NewManager(client).WaitForCompletion(context.Background(), taskId); err != nil {
return err
} else if taskId, err := m.EnableSoftwareManagement(d.Id(), false); err != nil {
return err
} else if _, err := tasks.NewManager(client).WaitForCompletion(context.Background(), taskId); err != nil {
return err
} else {
return nil
}
}

func getComponentsToAdd(old, new map[string]interface{}) map[string]string {
result := make(map[string]string)

for k, v := range new {
if _, contains := old[k]; !contains {
version, _ := v.(map[string]interface{})["version"].(string)
result[k] = version
}
}

return result
}

func getComponentsToRemove(old, new map[string]interface{}) []string {
result := make([]string, 0)

for k, _ := range old {
if _, contains := new[k]; !contains {
result = append(result, k)
}
}

return result
}

func getComponentsMap(components []interface{}) map[string]interface{} {
result := make(map[string]interface{})

for _, component := range components {
result[component.(map[string]interface{})["key"].(string)] = component
}

return result
}

// resourceVSphereComputeClusterReadCustomAttributes reads the custom
// attributes for vsphere_compute_cluster.
func resourceVSphereComputeClusterReadCustomAttributes(
Expand Down
63 changes: 63 additions & 0 deletions vsphere/resource_vsphere_compute_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ func TestAccResourceVSphereComputeCluster_drsHAEnabled(t *testing.T) {
})
}

func TestAccResourceVSphereComputeCluster_vlcm(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
RunSweepers()
testAccPreCheck(t)
testAccResourceVSphereComputeClusterPreCheck(t)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccResourceVSphereComputeClusterConfigVlcm(""),
Check: resource.ComposeTestCheckFunc(),
},
{
Config: testAccResourceVSphereComputeClusterConfigVlcm(testAccResourceVSphereComputeClusterImageConfig()),
Check: resource.ComposeTestCheckFunc(),
},
{
Config: testAccResourceVSphereComputeClusterConfigVlcm(""),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}

func TestAccResourceVSphereComputeCluster_vsanDedupEnabled(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
Expand Down Expand Up @@ -1271,6 +1296,44 @@ resource "vsphere_compute_cluster" "compute_cluster" {
)
}

func testAccResourceVSphereComputeClusterConfigVlcm(imageConfig string) string {
return fmt.Sprintf(`
data "vsphere_host_base_images" "base_images" {}
%s
resource "vsphere_compute_cluster" "compute_cluster" {
name = "testacc-compute-cluster"
datacenter_id = "${data.vsphere_datacenter.rootdc1.id}"
host_system_ids = [data.vsphere_host.roothost3.id]
force_evacuate_on_destroy = true
%s
}
`,
testhelper.CombineConfigs(
testhelper.ConfigDataRootDC1(),
testhelper.ConfigDataRootHost2(),
testhelper.ConfigDataRootHost3(),
testhelper.ConfigDataSoftwareDepot(),
),
imageConfig,
)
}

func testAccResourceVSphereComputeClusterImageConfig() string {
return `
host_image {
esx_version = "${data.vsphere_host_base_images.base_images.version.0}"
component {
key = vsphere_offline_software_depot.depot.component.0.key
version = vsphere_offline_software_depot.depot.component.0.version.0
}
}
`
}

func testAccResourceVSphereComputeClusterConfigDRSHABasicExplicitFailoverHost() string {
return fmt.Sprintf(`
%s
Expand Down
Loading

0 comments on commit 40e4997

Please sign in to comment.