diff --git a/docs/data-sources/svm_data_source.md b/docs/data-sources/svm_data_source.md index 9fecff6b..0b0d076e 100644 --- a/docs/data-sources/svm_data_source.md +++ b/docs/data-sources/svm_data_source.md @@ -13,6 +13,7 @@ Retrieves the configuration of SVM ## Supported Platforms * On-prem ONTAP system 9.6 or higher + * `storage_limit` attribute supported with ONTAP system 9.13 or higher * Amazon FSx for NetApp ONTAP ## Example Usage @@ -42,4 +43,5 @@ data "netapp-ontap_svm" "svm" { - `language` (String) Language to use for svm - `max_volumes` (String) Maximum number of volumes that can be created on the svm. Expects an integer or unlimited - `snapshot_policy` (String) The name of the snapshot policy to manage -- `subtype` (String) The subtype for svm to be created \ No newline at end of file +- `subtype` (String) The subtype for svm to be created +- `storage_limit` (Number) Maximum storage permitted on svm, in bytes \ No newline at end of file diff --git a/docs/data-sources/svms_data_source.md b/docs/data-sources/svms_data_source.md index 1116d868..7af52b6c 100644 --- a/docs/data-sources/svms_data_source.md +++ b/docs/data-sources/svms_data_source.md @@ -13,6 +13,7 @@ Retrieves the configuration of SVMs. ## Supported Platforms * On-prem ONTAP system 9.6 or higher + * `storage_limit` attribute supported with ONTAP system 9.13 or higher * Amazon FSx for NetApp ONTAP ## Example Usage @@ -69,3 +70,4 @@ Read-Only: - `max_volumes` (String) Maximum number of volumes that can be created on the svm. Expects an integer or unlimited - `snapshot_policy` (String) The name of the snapshot policy to manage - `subtype` (String) The subtype for svm to be created +- `storage_limit` (Number) Maximum storage permitted on svm, in bytes diff --git a/docs/resources/svm_resource.md b/docs/resources/svm_resource.md index 18017469..0dafde49 100644 --- a/docs/resources/svm_resource.md +++ b/docs/resources/svm_resource.md @@ -21,11 +21,12 @@ Create/Modify/Delete a SVM ## Supported Platforms * On-prem ONTAP system 9.6 or higher + * `storage_limit` attribute supported with ONTAP system 9.13 or higher * Amazon FSx for NetApp ONTAP ## Example Usage -This creates a new SVM called `tfsvm4`. In IPspace `terraformIpspace_newname`, which can have up to 200 volumes which will be cased on aggr2 +This creates a new SVM called `tfsvm4` in IPspace `terraformIpspace_newname`, which can have up to 200 volumes and up to 1 GB storage, which will be cased on aggr2. ```terraform resource "netapp-ontap_svm" "example" { @@ -35,8 +36,11 @@ resource "netapp-ontap_svm" "example" { comment = "test" snapshot_policy = "default-1weekly" language = "en_us.utf_8" - aggregates = ["aggr2"] + aggregates = [ + { name = "aggr2" } + ] max_volumes = "200" + storage_limit = 1073741824 }` ``` @@ -57,6 +61,7 @@ resource "netapp-ontap_svm" "example" { - `max_volumes` (String) Maximum number of volumes that can be created on the svm. Expects an integer or unlimited - `snapshot_policy` (String) The name of the snapshot policy to manage - `subtype` (String) The subtype for svm to be created +- `storage_limit` (Number) Maximum storage permitted on svm, in bytes ### Read-Only diff --git a/examples/resources/netapp-ontap_svm/resource.tf b/examples/resources/netapp-ontap_svm/resource.tf index 100a22f0..b7145088 100644 --- a/examples/resources/netapp-ontap_svm/resource.tf +++ b/examples/resources/netapp-ontap_svm/resource.tf @@ -1,11 +1,15 @@ resource "netapp-ontap_svm" "example" { cx_profile_name = "cluster2" - name = "tfsvm" - ipspace = "test" - comment = "test" + name = "tfsvm" + ipspace = "test" + comment = "test" snapshot_policy = "default-1weekly" //subtype = "dp_destination" language = "en_us.utf_8" - aggregates = ["aggr1", "test"] - max_volumes = "200" + aggregates = [ + { name = "aggr1" }, + { name = "test" }, + ] + max_volumes = "200" + storage_limit = 1073741824 } diff --git a/internal/interfaces/svm.go b/internal/interfaces/svm.go index fe658ebd..5fc39c1b 100644 --- a/internal/interfaces/svm.go +++ b/internal/interfaces/svm.go @@ -24,27 +24,29 @@ type SvmDataModelONTAP struct { // SvmResourceModel describes the resource data model. type SvmResourceModel struct { - Name string `mapstructure:"name,omitempty"` - Ipspace Ipspace `mapstructure:"ipspace"` - SnapshotPolicy SnapshotPolicy `mapstructure:"snapshot_policy,omitempty"` - SubType string `mapstructure:"subtype,omitempty"` + Aggregates []map[string]string `mapstructure:"aggregates"` Comment string `mapstructure:"comment"` + Ipspace Ipspace `mapstructure:"ipspace"` Language string `mapstructure:"language,omitempty"` MaxVolumes string `mapstructure:"max_volumes,omitempty"` - Aggregates []map[string]string `mapstructure:"aggregates"` + Name string `mapstructure:"name,omitempty"` + SnapshotPolicy SnapshotPolicy `mapstructure:"snapshot_policy,omitempty"` + Storage Storage `mapstructure:"storage,omitempty"` + SubType string `mapstructure:"subtype,omitempty"` } // SvmGetDataSourceModel describes the data source model. type SvmGetDataSourceModel struct { - Name string `mapstructure:"name"` - UUID string `mapstructure:"uuid"` - Ipspace Ipspace `mapstructure:"ipspace"` - SnapshotPolicy SnapshotPolicy `mapstructure:"snapshot_policy"` - SubType string `mapstructure:"subtype,omitempty"` + Aggregates []Aggregate `mapstructure:"aggregates,omitempty"` Comment string `mapstructure:"comment,omitempty"` + Ipspace Ipspace `mapstructure:"ipspace"` Language string `mapstructure:"language,omitempty"` - Aggregates []Aggregate `mapstructure:"aggregates,omitempty"` MaxVolumes string `mapstructure:"max_volumes,omitempty"` + Name string `mapstructure:"name"` + SnapshotPolicy SnapshotPolicy `mapstructure:"snapshot_policy"` + Storage Storage `mapstructure:"storage,omitempty"` + SubType string `mapstructure:"subtype,omitempty"` + UUID string `mapstructure:"uuid"` } // Ipspace describes the resource data model. @@ -57,6 +59,11 @@ type SnapshotPolicy struct { Name string `mapstructure:"name,omitempty"` } +// Storage describes the resource data model. +type Storage struct { + Limit int `mapstructure:"limit,omitempty"` +} + // SvmDataSourceFilterModel describes the data source data model for queries. type SvmDataSourceFilterModel struct { Name string `mapstructure:"name"` @@ -122,11 +129,25 @@ func GetSvmByNameIgnoreNotFound(errorHandler *utils.ErrorHandler, r restclient.R } // GetSvmByNameDataSource to get data source svm info -func GetSvmByNameDataSource(errorHandler *utils.ErrorHandler, r restclient.RestClient, name string) (*SvmGetDataSourceModel, error) { +func GetSvmByNameDataSource(errorHandler *utils.ErrorHandler, r restclient.RestClient, name string, version versionModelONTAP) (*SvmGetDataSourceModel, error) { api := "svm/svms" query := r.NewQuery() - query.Fields([]string{"name", "ipspace", "snapshot_policy", "subtype", "comment", "language", "max_volumes", "aggregates"}) + fields := []string{ + "name", + "ipspace", + "snapshot_policy", + "subtype", + "comment", + "language", + "max_volumes", + "aggregates", + } + if version.Generation >= 9 && version.Major >= 13 { + fields = append(fields, "storage.limit") + } + query.Fields(fields) query.Add("name", name) + statusCode, response, err := r.GetNilOrOneRecord(api, query, nil) if err == nil && response == nil { err = fmt.Errorf("no response for GET %s", api) @@ -144,10 +165,23 @@ func GetSvmByNameDataSource(errorHandler *utils.ErrorHandler, r restclient.RestC } // GetSvmsByName to get data source list svm info -func GetSvmsByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, filter *SvmDataSourceFilterModel) ([]SvmGetDataSourceModel, error) { +func GetSvmsByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, filter *SvmDataSourceFilterModel, version versionModelONTAP) ([]SvmGetDataSourceModel, error) { api := "svm/svms" query := r.NewQuery() - query.Fields([]string{"name", "ipspace", "snapshot_policy", "subtype", "comment", "language", "max_volumes", "aggregates"}) + fields := []string{ + "name", + "ipspace", + "snapshot_policy", + "subtype", + "comment", + "language", + "max_volumes", + "aggregates", + } + if version.Generation >= 9 && version.Major >= 13 { + fields = append(fields, "storage.limit") + } + query.Fields(fields) if filter != nil { var filterMap map[string]interface{} @@ -179,7 +213,7 @@ func GetSvmsByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, fi } // CreateSvm to create svm -func CreateSvm(errorHandler *utils.ErrorHandler, r restclient.RestClient, data SvmResourceModel, setAggrEmpty bool, setCommentEmpty bool) (*SvmGetDataModelONTAP, error) { +func CreateSvm(errorHandler *utils.ErrorHandler, r restclient.RestClient, data SvmResourceModel, setAggrEmpty bool, setCommentEmpty bool, setStorageLimitEmpty bool) (*SvmGetDataModelONTAP, error) { var body map[string]interface{} if err := mapstructure.Decode(data, &body); err != nil { return nil, errorHandler.MakeAndReportError("error encoding svm body", fmt.Sprintf("error on encoding svm/svms body: %s, body: %#v", err, data)) @@ -190,6 +224,13 @@ func CreateSvm(errorHandler *utils.ErrorHandler, r restclient.RestClient, data S if setCommentEmpty { delete(body, "comment") } + if setStorageLimitEmpty { + // delete storage.limit from request body, so that ONTAP uses default value + if v, ok := body["storage"].(map[string]interface{}); ok { + delete(v, "limit") + } + } + query := r.NewQuery() query.Add("return_records", "true") statusCode, response, err := r.CallCreateMethod("svm/svms", query, body) diff --git a/internal/provider/svm/svm_data_source.go b/internal/provider/svm/svm_data_source.go index 9302d7d1..85a02d31 100644 --- a/internal/provider/svm/svm_data_source.go +++ b/internal/provider/svm/svm_data_source.go @@ -51,6 +51,7 @@ type SvmDataSourceModel struct { Language types.String `tfsdk:"language"` Aggregates []types.String `tfsdk:"aggregates"` MaxVolumes types.String `tfsdk:"max_volumes"` + StorageLimit types.Int64 `tfsdk:"storage_limit"` ID types.String `tfsdk:"id"` } @@ -108,6 +109,10 @@ func (d *SvmDataSource) Schema(ctx context.Context, req datasource.SchemaRequest MarkdownDescription: "Maximum number of volumes that can be created on the svm. Expects an integer or unlimited", Computed: true, }, + "storage_limit": schema.Int64Attribute{ + MarkdownDescription: "Maximum storage permitted on svm, in bytes", + Computed: true, + }, "id": schema.StringAttribute{ Computed: true, }, @@ -150,7 +155,17 @@ func (d *SvmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re return } - restInfo, err := interfaces.GetSvmByNameDataSource(errorHandler, *client, data.Name.ValueString()) + cluster, err := interfaces.GetCluster(errorHandler, *client) + if err != nil { + // error reporting done inside GetCluster + return + } + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "cluster not found") + return + } + + restInfo, err := interfaces.GetSvmByNameDataSource(errorHandler, *client, data.Name.ValueString(), cluster.Version) if err != nil { // error reporting done inside GetSvm return @@ -170,6 +185,7 @@ func (d *SvmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re data.Language = types.StringValue(restInfo.Language) data.Aggregates = aggregates data.MaxVolumes = types.StringValue(restInfo.MaxVolumes) + data.StorageLimit = types.Int64Value(int64(restInfo.Storage.Limit)) // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log diff --git a/internal/provider/svm/svm_resource.go b/internal/provider/svm/svm_resource.go index f651431e..1f44e440 100644 --- a/internal/provider/svm/svm_resource.go +++ b/internal/provider/svm/svm_resource.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -56,6 +57,7 @@ type SvmResourceModel struct { Language types.String `tfsdk:"language"` Aggregates []Aggregate `tfsdk:"aggregates"` MaxVolumes types.String `tfsdk:"max_volumes"` + StorageLimit types.Int64 `tfsdk:"storage_limit"` ID types.String `tfsdk:"id"` } @@ -120,6 +122,12 @@ func (r *SvmResource) Schema(ctx context.Context, req resource.SchemaRequest, re MarkdownDescription: "Maximum number of volumes that can be created on the svm. Expects an integer or unlimited", Optional: true, }, + "storage_limit": schema.Int64Attribute{ + MarkdownDescription: "Maximum storage permitted on svm, in bytes", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + }, "id": schema.StringAttribute{ Computed: true, MarkdownDescription: "SVM identifier", @@ -191,6 +199,12 @@ func (r *SvmResource) Create(ctx context.Context, req resource.CreateRequest, re request.MaxVolumes = data.MaxVolumes.ValueString() } + setStorageLimitEmpty := true + if !data.StorageLimit.Equal(types.Int64Value(0)) { + setStorageLimitEmpty = false + request.Storage.Limit = int(data.StorageLimit.ValueInt64()) + } + setAggrEmpty := false if len(data.Aggregates) != 0 { aggregates := []interfaces.Aggregate{} @@ -214,7 +228,14 @@ func (r *SvmResource) Create(ctx context.Context, req resource.CreateRequest, re // error reporting done inside NewClient return } - svm, err := interfaces.CreateSvm(errorHandler, *client, request, setAggrEmpty, setCommentEmpty) + svm, err := interfaces.CreateSvm( + errorHandler, + *client, + request, + setAggrEmpty, + setCommentEmpty, + setStorageLimitEmpty, + ) if err != nil { return } @@ -244,12 +265,23 @@ func (r *SvmResource) Read(ctx context.Context, req resource.ReadRequest, resp * // error reporting done inside NewClient return } + + cluster, err := interfaces.GetCluster(errorHandler, *client) + if err != nil { + // error reporting done inside GetCluster + return + } + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "cluster not found") + return + } + tflog.Debug(ctx, fmt.Sprintf("read a svm resource: %#v", data)) var svm *interfaces.SvmGetDataSourceModel if data.ID.ValueString() != "" { svm, err = interfaces.GetSvm(errorHandler, *client, data.ID.ValueString()) } else { - svm, err = interfaces.GetSvmByNameDataSource(errorHandler, *client, data.Name.ValueString()) + svm, err = interfaces.GetSvmByNameDataSource(errorHandler, *client, data.Name.ValueString(), cluster.Version) } if err != nil { return @@ -295,6 +327,8 @@ func (r *SvmResource) Read(ctx context.Context, req resource.ReadRequest, resp * data.MaxVolumes = types.StringValue(svm.MaxVolumes) } + data.StorageLimit = types.Int64Value(int64(svm.Storage.Limit)) + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -375,6 +409,10 @@ func (r *SvmResource) Update(ctx context.Context, req resource.UpdateRequest, re request.MaxVolumes = data.MaxVolumes.ValueString() } + if !data.StorageLimit.Equal(state.StorageLimit) { + request.Storage.Limit = int(data.StorageLimit.ValueInt64()) + } + // aggregates can be modified as empty list aggregates := []interfaces.Aggregate{} if len(data.Aggregates) != 0 { diff --git a/internal/provider/svm/svm_resource_test.go b/internal/provider/svm/svm_resource_test.go index 820d3d4a..232d0fbf 100644 --- a/internal/provider/svm/svm_resource_test.go +++ b/internal/provider/svm/svm_resource_test.go @@ -17,7 +17,7 @@ func TestAccSvmResource(t *testing.T) { ProtoV6ProviderFactories: ntest.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSvmResourceConfig("tfsvm4", "test", "default"), + Config: testAccSvmResourceConfig("tfsvm4", "test", "default", 0), Check: resource.ComposeTestCheckFunc( // Check to see the svm name is correct, resource.TestCheckResourceAttr("netapp-ontap_svm.example", "name", "tfsvm4"), @@ -25,25 +25,28 @@ func TestAccSvmResource(t *testing.T) { resource.TestCheckResourceAttr("netapp-ontap_svm.example", "ipspace", "Default"), // Check that a ID has been set (we don't know what the vaule is as it changes resource.TestCheckResourceAttrSet("netapp-ontap_svm.example", "id"), - resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", "test")), + resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", "test"), + // Check to see if storage_limit is set correctly + resource.TestCheckResourceAttr("netapp-ontap_svm.example", "storage_limit", "0"), + ), }, // Update a comment { - Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", "default"), + Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", "default", 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", "carchi8py was here"), resource.TestCheckResourceAttr("netapp-ontap_svm.example", "name", "tfsvm4")), }, // Update a comment with an empty string { - Config: testAccSvmResourceConfig("tfsvm4", "", "default"), + Config: testAccSvmResourceConfig("tfsvm4", "", "default", 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", ""), resource.TestCheckResourceAttr("netapp-ontap_svm.example", "name", "tfsvm4")), }, // Update snapshot policy default-1weekly and comment "carchi8py was here" { - Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", "default-1weekly"), + Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", "default-1weekly", 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", "carchi8py was here"), resource.TestCheckResourceAttr("netapp-ontap_svm.example", "snapshot_policy", "default-1weekly"), @@ -51,19 +54,19 @@ func TestAccSvmResource(t *testing.T) { }, // Update snapshot policy with empty string { - Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", ""), + Config: testAccSvmResourceConfig("tfsvm4", "carchi8py was here", "", 0), ExpectError: regexp.MustCompile("cannot be updated with empty string"), }, // change SVM name { - Config: testAccSvmResourceConfig("tfsvm3", "carchi8py was here", "default"), + Config: testAccSvmResourceConfig("tfsvm3", "carchi8py was here", "default", 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netapp-ontap_svm.example", "comment", "carchi8py was here"), resource.TestCheckResourceAttr("netapp-ontap_svm.example", "name", "tfsvm3")), }, // Fail if the name already exist { - Config: testAccSvmResourceConfig("terraform", "carchi8py was here", "default"), + Config: testAccSvmResourceConfig("terraform", "carchi8py was here", "default", 0), ExpectError: regexp.MustCompile("13434908"), }, // Import and read @@ -75,10 +78,22 @@ func TestAccSvmResource(t *testing.T) { resource.TestCheckResourceAttr("netapp-ontap_svm.example", "name", "terraform"), ), }, + // Update storage_limit + { + Config: testAccSvmResourceConfig("tfsvm3", "carchi8py was here", "default", (1024 * 1024 * 1024)), // 1GB + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_svm.example", "storage_limit", "1073741824"), + ), + }, + // Fail if storage_limit too low + { + Config: testAccSvmResourceConfig("tfsvm3", "carchi8py was here", "default", (1024 * 1024)), // 1MB + ExpectError: regexp.MustCompile("13434880"), + }, }, }) } -func testAccSvmResourceConfig(svm, comment string, snapshotPolicy string) string { +func testAccSvmResourceConfig(svm, comment, snapshotPolicy string, storageLimit int) string { host := os.Getenv("TF_ACC_NETAPP_HOST5") admin := os.Getenv("TF_ACC_NETAPP_USER") password := os.Getenv("TF_ACC_NETAPP_PASS2") @@ -113,5 +128,6 @@ resource "netapp-ontap_svm" "example" { }, ] max_volumes = "unlimited" -}`, host, admin, password, svm, comment, snapshotPolicy) + storage_limit = %d +}`, host, admin, password, svm, comment, snapshotPolicy, storageLimit) } diff --git a/internal/provider/svm/svms_data_source.go b/internal/provider/svm/svms_data_source.go index 30323e8c..8dd6c0fc 100644 --- a/internal/provider/svm/svms_data_source.go +++ b/internal/provider/svm/svms_data_source.go @@ -165,7 +165,18 @@ func (d *SvmsDataSource) Read(ctx context.Context, req datasource.ReadRequest, r Name: data.Filter.Name.ValueString(), } } - restInfo, err := interfaces.GetSvmsByName(errorHandler, *client, filter) + + cluster, err := interfaces.GetCluster(errorHandler, *client) + if err != nil { + // error reporting done inside GetCluster + return + } + if cluster == nil { + errorHandler.MakeAndReportError("No cluster found", "cluster not found") + return + } + + restInfo, err := interfaces.GetSvmsByName(errorHandler, *client, filter, cluster.Version) if err != nil { // error reporting done inside GetSvms return