From b63ef815f7a2f78b9f71d3357ad43214ae7555bc Mon Sep 17 00:00:00 2001 From: zxinyu08 Date: Tue, 1 Aug 2023 21:32:12 +0800 Subject: [PATCH] feat: add vSAN File Service support --- .../helper/vsanclient/vsan_client_helper.go | 62 +++++ vsphere/resource_vsphere_compute_cluster.go | 223 ++++++++++++++++++ .../resource_vsphere_compute_cluster_test.go | 50 ++++ website/docs/r/compute_cluster.html.markdown | 41 ++++ 4 files changed, 376 insertions(+) diff --git a/vsphere/internal/helper/vsanclient/vsan_client_helper.go b/vsphere/internal/helper/vsanclient/vsan_client_helper.go index a2f792e54..64c756c7e 100644 --- a/vsphere/internal/helper/vsanclient/vsan_client_helper.go +++ b/vsphere/internal/helper/vsanclient/vsan_client_helper.go @@ -7,11 +7,19 @@ import ( "context" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/provider" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/object" vimtypes "github.com/vmware/govmomi/vim25/types" "github.com/vmware/govmomi/vsan" + "github.com/vmware/govmomi/vsan/methods" vsantypes "github.com/vmware/govmomi/vsan/types" ) +var VsanClusterFileServiceSystemInstance = vimtypes.ManagedObjectReference{ + Type: "VsanFileServiceSystem", + Value: "vsan-cluster-file-service-system", +} + func Reconfigure(vsanClient *vsan.Client, cluster vimtypes.ManagedObjectReference, spec vsantypes.VimVsanReconfigSpec) error { ctx := context.TODO() @@ -30,3 +38,57 @@ func GetVsanConfig(vsanClient *vsan.Client, cluster vimtypes.ManagedObjectRefere return vsanConfig, err } + +func FindOvfDownloadUrl(vsanClient *vsan.Client, cluster vimtypes.ManagedObjectReference) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + + req := vsantypes.VsanFindOvfDownloadUrl{ + This: VsanClusterFileServiceSystemInstance, + Cluster: cluster.Reference(), + } + + res, err := methods.VsanFindOvfDownloadUrl(ctx, vsanClient, &req) + if err != nil { + return "", err + } + + return res.Returnval, err +} + +func DownloadFileServiceOvf(vsanClient *vsan.Client, client *govmomi.Client, fileServiceOvfUrl string) error { + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + + req := vsantypes.VsanDownloadFileServiceOvf{ + This: VsanClusterFileServiceSystemInstance, + DownloadUrl: fileServiceOvfUrl, + } + + res, err := methods.VsanDownloadFileServiceOvf(ctx, vsanClient, &req) + if err != nil { + return err + } + + task := object.NewTask(client.Client, res.Returnval) + return task.Wait(ctx) +} + +func CreateFileServiceDomain(vsanClient *vsan.Client, client *govmomi.Client, domainConfig vsantypes.VsanFileServiceDomainConfig, cluster vimtypes.ManagedObjectReference) error { + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + + req := vsantypes.VsanClusterCreateFsDomain{ + This: VsanClusterFileServiceSystemInstance, + DomainConfig: domainConfig, + Cluster: &cluster, + } + + res, err := methods.VsanClusterCreateFsDomain(ctx, vsanClient, &req) + if err != nil { + return err + } + + task := object.NewTask(client.Client, res.Returnval) + return task.Wait(ctx) +} diff --git a/vsphere/resource_vsphere_compute_cluster.go b/vsphere/resource_vsphere_compute_cluster.go index 77b822ad8..e79b94636 100644 --- a/vsphere/resource_vsphere_compute_cluster.go +++ b/vsphere/resource_vsphere_compute_cluster.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/datastore" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/network" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/provider" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/vsanclient" @@ -577,6 +578,89 @@ func resourceVSphereComputeCluster() *schema.Resource { }, }, }, + "vsan_file_service_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether the vSAN file service is enabled for the cluster.", + }, + "vsan_file_service_conf": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Description: "The configuration for vsan file service.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "network": { + Type: schema.TypeString, + Optional: true, + Description: "The network selected for vsan file service.", + }, + "vsan_file_service_domain_conf": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Description: "The domain configuration for vsan file service.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of vsan file service domain.", + }, + "dns_server_addresses": { + Type: schema.TypeSet, + Optional: true, + Description: "The dns server addresses of vsan file service domain.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dns_suffixes": { + Type: schema.TypeSet, + Optional: true, + Description: "The dns suffixes of vsan file service domain.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "gateway": { + Type: schema.TypeString, + Optional: true, + Description: "The gateway of vsan file server ip config.", + }, + "subnet_mask": { + Type: schema.TypeString, + Optional: true, + Description: "The subnet mask of vsan file server ip config.", + }, + "file_server_ip_config": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Description: "The ip config for vsan file server.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Optional: true, + Description: "The ip address of vsan file server ip config.", + }, + "fqdn": { + Type: schema.TypeString, + Optional: true, + Description: "The fqdn of vsan file server ip config.", + }, + "is_primary": { + Type: schema.TypeBool, + Optional: true, + Description: "If it is primary file server ip.", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, vSphereTagAttributeKey: tagsSchema(), customattribute.ConfigKey: customattribute.ConfigSchema(), }, @@ -1397,6 +1481,10 @@ func resourceVSphereComputeClusterFlattenData( } } + if err := flattenVsanFileServiceConfig(d, cluster, vsanConfig); err != nil { + return err + } + return flattenClusterConfigSpecEx(d, props.ConfigurationEx.(*types.ClusterConfigInfoEx), version) } @@ -1457,6 +1545,49 @@ func expandVsanDatastoreConfig(d *schema.ResourceData, meta interface{}) (*vsant return conf, nil } +func expandVsanFileServiceConfig(d *schema.ResourceData, meta interface{}, fileServiceConf map[string]interface{}) (*vsantypes.VsanFileServiceConfig, error) { + vimClient := meta.(*Client).vimClient + networkID := fileServiceConf["network"].(string) + network, err := network.FromID(vimClient, networkID) + if err != nil { + return nil, fmt.Errorf("error locating network ID %q: %s", networkID, err) + } + + return &vsantypes.VsanFileServiceConfig{ + Enabled: d.Get("vsan_file_service_enabled").(bool), + Network: types.NewReference(network.Reference()), + }, nil +} + +func expandVsanFileServiceDomainConfig(d *schema.ResourceData, fileServiceConf map[string]interface{}) (vsantypes.VsanFileServiceDomainConfig, error) { + // TODO: add FS active directory support once govmomi is updated. + domainConf := fileServiceConf["vsan_file_service_domain_conf"].(*schema.Set).List()[0].(map[string]interface{}) + + var serverIpConf []vsantypes.VsanFileServiceIpConfig + for _, ip := range domainConf["file_server_ip_config"].(*schema.Set).List() { + ipConf := ip.(map[string]interface{}) + fqdn, _ := ipConf["fqdn"].(string) + ipAddress, _ := ipConf["ip_address"].(string) + isPrimary, _ := ipConf["is_primary"].(bool) + serverIpConf = append(serverIpConf, vsantypes.VsanFileServiceIpConfig{ + HostIpConfig: types.HostIpConfig{ + IpAddress: ipAddress, + SubnetMask: domainConf["subnet_mask"].(string), + }, + Fqdn: fqdn, + IsPrimary: &isPrimary, + Gateway: domainConf["gateway"].(string), + }) + } + + return vsantypes.VsanFileServiceDomainConfig{ + Name: domainConf["name"].(string), + DnsServerAddresses: structure.SliceInterfacesToStrings(domainConf["dns_server_addresses"].(*schema.Set).List()), + DnsSuffixes: structure.SliceInterfacesToStrings(domainConf["dns_suffixes"].(*schema.Set).List()), + FileServerIpConfig: serverIpConf, + }, nil +} + func resourceVSphereComputeClusterApplyVsanConfig(d *schema.ResourceData, meta interface{}, cluster *object.ClusterComputeResource) error { client, err := resourceVSphereComputeClusterClient(meta) if err != nil { @@ -1518,6 +1649,57 @@ func resourceVSphereComputeClusterApplyVsanConfig(d *schema.ResourceData, meta i } } + // handle file service + if d.Get("vsan_file_service_enabled").(bool) && d.HasChange("vsan_file_service_enabled") { + fileServiceOvfUrl, err := vsanclient.FindOvfDownloadUrl(meta.(*Client).vsanClient, cluster.Reference()) + if err != nil { + return fmt.Errorf("cannot find vsan file service OVF url, err: %s", err) + } + + if err := vsanclient.DownloadFileServiceOvf(meta.(*Client).vsanClient, meta.(*Client).vimClient, fileServiceOvfUrl); err != nil { + return fmt.Errorf("cannot download vsan file service OVF with url: %s", fileServiceOvfUrl) + } else { + log.Printf("[DEBUG] downloaded vsan file service OVF with url: %s", fileServiceOvfUrl) + } + + fileServiceConf := d.Get("vsan_file_service_conf").(*schema.Set).List()[0].(map[string]interface{}) + + fileServiceConfig, err := expandVsanFileServiceConfig(d, meta, fileServiceConf) + if err != nil { + return err + } + if err := vsanclient.Reconfigure(meta.(*Client).vsanClient, cluster.Reference(), vsantypes.VimVsanReconfigSpec{ + Modify: true, + FileServiceConfig: fileServiceConfig, + }); err != nil { + return fmt.Errorf("cannot apply vsan file service on cluster '%s': %s", d.Get("name").(string), err) + } + + if len(fileServiceConf["vsan_file_service_domain_conf"].(*schema.Set).List()) != 0 { + domainConfig, err := expandVsanFileServiceDomainConfig(d, fileServiceConf) + if err != nil { + return err + } + if err := vsanclient.CreateFileServiceDomain(meta.(*Client).vsanClient, meta.(*Client).vimClient, domainConfig, cluster.Reference()); err != nil { + return fmt.Errorf("cannot configure vsan file service domain, err: %s", err) + } else { + log.Printf("[DEBUG] configure vsan file service domain") + } + } + } + + if !d.Get("vsan_file_service_enabled").(bool) && d.HasChange("vsan_file_service_enabled") { + if err := vsanclient.Reconfigure(meta.(*Client).vsanClient, cluster.Reference(), vsantypes.VimVsanReconfigSpec{ + Modify: true, + FileServiceConfig: &vsantypes.VsanFileServiceConfig{ + Enabled: d.Get("vsan_file_service_enabled").(bool), + }, + }); err != nil { + return fmt.Errorf("cannot disable vsan file service on cluster '%s': %s", d.Get("name").(string), err) + } + } + // TODO: reconfigure FS domain + return nil } @@ -1701,6 +1883,47 @@ func flattenVsanDisks(d *schema.ResourceData, cluster *object.ClusterComputeReso return d.Set("vsan_disk_group", diskMap) } +func flattenVsanFileServiceConfig(d *schema.ResourceData, cluster *object.ClusterComputeResource, vsanConfig *vsantypes.VsanConfigInfoEx) error { + if !vsanConfig.FileServiceConfig.Enabled { + return d.Set("vsan_file_service_enabled", vsanConfig.FileServiceConfig.Enabled) + } + + fsDomainConf := []interface{}{} + for _, domainConf := range vsanConfig.FileServiceConfig.Domains { + // handle file server ip config. + fsServerIpConf := []interface{}{} + for _, serverIpConf := range domainConf.FileServerIpConfig { + fsServerIpConf = append(fsServerIpConf, map[string]interface{}{ + "ip_address": serverIpConf.HostIpConfig.IpAddress, + "fqdn": serverIpConf.Fqdn, + "is_primary": serverIpConf.IsPrimary, + }) + } + // TODO: handle active directory server config. + fsDomainConf = append(fsDomainConf, map[string]interface{}{ + "name": domainConf.Name, + "dns_server_addresses": domainConf.DnsServerAddresses, + "dns_suffixes": domainConf.DnsSuffixes, + "gateway": domainConf.FileServerIpConfig[0].Gateway, + "subnet_mask": domainConf.FileServerIpConfig[0].HostIpConfig.SubnetMask, + "file_server_ip_config": fsServerIpConf, + }) + } + + serviceConf := []interface{}{} + serviceConf = append(serviceConf, map[string]interface{}{ + // TODO: add more params configurations, like FileServerMemoryMB, FileServerCPUMhz etc. + "network": vsanConfig.FileServiceConfig.Network.Value, + "vsan_file_service_domain_conf": fsDomainConf, + }) + + if err := d.Set("vsan_file_service_conf", serviceConf); err != nil { + return err + } + + return d.Set("vsan_file_service_enabled", vsanConfig.FileServiceConfig.Enabled) +} + // flattenClusterConfigSpecEx saves a ClusterConfigSpecEx into the supplied // ResourceData. func flattenClusterConfigSpecEx(d *schema.ResourceData, obj *types.ClusterConfigInfoEx, version viapi.VSphereVersion) error { diff --git a/vsphere/resource_vsphere_compute_cluster_test.go b/vsphere/resource_vsphere_compute_cluster_test.go index ff3efc95f..8c6ffd3c9 100644 --- a/vsphere/resource_vsphere_compute_cluster_test.go +++ b/vsphere/resource_vsphere_compute_cluster_test.go @@ -304,6 +304,28 @@ func TestAccResourceVSphereComputeCluster_vsanDITEncryption(t *testing.T) { }) } +func TestAccResourceVSphereComputeCluster_vsanFileServiceEnabled(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereComputeClusterPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereComputeClusterCheckExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereComputeClusterConfigVSANFileServiceEnabled(), + Check: resource.ComposeTestCheckFunc( + testAccResourceVSphereComputeClusterCheckExists(true), + resource.TestCheckResourceAttr("vsphere_compute_cluster.compute_cluster", "vsan_enabled", "true"), + resource.TestCheckResourceAttr("vsphere_compute_cluster.compute_cluster", "vsan_file_service_enabled", "true"), + ), + }, + }, + }) +} + func TestAccResourceVSphereComputeCluster_explicitFailoverHost(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { @@ -982,6 +1004,34 @@ resource "vsphere_compute_cluster" "compute_cluster" { ) } +func testAccResourceVSphereComputeClusterConfigVSANFileServiceEnabled() string { + return fmt.Sprintf(` +%s + +resource "vsphere_compute_cluster" "compute_cluster" { + name = "testacc-compute-cluster" + datacenter_id = data.vsphere_datacenter.rootdc1.id + host_system_ids = [data.vsphere_host.roothost2.id, data.vsphere_host.roothost3.id, data.vsphere_host.roothost4.id] + + vsan_enabled = true + vsan_file_service_enabled = true + vsan_file_service_conf { + network = data.vsphere_network.vmnet.id + } + force_evacuate_on_destroy = true +} + +`, + testhelper.CombineConfigs( + testhelper.ConfigDataRootDC1(), + testhelper.ConfigDataRootHost2(), + testhelper.ConfigDataRootHost3(), + testhelper.ConfigDataRootHost4(), + testhelper.ConfigDataRootVMNet(), + ), + ) +} + func testAccResourceVSphereComputeClusterConfigBasic() string { return fmt.Sprintf(` %s diff --git a/website/docs/r/compute_cluster.html.markdown b/website/docs/r/compute_cluster.html.markdown index a027e6e3b..eedc174f3 100644 --- a/website/docs/r/compute_cluster.html.markdown +++ b/website/docs/r/compute_cluster.html.markdown @@ -489,6 +489,21 @@ details, see the referenced link in the above paragraph. group in the cluster. * `cache` - The canonical name of the disk to use for vSAN cache. * `storage` - An array of disk canonical names for vSAN storage. +* `vsan_file_service_enabled` - (Optional) Enables vSAN file service on the + cluster. `vsan_file_service_conf` should be configured when this is `true`. +* `vsan_file_service_conf` - (Optional) Configurations of vSAN file service. + * `network` - The network selected for vsan file service. + It's mandatory when `vsan_file_service_enabled` is set to `true`. + * `vsan_file_service_domain_conf` - (Optional) Domain configurations of vSAN file service. + * `name` - The name of vSAN file service domain. + * `dns_server_addresses` - The DNS server addresses of vSAN file service domain. + * `dns_suffixes` - The DNS suffixes of vSAN file service domain. + * `gateway` - The gateway of vSAN file server IP config. + * `subnet_mask` - The subnet mask of vSAN file server IP config. + * `file_server_ip_config` - The IP config for vSAN file server. + * `ip_address` - The IP address of vSAN file server IP config. + * `fqdn` - The FQDN of vSAN file server IP config. + * `is_primary` - If it is primary file server IP. ~> **NOTE:** You must disable vSphere HA before you enable vSAN on the cluster. You can enable or re-enable vSphere HA after vSAN is configured. @@ -517,6 +532,32 @@ resource "vsphere_compute_cluster" "compute_cluster" { cache = data.vsphere_vmfs_disks.cache_disks[0] storage = data.vsphere_vmfs_disks.storage_disks } + vsan_file_service_enabled = true + vsan_file_service_conf { + network = data.vsphere_network.network.id + vsan_file_service_domain_conf { + name = "" + dns_server_addresses = ["1.2.3.4"] + dns_suffixes = ["example.com"] + gateway = "192.168.111.1" + subnet_mask = "255.255.255.0" + file_server_ip_config { + fqdn = "h192-168-111-2.example.com" + ip_address = "192.168.111.2" + is_primary = true + } + file_server_ip_config { + fqdn = "h192-168-111-3.example.com" + ip_address = "192.168.111.3" + is_primary = false + } + file_server_ip_config { + fqdn = "h192-168-111-4.example.com" + ip_address = "192.168.111.4" + is_primary = false + } + } + } } ```