diff --git a/examples/resources/netapp-ontap_network_broadcast_domain/provider.tf b/examples/resources/netapp-ontap_network_broadcast_domain/provider.tf new file mode 120000 index 00000000..c6b7138f --- /dev/null +++ b/examples/resources/netapp-ontap_network_broadcast_domain/provider.tf @@ -0,0 +1 @@ +../../provider/provider.tf \ No newline at end of file diff --git a/examples/resources/netapp-ontap_network_broadcast_domain/resource.tf b/examples/resources/netapp-ontap_network_broadcast_domain/resource.tf new file mode 100644 index 00000000..80e8ad45 --- /dev/null +++ b/examples/resources/netapp-ontap_network_broadcast_domain/resource.tf @@ -0,0 +1,7 @@ +resource "netapp-ontap_network_broadcast_domain" "example" { + # required to know which system to interface with + cx_profile_name = "svacluster" + ipspace = "Default" + mtu = 1500 + name = "testme" +} diff --git a/examples/resources/netapp-ontap_network_broadcast_domain/terraform.tfvars b/examples/resources/netapp-ontap_network_broadcast_domain/terraform.tfvars new file mode 120000 index 00000000..8d9d1c96 --- /dev/null +++ b/examples/resources/netapp-ontap_network_broadcast_domain/terraform.tfvars @@ -0,0 +1 @@ +../../provider/terraform.tfvars \ No newline at end of file diff --git a/examples/resources/netapp-ontap_network_broadcast_domain/variables.tf b/examples/resources/netapp-ontap_network_broadcast_domain/variables.tf new file mode 120000 index 00000000..395ce618 --- /dev/null +++ b/examples/resources/netapp-ontap_network_broadcast_domain/variables.tf @@ -0,0 +1 @@ +../../provider/variables.tf \ No newline at end of file diff --git a/internal/interfaces/networking_broadcast_domain.go b/internal/interfaces/networking_broadcast_domain.go index 1e16d103..5678ddd1 100644 --- a/internal/interfaces/networking_broadcast_domain.go +++ b/internal/interfaces/networking_broadcast_domain.go @@ -10,6 +10,7 @@ import ( ) // BroadcastDomainGetDataModelONTAP describes the GET record data model using go types for mapping. +// https://docs.netapp.com/us-en/ontap-restapi/ontap/get-network-ethernet-broadcast-domains-.html#response type BroadcastDomainGetDataModelONTAP struct { IPspace BroadcastDomainIPSpace `mapstructure:"ipspace"` MTU int64 `mapstructure:"mtu"` @@ -19,24 +20,25 @@ type BroadcastDomainGetDataModelONTAP struct { } // BroadcastDomainResourceBodyDataModelONTAP describes the body data model using go types for mapping. +// https://docs.netapp.com/us-en/ontap-restapi/ontap/post-network-ethernet-broadcast-domains.html#request-body type BroadcastDomainResourceBodyDataModelONTAP struct { - IPspace BroadcastDomainIPSpace `mapstructure:"ipspace"` - MTU int64 `mapstructure:"mtu"` - Name string `mapstructure:"name"` - Ports []BroadcastDomainPort `mapstructure:"ports"` - UUID string `mapstructure:"uuid"` + IPspace BroadcastDomainIPSpace `mapstructure:"ipspace,omitempty"` + MTU int64 `mapstructure:"mtu,omitempty"` + Name string `mapstructure:"name,omitempty"` + Ports []BroadcastDomainPort `mapstructure:"ports,omitempty"` + UUID string `mapstructure:"uuid,omitempty"` } // BroadcastDomainIPSpace describes an IP space specifically for broadcast domains. type BroadcastDomainIPSpace struct { - Name string `mapstructure:"name"` - UUID string `mapstructure:"uuid"` + Name string `mapstructure:"name,omitempty"` + // UUID string `mapstructure:"uuid,omitempty"` } // BroadcastDomainPort describes an ethernet port specifically for broadcast domains. type BroadcastDomainPort struct { Name string `mapstructure:"name"` - UUID string `mapstructure:"uuid"` + // UUID string `mapstructure:"uuid"` } // BroadcastDomainDataSourceFilterModel describes filter model. @@ -45,7 +47,8 @@ type BroadcastDomainDataSourceFilterModel struct { Name string `tfsdk:"name"` } -// GetBroadcastDomain to get broadcast_domain info +// Retrieve broadcast domain details +// https://docs.netapp.com/us-en/ontap-restapi/ontap/get-network-ethernet-broadcast-domains-.html func GetBroadcastDomain(errorHandler *utils.ErrorHandler, r restclient.RestClient, id string) (*BroadcastDomainGetDataModelONTAP, error) { api := "/network/ethernet/broadcast-domains/" + id query := r.NewQuery() @@ -76,14 +79,18 @@ func GetBroadcastDomain(errorHandler *utils.ErrorHandler, r restclient.RestClien ) } - tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP)) + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP), + ) return &dataONTAP, nil } -// GetBroadcastDomainByName to get broadcast_domain info +// Retrieve broadcast domain details +// https://docs.netapp.com/us-en/ontap-restapi/ontap/get-network-ethernet-broadcast-domains.html func GetBroadcastDomainByName(errorHandler *utils.ErrorHandler, r restclient.RestClient, ipspace, name string) (*BroadcastDomainGetDataModelONTAP, error) { - api := "/network/ethernet/broadcast-domains/" + api := "/network/ethernet/broadcast-domains" query := r.NewQuery() query.Set("ipspace", ipspace) query.Set("name", name) @@ -114,14 +121,18 @@ func GetBroadcastDomainByName(errorHandler *utils.ErrorHandler, r restclient.Res ) } - tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP)) + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP), + ) return &dataONTAP, nil } -// GetListBroadcastDomains to get broadcast_domain info for all resources matching a filter +// Retrieve broadcast domains for the entire cluster +// https://docs.netapp.com/us-en/ontap-restapi/ontap/get-network-ethernet-broadcast-domains.html func GetListBroadcastDomains(errorHandler *utils.ErrorHandler, r restclient.RestClient, filter *BroadcastDomainDataSourceFilterModel) ([]BroadcastDomainGetDataModelONTAP, error) { - api := "network/ethernet/broadcast-domains/" + api := "network/ethernet/broadcast-domains" query := r.NewQuery() query.Fields([]string{ "ipspace", @@ -163,7 +174,97 @@ func GetListBroadcastDomains(errorHandler *utils.ErrorHandler, r restclient.Rest dataONTAP = append(dataONTAP, record) } - tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP)) + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Read broadcast_domain data source: %#v", dataONTAP), + ) return dataONTAP, nil } + +// Create a new broadcast domain +// https://docs.netapp.com/us-en/ontap-restapi/ontap/post-network-ethernet-broadcast-domains.html +func CreateBroadcastDomain(errorHandler *utils.ErrorHandler, r restclient.RestClient, body BroadcastDomainResourceBodyDataModelONTAP) (*BroadcastDomainGetDataModelONTAP, error) { + api := "/network/ethernet/broadcast-domains" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return nil, errorHandler.MakeAndReportError( + "error encoding broadcast-domain body", + fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body), + ) + } + query := r.NewQuery() + query.Add("return_records", "true") + + statusCode, response, err := r.CallCreateMethod(api, query, bodyMap) + if err != nil { + return nil, errorHandler.MakeAndReportError( + "error creating broadcast-domain", + fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode), + ) + } + + var dataONTAP BroadcastDomainGetDataModelONTAP + if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil { + return nil, errorHandler.MakeAndReportError( + "error decoding broadcast-domain info", + fmt.Sprintf("error on decode broadcast-domain info: %s, statusCode %d, response %#v", err, statusCode, response), + ) + } + + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Create broadcast_domain resource: %#v", dataONTAP), + ) + + return &dataONTAP, nil +} + +// Update broadcast domain properties +// https://docs.netapp.com/us-en/ontap-restapi/ontap/patch-network-ethernet-broadcast-domains-.html +func UpdateBroadcastDomain(errorHandler *utils.ErrorHandler, r restclient.RestClient, body BroadcastDomainResourceBodyDataModelONTAP, id string) error { + api := "/network/ethernet/broadcast-domains/" + id + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return errorHandler.MakeAndReportError( + "error encoding broadcast-domain body", + fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body), + ) + } + + statusCode, _, err := r.CallUpdateMethod(api, nil, bodyMap) + if err != nil { + return errorHandler.MakeAndReportError( + "error updating broadcast-domain", + fmt.Sprintf("error on PATCH %s: %s, statusCode %d", api, err, statusCode), + ) + } + + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Update broadcast_domain resource: %#v", bodyMap), + ) + + return nil +} + +// Delete a broadcast domain +// https://docs.netapp.com/us-en/ontap-restapi/ontap/delete-network-ethernet-broadcast-domains-.html +func DeleteBroadcastDomain(errorHandler *utils.ErrorHandler, r restclient.RestClient, id string) error { + api := "/network/ethernet/broadcast-domains/" + id + + statusCode, _, err := r.CallDeleteMethod(api, nil, nil) + if err != nil { + return errorHandler.MakeAndReportError( + "error deleting broadcast-domain", + fmt.Sprintf("error on DELETE %s: %s, statusCode %d", api, err, statusCode), + ) + } + + tflog.Debug( + errorHandler.Ctx, + fmt.Sprintf("Delete broadcast_domain resource: %s", id), + ) + + return nil +} diff --git a/internal/provider/networking/network_broadcast_domain_resource.go b/internal/provider/networking/network_broadcast_domain_resource.go new file mode 100644 index 00000000..4a22f785 --- /dev/null +++ b/internal/provider/networking/network_broadcast_domain_resource.go @@ -0,0 +1,325 @@ +package networking + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/netapp/terraform-provider-netapp-ontap/internal/interfaces" + "github.com/netapp/terraform-provider-netapp-ontap/internal/provider/connection" + "github.com/netapp/terraform-provider-netapp-ontap/internal/utils" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &BroadcastDomainResource{} +var _ resource.ResourceWithImportState = &BroadcastDomainResource{} + +// NewBroadcastDomainResource is a helper function to simplify the provider implementation. +func NewBroadcastDomainResource() resource.Resource { + return &BroadcastDomainResource{ + config: connection.ResourceOrDataSourceConfig{ + Name: "network_broadcast_domain", + }, + } +} + +// BroadcastDomainResource defines the resource implementation. +type BroadcastDomainResource struct { + config connection.ResourceOrDataSourceConfig +} + +// BroadcastDomainResourceModel describes the resource data model. +type BroadcastDomainResourceModel struct { + CxProfileName types.String `tfsdk:"cx_profile_name"` + IPSpace types.String `tfsdk:"ipspace"` + Name types.String `tfsdk:"name"` + MTU types.Int64 `tfsdk:"mtu"` + Ports types.Set `tfsdk:"ports"` + ID types.String `tfsdk:"id"` +} + +// Metadata returns the resource type name. +func (r *BroadcastDomainResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + r.config.Name +} + +// Schema defines the schema for the resource. +func (r *BroadcastDomainResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Broadcast Domain resource", + + Attributes: map[string]schema.Attribute{ + "cx_profile_name": schema.StringAttribute{ + MarkdownDescription: "Connection profile name", + Required: true, + }, + "ipspace": schema.StringAttribute{ + MarkdownDescription: "Name of the IPspace the broadcast domain belongs to", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("Default"), + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the broadcast domain, scoped to its IPspace", + Required: true, + }, + "mtu": schema.Int64Attribute{ + MarkdownDescription: "Maximum transmission unit, largest packet size on this network", + Required: true, + }, + "ports": schema.SetAttribute{ + ElementType: types.StringType, + MarkdownDescription: "Ports that belong to the broadcast domain", + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Broadcast domain UUID", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *BroadcastDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(connection.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.config.ProviderConfig = config +} + +// Read refreshes the Terraform state with the latest data. +func (r *BroadcastDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data BroadcastDomainResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use existing-, or create new REST API client + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + // we need to defer setting the client until we can read the connection profile name + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + // Call ONTAP REST API for reading broadcast_domain info + var restInfo *interfaces.BroadcastDomainGetDataModelONTAP + if data.ID.IsNull() { + restInfo, err = interfaces.GetBroadcastDomainByName( + errorHandler, + *client, + data.IPSpace.ValueString(), + data.Name.ValueString(), + ) + if err != nil { + // error reporting done inside GetBroadcastDomainByName + return + } + } else { + restInfo, err = interfaces.GetBroadcastDomain( + errorHandler, + *client, + data.ID.ValueString(), + ) + if err != nil { + // error reporting done inside GetBroadcastDomain + return + } + } + if restInfo == nil { + errorHandler.MakeAndReportError( + "No broadcast domain found", + fmt.Sprintf("No broadcast-domain, %s found.", data.Name.ValueString()), + ) + return + } + + // Copy broadcast_domain info to data source model + data.IPSpace = types.StringValue(restInfo.IPspace.Name) + data.Name = types.StringValue(restInfo.Name) + data.MTU = types.Int64Value(restInfo.MTU) + data.ID = types.StringValue(restInfo.UUID) + + var ports []attr.Value + for _, v := range restInfo.Ports { + ports = append(ports, types.StringValue(v.Name)) + } + data.Ports, _ = types.SetValue(types.StringType, ports) + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Debug(ctx, fmt.Sprintf("read a resource: %#v", data)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Create a resource and retrieve UUID +func (r *BroadcastDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *BroadcastDomainResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + // Copy broadcast_domain info to request body + var body interfaces.BroadcastDomainResourceBodyDataModelONTAP + body.IPspace.Name = data.IPSpace.ValueString() + body.MTU = data.MTU.ValueInt64() + body.Name = data.Name.ValueString() + + // Use existing-, or create new REST API client + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + // Call ONTAP REST API for creating broadcast_domain + resource, err := interfaces.CreateBroadcastDomain(errorHandler, *client, body) + if err != nil { + return + } + + // Copy broadcast_domain info to data source model + data.ID = types.StringValue(resource.UUID) + + var ports []attr.Value + for _, v := range resource.Ports { + ports = append(ports, types.StringValue(v.Name)) + } + data.Ports, _ = types.SetValue(types.StringType, ports) + + tflog.Trace(ctx, fmt.Sprintf("created a resource, UUID=%s", data.ID)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *BroadcastDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state *BroadcastDomainResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + // Read state file data + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + // Copy broadcast_domain info to request body + var body interfaces.BroadcastDomainResourceBodyDataModelONTAP + if !data.IPSpace.Equal(state.IPSpace) { + body.IPspace.Name = data.IPSpace.ValueString() + } + if !data.Name.Equal(state.Name) { + body.Name = data.Name.ValueString() + } + if !data.MTU.Equal(state.MTU) { + body.MTU = data.MTU.ValueInt64() + } + + // Use existing-, or create new REST API client + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + // Call ONTAP REST API for updating broadcast_domain + err = interfaces.UpdateBroadcastDomain(errorHandler, *client, body, data.ID.ValueString()) + if err != nil { + return + } + + tflog.Trace(ctx, fmt.Sprintf("updated a resource, UUID=%s", data.ID)) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *BroadcastDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *BroadcastDomainResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + // Use existing-, or create new REST API client + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + // Ensure that ID in known + if data.ID.IsNull() { + errorHandler.MakeAndReportError("ID is null", "broadcast_domain ID is null") + return + } + + // Call ONTAP REST API for deleting broadcast_domain + err = interfaces.DeleteBroadcastDomain(errorHandler, *client, data.ID.ValueString()) + if err != nil { + return + } + + tflog.Trace(ctx, fmt.Sprintf("deleted a resource, UUID=%s", data.ID)) +} + +// ImportState imports a resource using ID from terraform import command by calling the Read method. +func (r *BroadcastDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, fmt.Sprintf("import req a network broadcast domain resource: %#v", req)) + // idParts := strings.Split(req.ID, ",") + // if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + // resp.Diagnostics.AddError( + // "Unexpected Import Identifier", + // fmt.Sprintf("Expected import identifier with format: name,svm_name,cx_profile_name. Got: %q", req.ID), + // ) + // return + // } + // resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[0])...) + // resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("svm_name"), idParts[1])...) + // resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[2])...) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4f837d71..d4720242 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -220,6 +220,7 @@ func (p *ONTAPProvider) Resources(ctx context.Context) []func() resource.Resourc cluster.NewClusterScheduleResource, name_services.NewNameServicesDNSResource, name_services.NewNameServicesLDAPResource, + networking.NewBroadcastDomainResource, networking.NewIPInterfaceResource, networking.NewIPRouteResource, NewExampleResource,