From 8370be86d2c774f1cd21cfd6372bd0dff63ca982 Mon Sep 17 00:00:00 2001 From: Nikos Fotiou Date: Mon, 12 Feb 2024 20:29:46 +0200 Subject: [PATCH] feat: add disk stats for datastore (#1896) - `data/vsphere_datastore_stats` Add datastore stats to report total capacity and free space of datastores. - `data/vsphere_datastore` Add stats for a single datastore. Signed-off-by: nikfot --- vsphere/data_source_vsphere_datastore.go | 10 +++ .../data_source_vsphere_datastore_stats.go | 71 +++++++++++++++ ...ata_source_vsphere_datastore_stats_test.go | 86 +++++++++++++++++++ vsphere/data_source_vsphere_datastore_test.go | 42 +++++++++ vsphere/provider.go | 1 + website/docs/d/datastore.html.markdown | 14 +-- website/docs/d/datastore_stats.html .markdown | 80 +++++++++++++++++ 7 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 vsphere/data_source_vsphere_datastore_stats.go create mode 100644 vsphere/data_source_vsphere_datastore_stats_test.go create mode 100644 website/docs/d/datastore_stats.html .markdown diff --git a/vsphere/data_source_vsphere_datastore.go b/vsphere/data_source_vsphere_datastore.go index 173276ccd..b425d0451 100644 --- a/vsphere/data_source_vsphere_datastore.go +++ b/vsphere/data_source_vsphere_datastore.go @@ -26,6 +26,11 @@ func dataSourceVSphereDatastore() *schema.Resource { Description: "The managed object ID of the datacenter the datastore is in. This is not required when using ESXi directly, or if there is only one datacenter in your infrastructure.", Optional: true, }, + "stats": { + Type: schema.TypeMap, + Description: "The usage stats of the datastore, include total capacity and free space in bytes.", + Optional: true, + }, }, } } @@ -48,5 +53,10 @@ func dataSourceVSphereDatastoreRead(d *schema.ResourceData, meta interface{}) er } d.SetId(ds.Reference().Value) + props, err := datastore.Properties(ds) + if err != nil { + return fmt.Errorf("error getting properties for datastore ID %q: %s", ds.Reference().Value, err) + } + d.Set("stats", map[string]string{"capacity": fmt.Sprintf("%v", props.Summary.Capacity), "free": fmt.Sprintf("%v", props.Summary.FreeSpace)}) return nil } diff --git a/vsphere/data_source_vsphere_datastore_stats.go b/vsphere/data_source_vsphere_datastore_stats.go new file mode 100644 index 000000000..fc153a8d3 --- /dev/null +++ b/vsphere/data_source_vsphere_datastore_stats.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vsphere + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/datastore" + "github.com/vmware/govmomi/object" +) + +func dataSourceVSphereDatastoreStats() *schema.Resource { + return &schema.Resource{ + Read: dataSourceVSphereDatastoreStatsRead, + + Schema: map[string]*schema.Schema{ + "datacenter_id": { + Type: schema.TypeString, + Description: "The managed object ID of the datacenter to get datastores from.", + Required: true, + }, + "free_space": { + Type: schema.TypeMap, + Description: "The free space of the datastores.", + Optional: true, + }, + "capacity": { + Type: schema.TypeMap, + Description: "The capacity of the datastores.", + Optional: true, + }, + }, + } +} + +func dataSourceVSphereDatastoreStatsRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + var dc *object.Datacenter + if dcID, ok := d.GetOk("datacenter_id"); ok { + var err error + dc, err = datacenterFromID(client, dcID.(string)) + if err != nil { + return fmt.Errorf("cannot locate datacenter: %s", err) + } + } + dss, err := datastore.List(client) + if err != nil { + return fmt.Errorf("error listing datastores: %s", err) + } + for i := range dss { + ds, err := datastore.FromPath(client, dss[i].Name(), dc) + if err != nil { + return fmt.Errorf("error fetching datastore: %s", err) + } + props, err := datastore.Properties(ds) + if err != nil { + return fmt.Errorf("error getting properties for datastore ID %q: %s", ds.Reference().Value, err) + } + cap := d.Get("capacity").(map[string]interface{}) + cap[dss[i].Name()] = fmt.Sprintf("%v", props.Summary.Capacity) + d.Set("capacity", cap) + fr := d.Get("free_space").(map[string]interface{}) + fr[dss[i].Name()] = fmt.Sprintf("%v", props.Summary.FreeSpace) + d.Set("free_space", fr) + } + d.SetId(fmt.Sprintf("%s_stats", dc.Reference().Value)) + + return nil +} diff --git a/vsphere/data_source_vsphere_datastore_stats_test.go b/vsphere/data_source_vsphere_datastore_stats_test.go new file mode 100644 index 000000000..1214c1da3 --- /dev/null +++ b/vsphere/data_source_vsphere_datastore_stats_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vsphere + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceVSphereDatastoreStats_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccDataSourceVSphereDatastoreStatsPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceVSphereDatastoreStatsConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.vsphere_datastore_stats.datastore_stats", "datacenter_id", os.Getenv("VSPHERE_DATACENTER"), + ), + resource.TestCheckResourceAttr( + "data.vsphere_datastore_stats.datastore_stats", "id", fmt.Sprintf("%s_stats", os.Getenv("VSPHERE_DATACENTER")), + ), + testCheckOutputBool("found_free_space", "true"), + testCheckOutputBool("found_capacity", "true"), + testCheckOutputBool("free_values_exist", "true"), + testCheckOutputBool("capacity_values_exist", "true"), + ), + }, + }, + }) +} + +func testAccDataSourceVSphereDatastoreStatsPreCheck(t *testing.T) { + if os.Getenv("VSPHERE_DATACENTER") == "" { + t.Skip("set TF_VAR_VSPHERE_DATACENTER to run vsphere_datastore_stats acceptance tests") + } + if os.Getenv("VSPHERE_USER") == "" { + t.Skip("set TF_VAR_VSPHERE_DATACENTER to run vsphere_datastore_stats acceptance tests") + } + if os.Getenv("VSPHERE_PASSWORD") == "" { + t.Skip("set TF_VAR_VSPHERE_PASSWORD to run vsphere_datastore_stats acceptance tests") + } +} + +func testAccDataSourceVSphereDatastoreStatsConfig() string { + return fmt.Sprintf(` +variable "datacenter_id" { + default = "%s" +} + +data "vsphere_datastore_stats" "datastore_stats" { + datacenter_id = "${var.datacenter_id}" +} + +output "found_free_space" { + value = "${length(data.vsphere_datastore_stats.datastore_stats.free_space) >= 1 ? "true" : "false" }" +} + +output "found_capacity" { + value = "${length(data.vsphere_datastore_stats.datastore_stats.capacity) >= 1 ? "true" : "false" }" +} + +output "free_values_exist" { + value = alltrue([ + for free in values(data.vsphere_datastore_stats.datastore_stats.free_space): + free >= 1 + ]) +} + +output "capacity_values_exist" { + value = alltrue([ + for free in values(data.vsphere_datastore_stats.datastore_stats.capacity): + free >= 1 + ]) +} +`, os.Getenv("VSPHERE_DATACENTER")) +} diff --git a/vsphere/data_source_vsphere_datastore_test.go b/vsphere/data_source_vsphere_datastore_test.go index 95642ba08..8481f711f 100644 --- a/vsphere/data_source_vsphere_datastore_test.go +++ b/vsphere/data_source_vsphere_datastore_test.go @@ -57,6 +57,28 @@ func TestAccDataSourceVSphereDatastore_noDatacenterAndAbsolutePath(t *testing.T) }) } +func TestAccDataSourceVSphereDatastore_getStats(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceVSphereDatastoreConfigGetStats(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.vsphere_datastore.datastore_data", "name", os.Getenv("VSPHERE_DATASTORE_NAME"), + ), + + testCheckOutputBool("found_stats", "true"), + ), + }, + }, + }) +} + func testAccDataSourceVSphereDatastorePreCheck(t *testing.T) { if os.Getenv("TF_VAR_VSPHERE_NFS_DS_NAME") == "" { t.Skip("set TF_VAR_VSPHERE_NFS_DS_NAME to run vsphere_nas_datastore acceptance tests") @@ -90,3 +112,23 @@ data "vsphere_datastore" "datastore_data" { testhelper.CombineConfigs(testhelper.ConfigDataRootDC1(), testhelper.ConfigDataRootHost1(), testhelper.ConfigDataRootHost2(), testhelper.ConfigResDS1(), testhelper.ConfigDataRootComputeCluster1(), testhelper.ConfigResResourcePool1(), testhelper.ConfigDataRootPortGroup1()), ) } + +func testAccDataSourceVSphereDatastoreConfigGetStats() string { + return fmt.Sprintf(` +variable "datastore_name" { + default = "%s" +} +variable "datacenter_id" { + default = "%s" +} +data "vsphere_datastore" "datastore_data" { + name = "${var.datastore_name}" + datacenter_id = "${var.datacenter_id}" +} + +output "found_stats" { + value = "${length(data.vsphere_datastore.datastore_data.stats) >= 1 ? "true" : "false" }" +} +`, os.Getenv("VSPHERE_DATASTORE_NAME"), os.Getenv("VSPHERE_DATACENTER"), + ) +} diff --git a/vsphere/provider.go b/vsphere/provider.go index 246051b6f..0bf0c365c 100644 --- a/vsphere/provider.go +++ b/vsphere/provider.go @@ -153,6 +153,7 @@ func Provider() *schema.Provider { "vsphere_datacenter": dataSourceVSphereDatacenter(), "vsphere_datastore": dataSourceVSphereDatastore(), "vsphere_datastore_cluster": dataSourceVSphereDatastoreCluster(), + "vsphere_datastore_stats": dataSourceVSphereDatastoreStats(), "vsphere_distributed_virtual_switch": dataSourceVSphereDistributedVirtualSwitch(), "vsphere_dynamic": dataSourceVSphereDynamic(), "vsphere_folder": dataSourceVSphereFolder(), diff --git a/website/docs/d/datastore.html.markdown b/website/docs/d/datastore.html.markdown index 37f336dd0..cd016bd21 100644 --- a/website/docs/d/datastore.html.markdown +++ b/website/docs/d/datastore.html.markdown @@ -7,7 +7,7 @@ description: |- Provides a data source to return the ID of a vSphere datastore object. --- -# vsphere\_datastore +# vsphere_datastore The `vsphere_datastore` data source can be used to discover the ID of a vSphere datastore object. This can then be used with resources or data sources @@ -33,8 +33,8 @@ data "vsphere_datastore" "datastore" { The following arguments are supported: -* `name` - (Required) The name of the datastore. This can be a name or path. -* `datacenter_id` - (Optional) The [managed object reference ID][docs-about-morefs] +- `name` - (Required) The name of the datastore. This can be a name or path. +- `datacenter_id` - (Optional) The [managed object reference ID][docs-about-morefs] of the datacenter the datastore is located in. This can be omitted if the search path used in `name` is an absolute path. For default datacenters, use the `id` attribute from an empty `vsphere_datacenter` data source. @@ -43,5 +43,9 @@ The following arguments are supported: ## Attribute Reference -The only exported attribute from this data source is `id`, which represents the -ID of the datastore. +The following attributes are exported: + +- `id` - The ID of the datastore. +- `stats` - The disk space usage statistics for the specific datastore. The total + datastore capacity is represented as `capacity` and the free remaining disk is + represented as `free`. diff --git a/website/docs/d/datastore_stats.html .markdown b/website/docs/d/datastore_stats.html .markdown new file mode 100644 index 000000000..d3b72f826 --- /dev/null +++ b/website/docs/d/datastore_stats.html .markdown @@ -0,0 +1,80 @@ +--- +subcategory: "Storage" +layout: "vsphere" +page_title: "VMware vSphere: vsphere_datastore_stats" +sidebar_current: "docs-vsphere-data-source-datastore-stats" +description: |- + Provides a data source to return the usage stats for all vSphere datastore objects + in a datacenter. +--- + +# vsphere_datastore_stats + +The `vsphere_datastore_stats` data source can be used to retrieve the usage stats +of all vSphere datastore objects in a datacenter. This can then be used as a +standalone datasource to get information required as input to other data sources. + +## Example Usage + +```hcl +data "vsphere_datacenter" "datacenter" { + name = "dc-01" +} + +data "vsphere_datastore_stats" "datastore_stats" { + datacenter_id = data.vsphere_datacenter.datacenter.id +} +``` + +A usefull example of this datasource would be to determine the +datastore with the most free space. For example, in addition to +the above: + +Create an `outputs.tf` like that: + +```hcl +output "max_free_space_name" { + value = local.max_free_space_name +} + +output "max_free_space" { + value = local.max_free_space +} +``` + +and a `locals.tf` like that: + +```hcl +locals { + free_space_values = { for k, v in data.vsphere_datastore_stats.datastore_stats.free_space : k => tonumber(v) } + filtered_values = { for k, v in local.free_space_values : k => tonumber(v) if v != null } + numeric_values = [for v in values(local.filtered_values) : tonumber(v)] + max_free_space = max(local.numeric_values...) + max_free_space_name = [for k, v in local.filtered_values : k if v == local.max_free_space][0] +} +``` + +This way you can get the storage object with the most +space available. + +## Argument Reference + +The following arguments are supported: + +- `datacenter_id` - (Required) The [managed object reference ID][docs-about-morefs] + of the datacenter the datastores are located in. For default datacenters, use + the `id` attribute from an empty `vsphere_datacenter` data source. + +[docs-about-morefs]: /docs/providers/vsphere/index.html#use-of-managed-object-references-by-the-vsphere-provider + +## Attribute Reference + +The following attributes are exported: + +- `datacenter_id` - The [managed object reference ID][docs-about-morefs] + of the datacenter the datastores are located in. +- `free_space` - A mapping of the free space for each datastore in the + datacenter, where the name of the datastore is used as key and the free + space as value. +- `capacity` - A mapping of the capacity for all datastore in the datacenter + , where the name of the datastore is used as key and the capacity as value.