Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vsan file services #1968

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions vsphere/internal/helper/vsanclient/vsan_client_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
}
223 changes: 223 additions & 0 deletions vsphere/resource_vsphere_compute_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like @appilon suggested in you may use schema.TypeList and MaxItems: 1 in order to ensure a single entry.

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(),
},
Expand Down Expand Up @@ -1397,6 +1481,10 @@ func resourceVSphereComputeClusterFlattenData(
}
}

if err := flattenVsanFileServiceConfig(d, cluster, vsanConfig); err != nil {
return err
}

return flattenClusterConfigSpecEx(d, props.ConfigurationEx.(*types.ClusterConfigInfoEx), version)
}

Expand Down Expand Up @@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since thevsan_file_service_domain_conf is optional should not there be something like a null check ?
I also do not see where the d variable is used. If I am not missing something this should be removed from the function arguments.

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 {
Expand Down Expand Up @@ -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)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add log statement with fileServiceOvfUrl

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need of the else statement sine there is a return in the if statement

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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about if the file service is enabled and there is a change ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this comment and the next one, we're planning to combine two fields as former features (fd/sc) did, if the block start to appear it means we're going to enable file service, if the content of block changed, it means reconfigure file service, and if we remove whole block from .tf file, it means disable file service, however in this PR, for the first version of this feature, only basic enable/disable will be supported, since reconfigure fs need new vmodl API, we'll add inline comments accordingly.

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
}

Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is vsan_file_service_conf set if vsanConfig.FileServiceConfig.Enabled equals false ?

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 {
Expand Down
Loading