diff --git a/docs/resources/cluster_peers_resource.md b/docs/resources/cluster_peers_resource.md
new file mode 100644
index 00000000..f9c2364f
--- /dev/null
+++ b/docs/resources/cluster_peers_resource.md
@@ -0,0 +1,126 @@
+---
+page_title: "ONTAP: Cluster Peers"
+subcategory: "Cluster"
+description: |-
+ Cluster peers resource
+---
+
+# Resource Cluster Peers
+
+Create/Modify/Delete a cluster peer.
+
+### Related ONTAP commands
+* cluster peer create
+* cluster peer modify
+* cluster peer delete
+
+## Example Usage
+
+```
+resource "netapp-ontap_cluster_peers_resource" "cluster_peers" {
+ # required to know which system to interface with
+ cx_profile_name = "cluster3"
+ name = "testme"
+ remote = {
+ ip_addresses = ["10.10.10.10", "10.10.10.11"]
+ }
+ source_details = {
+ ip_addresses = ["10.10.10.12"]
+ }
+ peer_cx_profile_name = "cluster2"
+ passphrase = "12345678"
+ peer_applications = ["snapmirror"]
+}
+```
+
+## Argument Reference
+
+### Required
+
+- `cx_profile_name` (String) Connection profile name
+- `remote` (Attributes) (see [below for nested schema](#nestedatt--remote))
+- `source_details` (Attributes) (see [below for nested schema](#nestedatt--source_details))
+
+### Optional
+
+- `passphrase` (String) User generated passphrase for use in authentication
+- `generate_passphrase` (String) When true, ONTAP automatically generates a passphrase to authenticate cluster peers
+- `name` (String) Name of the peering relationship or name of the remote peer
+- `peer_applications` (String) SVM peering applications
+- `peer_cx_profile_name` (String) Peer connection profile name, to be accepted from peer side to make the status OK
+
+### Read-Only
+
+- `id` (String) Cluster peer relation source identifier
+- `peer_id` (String) Cluster peer relation destination identifier
+- `state` (String) Cluster peering state
+
+
+### Nested Schema for `remote`
+
+Required:
+
+- `ip_addresses` (Set of String) list of the remote ip addresses
+
+
+### Nested Schema for `remote`
+
+Required:
+
+- `ip_addresses` (Set of String) list of the remote ip addresses
+
+
+## Import
+This Resource supports import, which allows you to import existing cluster peer relation into the state of this resoruce.
+Import require a unique ID composed of the cluster name and cx_profile_name, separated by a comma.
+
+ id = `name`,`cx_profile_name`
+
+ ### Terraform Import
+
+ For example
+ ```shell
+ terraform import netapp-ontap_cluster_peers_resource.example clutername-1,cluster4
+ ```
+
+!> The terraform import CLI command can only import resources into the state. Importing via the CLI does not generate configuration. If you want to generate the accompanying configuration for imported resources, use the import block instead.
+
+### Terrafomr Import Block
+This requires Terraform 1.5 or higher, and will auto create the configuration for you
+
+First create the block
+```terraform
+import {
+ to = netapp-ontap_cluster_peers_resource.example.cluster_import
+ id = "clutername-1,cluster4"
+}
+```
+Next run, this will auto create the configuration for you
+```shell
+terraform plan -generate-config-out=generated.tf
+```
+This will generate a file called generated.tf, which will contain the configuration for the imported resource
+```terraform
+# __generated__ by Terraform
+# Please review these resources and move them into your main configuration files.
+# __generated__ by Terraform from "clutername-1,cluster4"
+resource "netapp-ontap_cluster_peers_resource.example" "cluster_peers_import" {
+ cx_profile_name = "cluster3"
+ name = "test"
+ generate_passphrase = false
+ passphrase = "12345678"
+ peer_applications = ["snapmirror"]
+ peer_cx_profile_name = "cluster2"
+ remote = {
+ ip_addresses = [
+ "10.10.10.10"
+ ]
+ }
+ source_details = {
+ ip_addresses = [
+ "10.10.10.11"
+ ]
+ }
+ state = "pending"
+}
+```
diff --git a/docs/resources/cluster_schedule_resource.md b/docs/resources/cluster_schedule_resource.md
index 25507bd1..240689a3 100644
--- a/docs/resources/cluster_schedule_resource.md
+++ b/docs/resources/cluster_schedule_resource.md
@@ -39,8 +39,7 @@ resource "netapp-ontap_cluster_schedule_resource" "cs_example2" {
}
```
-
-### Related ONTAP commands
+## Argument Reference
### Required
diff --git a/docs/resources/snapmirror_resource.md b/docs/resources/snapmirror_resource.md
index 81b12432..27c39519 100644
--- a/docs/resources/snapmirror_resource.md
+++ b/docs/resources/snapmirror_resource.md
@@ -8,14 +8,18 @@ description: |-
# Resource Snapmirror
-Create/Delete a snapmirror resource
-
-~> **NOTE:** This module currently does not support modifying an existing snapmirror relationship. This will be added in a future release.
+Create/Modify/Delete a snapmirror resource
### Related ONTAP commands
* snapmirror create
+* snapmirror modify
* snapmirror delete
+## Supported Platforms
+* On-perm ONTAP system 9.6 or higher
+
+~> **NOTE:** Amazon FSx for NetApp ONTAP is not supported
+
## Example Usage
```
# Create a snapmirror
diff --git a/docs/resources/storage_lun_resource.md b/docs/resources/storage_lun_resource.md
index 70b291be..01fad77f 100644
--- a/docs/resources/storage_lun_resource.md
+++ b/docs/resources/storage_lun_resource.md
@@ -1,7 +1,7 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "netapp-ontap_storage_lun_resource Resource - terraform-provider-netapp-ontap"
-subcategory: ""
+subcategory: "storage"
description: |-
StorageLun resource
---
diff --git a/docs/resources/svm_peers_resource.md b/docs/resources/svm_peers_resource.md
index dacd51b2..eee50a94 100644
--- a/docs/resources/svm_peers_resource.md
+++ b/docs/resources/svm_peers_resource.md
@@ -53,7 +53,7 @@ resource "netapp-ontap_svm_peers_resource" "example" {
### Read-Only
-- `id` (String) svm peeer identifier
+- `id` (String) svm peer identifier
### Nested Schema for `peer`
diff --git a/examples/resources/netapp-ontap_cluster_peers/provider.tf b/examples/resources/netapp-ontap_cluster_peers/provider.tf
new file mode 120000
index 00000000..c6b7138f
--- /dev/null
+++ b/examples/resources/netapp-ontap_cluster_peers/provider.tf
@@ -0,0 +1 @@
+../../provider/provider.tf
\ No newline at end of file
diff --git a/examples/resources/netapp-ontap_cluster_peers/resource.tf b/examples/resources/netapp-ontap_cluster_peers/resource.tf
new file mode 100644
index 00000000..d447574a
--- /dev/null
+++ b/examples/resources/netapp-ontap_cluster_peers/resource.tf
@@ -0,0 +1,15 @@
+resource "netapp-ontap_cluster_peers_resource" "cluster_peers" {
+ # required to know which system to interface with
+ cx_profile_name = "cluster3"
+ # name = "testme"
+ remote = {
+ ip_addresses = ["10.10.10.10"]
+ }
+ source_details = {
+ ip_addresses = ["10.10.10.11"]
+ }
+ peer_cx_profile_name = "cluster2"
+ # generate_passphrase = true
+ passphrase = "12345678"
+ peer_applications = ["snapmirror"]
+}
diff --git a/examples/resources/netapp-ontap_cluster_peers/terraform.tfvars b/examples/resources/netapp-ontap_cluster_peers/terraform.tfvars
new file mode 120000
index 00000000..8d9d1c96
--- /dev/null
+++ b/examples/resources/netapp-ontap_cluster_peers/terraform.tfvars
@@ -0,0 +1 @@
+../../provider/terraform.tfvars
\ No newline at end of file
diff --git a/examples/resources/netapp-ontap_cluster_peers/variables.tf b/examples/resources/netapp-ontap_cluster_peers/variables.tf
new file mode 120000
index 00000000..395ce618
--- /dev/null
+++ b/examples/resources/netapp-ontap_cluster_peers/variables.tf
@@ -0,0 +1 @@
+../../provider/variables.tf
\ No newline at end of file
diff --git a/internal/interfaces/cluster_peer.go b/internal/interfaces/cluster_peer.go
index 0923c9a5..f2ea842d 100644
--- a/internal/interfaces/cluster_peer.go
+++ b/internal/interfaces/cluster_peer.go
@@ -2,6 +2,7 @@ package interfaces
import (
"fmt"
+
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/mitchellh/mapstructure"
"github.com/netapp/terraform-provider-netapp-ontap/internal/restclient"
@@ -14,21 +15,49 @@ type ClusterPeerGetDataModelONTAP struct {
UUID string `mapstructure:"uuid"`
Remote Remote `mapstructure:"remote"`
Status Status `mapstructure:"status"`
+ Authentication Authentication `mapstructure:"authentication"`
PeerApplications []string `mapstructure:"peer_applications"`
Encryption ClusterPeerEncryption `mapstructure:"encryption"`
IPAddress string `mapstructure:"ip_address"`
Ipspace ClusterPeerIpspace `mapstructure:"ipspace"`
}
+// ClusterPeersGetDataModelONTAP describes the GET record data model using go types for mapping.
+type ClusterPeersGetDataModelONTAP struct {
+ Name string `mapstructure:"name"`
+ UUID string `mapstructure:"uuid"`
+ Authentication AuthenticationCreateResponse `mapstructure:"authentication"`
+}
+
+// ClusterPeersResourceBodyDataModelONTAP describes the body data model using go types for mapping.
+type ClusterPeersResourceBodyDataModelONTAP struct {
+ Name string `mapstructure:"name,omitempty"`
+ Remote RemoteBody `mapstructure:"remote"`
+ PeerApplications []string `mapstructure:"peer_applications,omitempty"`
+ Authentication Authentication `mapstructure:"authentication"`
+}
+
// ClusterPeerIpspace describes the GET record data model using go types for mapping.
type ClusterPeerIpspace struct {
Name string `mapstructure:"name"`
}
+// Authentication describes the POST record body model using go types for mapping.
+type Authentication struct {
+ State string `mapstructure:"state,omitempty"`
+ GeneratePassphrase bool `mapstructure:"generate_passphrase,omitempty"`
+ Passphrase string `mapstructure:"passphrase,omitempty"`
+}
+
+// AuthenticationCreateResponse describes the POST record response model using go types for mapping.
+type AuthenticationCreateResponse struct {
+ Passphrase string `mapstructure:"passphrase,omitempty"`
+}
+
// ClusterPeerEncryption describes the GET record data model using go types for mapping.
type ClusterPeerEncryption struct {
- Propsed string `mapstructure:"proposed"`
- State string `mapstructure:"state"`
+ Proposed string `mapstructure:"proposed"`
+ State string `mapstructure:"state"`
}
// Remote describes the GET record data model using go types for mapping.
@@ -37,6 +66,11 @@ type Remote struct {
Name string `mapstructure:"name"`
}
+// RemoteBody describes the POST record body model using go types for mapping.
+type RemoteBody struct {
+ IPAddress []string `mapstructure:"ip_addresses"`
+}
+
// Status describes the GET record data model using go types for mapping.
type Status struct {
State string `mapstructure:"state"`
@@ -60,10 +94,10 @@ func GetClusterPeerByName(errorHandler *utils.ErrorHandler, r restclient.RestCli
query.Fields([]string{"name", "uuid", "remote", "status", "peer_applications", "encryption", "ip_address", "ipspace"})
statusCode, response, err := r.GetNilOrOneRecord("cluster/peers", query, nil)
if err != nil {
- return nil, errorHandler.MakeAndReportError("Error getting cluster peer", fmt.Sprint("error on get cluster/peer: #{err}"))
+ return nil, errorHandler.MakeAndReportError("Error getting cluster peer", fmt.Sprintf("error on get cluster/peer: %s, statusCode %d", err, statusCode))
}
if response == nil {
- return nil, errorHandler.MakeAndReportError("No cluster peer found", fmt.Sprint("no cluster peer found with name: #{name}"))
+ return nil, errorHandler.MakeAndReportError("No cluster peer found", fmt.Sprintf("no cluster peer found with name: %s, statusCode %d", name, statusCode))
}
var dataONTAP *ClusterPeerGetDataModelONTAP
if error := mapstructure.Decode(response, &dataONTAP); error != nil {
@@ -73,6 +107,21 @@ func GetClusterPeerByName(errorHandler *utils.ErrorHandler, r restclient.RestCli
return dataONTAP, nil
}
+// GetClusterPeer to get ClusterPeer info by uuid
+func GetClusterPeer(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string) (*ClusterPeerGetDataModelONTAP, error) {
+ statusCode, response, err := r.GetNilOrOneRecord("cluster/peers/"+uuid, nil, nil)
+ if err != nil {
+ return nil, errorHandler.MakeAndReportError("error reading cluster peer info", fmt.Sprintf("error on GET cluster/peers: %s, statusCode %d", err, statusCode))
+ }
+
+ var dataONTAP *ClusterPeerGetDataModelONTAP
+ if err := mapstructure.Decode(response, &dataONTAP); err != nil {
+ return nil, errorHandler.MakeAndReportError("failed to decode response from GET cluster peer", fmt.Sprintf("error: %s, statusCode %d, response %#v", err, statusCode, response))
+ }
+ tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read cluster peer info: %#v", dataONTAP))
+ return dataONTAP, nil
+}
+
// GetClusterPeers gets all cluster peers.
func GetClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, filter *ClusterPeerDataSourceFilterModel) ([]ClusterPeerGetDataModelONTAP, error) {
query := r.NewQuery()
@@ -86,10 +135,10 @@ func GetClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient,
}
statusCode, response, err := r.GetZeroOrMoreRecords("cluster/peers", query, nil)
if err != nil {
- return nil, errorHandler.MakeAndReportError("Error getting cluster peers", fmt.Sprint("error on get cluster/peers: #{err}"))
+ return nil, errorHandler.MakeAndReportError("Error getting cluster peers", fmt.Sprintf("error on get cluster/peers: %s, statusCode %d", err, statusCode))
}
if response == nil {
- return nil, errorHandler.MakeAndReportError("No cluster peers found", fmt.Sprint("no cluster peers found with name: #{name}"))
+ return nil, errorHandler.MakeAndReportError("No cluster peers found", "no cluster peers fouund")
}
var dataONTAP []ClusterPeerGetDataModelONTAP
for _, info := range response {
@@ -102,3 +151,54 @@ func GetClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient,
tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Read Cluster/peers source - udata: %#v", dataONTAP))
return dataONTAP, nil
}
+
+// CreateClusterPeers to create cluster_peers
+func CreateClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, body ClusterPeersResourceBodyDataModelONTAP) (*ClusterPeersGetDataModelONTAP, error) {
+ api := "cluster/peers"
+ var bodyMap map[string]interface{}
+ if err := mapstructure.Decode(body, &bodyMap); err != nil {
+ return nil, errorHandler.MakeAndReportError("error encoding cluster_peers body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body))
+ }
+ query := r.NewQuery()
+ query.Add("return_records", "true")
+ query.Add("return_timeout", "15")
+ statusCode, response, err := r.CallCreateMethod(api, query, bodyMap)
+ if err != nil {
+ return nil, errorHandler.MakeAndReportError("error creating cluster_peers", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode))
+ }
+
+ var dataONTAP ClusterPeersGetDataModelONTAP
+ if err := mapstructure.Decode(response.Records[0], &dataONTAP); err != nil {
+ return nil, errorHandler.MakeAndReportError("error decoding cluster_peers info", fmt.Sprintf("error on decode storage/cluster_peerss info: %s, statusCode %d, response %#v", err, statusCode, response))
+ }
+ tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Create cluster_peers source - udata: %#v", dataONTAP))
+ return &dataONTAP, nil
+}
+
+// UpdateClusterPeers updates Cluster Peer
+func UpdateClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, data any, uuid string) error {
+ api := "cluster/peers/" + uuid
+ var body map[string]interface{}
+ if err := mapstructure.Decode(data, &body); err != nil {
+ return errorHandler.MakeAndReportError("error encoding cluster_peers body", fmt.Sprintf("error on encoding cluster/peers body: %s, body: %#v", err, data))
+ }
+ query := r.NewQuery()
+ query.Add("return_records", "true")
+ query.Add("return_timeout", "15")
+ // API has no option to return records
+ statusCode, _, err := r.CallUpdateMethod(api, query, body)
+ if err != nil {
+ return errorHandler.MakeAndReportError("error updating cluster_peers", fmt.Sprintf("error on PATCH cluster/peers: %s, statusCode %d", err, statusCode))
+ }
+ return nil
+}
+
+// DeleteClusterPeers to delete cluster_peers
+func DeleteClusterPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string) error {
+ api := "cluster/peers"
+ statusCode, _, err := r.CallDeleteMethod(api+"/"+uuid, nil, nil)
+ if err != nil {
+ return errorHandler.MakeAndReportError("error deleting cluster_peers", fmt.Sprintf("error on DELETE %s: %s, statusCode %d", api, err, statusCode))
+ }
+ return nil
+}
diff --git a/internal/interfaces/svm_peers.go b/internal/interfaces/svm_peers.go
index 4c204d86..50c4b826 100644
--- a/internal/interfaces/svm_peers.go
+++ b/internal/interfaces/svm_peers.go
@@ -179,7 +179,7 @@ func CreateSVMPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, b
return &dataONTAP, nil
}
-// UpdateSVMPeers updates Snapmirror
+// UpdateSVMPeers updates svm peers
func UpdateSVMPeers(errorHandler *utils.ErrorHandler, r restclient.RestClient, data any, uuid string) error {
api := "svm/peers/" + uuid
var body map[string]interface{}
diff --git a/internal/provider/cluster_peer_data_source.go b/internal/provider/cluster_peer_data_source.go
index 60527793..c61e996f 100644
--- a/internal/provider/cluster_peer_data_source.go
+++ b/internal/provider/cluster_peer_data_source.go
@@ -208,7 +208,7 @@ func (d *ClusterPeerDataSource) Read(ctx context.Context, req datasource.ReadReq
data.PeerApplications[index] = types.StringValue(peerApplication)
}
data.Encryption = &ClusterPeerDataSourceEncryption{
- Proposed: types.StringValue(restInfo.Encryption.Propsed),
+ Proposed: types.StringValue(restInfo.Encryption.Proposed),
State: types.StringValue(restInfo.Encryption.State),
}
data.IPAddress = types.StringValue(restInfo.IPAddress)
diff --git a/internal/provider/cluster_peers_data_source.go b/internal/provider/cluster_peers_data_source.go
index 7eb1302f..eb2b657c 100644
--- a/internal/provider/cluster_peers_data_source.go
+++ b/internal/provider/cluster_peers_data_source.go
@@ -209,7 +209,7 @@ func (d *ClusterPeersDataSource) Read(ctx context.Context, req datasource.ReadRe
},
PeerApplications: make([]types.String, len(record.PeerApplications)),
Encryption: &ClusterPeerDataSourceEncryption{
- Proposed: types.StringValue(record.Encryption.Propsed),
+ Proposed: types.StringValue(record.Encryption.Proposed),
State: types.StringValue(record.Encryption.State),
},
IPAddress: types.StringValue(record.IPAddress),
diff --git a/internal/provider/cluster_peers_resource.go b/internal/provider/cluster_peers_resource.go
new file mode 100644
index 00000000..deb40266
--- /dev/null
+++ b/internal/provider/cluster_peers_resource.go
@@ -0,0 +1,432 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
+ "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/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "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/utils"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &ClusterPeersResource{}
+var _ resource.ResourceWithImportState = &ClusterPeersResource{}
+
+// NewClusterPeersResource is a helper function to simplify the provider implementation.
+func NewClusterPeersResource() resource.Resource {
+ return &ClusterPeersResource{
+ config: resourceOrDataSourceConfig{
+ name: "cluster_peers_resource",
+ },
+ }
+}
+
+// ClusterPeersResource defines the resource implementation.
+type ClusterPeersResource struct {
+ config resourceOrDataSourceConfig
+}
+
+// ClusterPeersResourceModel describes the resource data model.
+type ClusterPeersResourceModel struct {
+ CxProfileName types.String `tfsdk:"cx_profile_name"`
+ Passphrase types.String `tfsdk:"passphrase"`
+ Name types.String `tfsdk:"name"`
+ Remote *Remote `tfsdk:"remote"`
+ SourceDetails *Remote `tfsdk:"source_details"`
+ PeerCxProfileName types.String `tfsdk:"peer_cx_profile_name"`
+ GeneratePassphrase types.Bool `tfsdk:"generate_passphrase"`
+ PeerApplications []types.String `tfsdk:"peer_applications"`
+ State types.String `tfsdk:"state"`
+ PeerID types.String `tfsdk:"peer_id"`
+ ID types.String `tfsdk:"id"`
+}
+
+// Remote describes Remote data model.
+type Remote struct {
+ IPAddresses []types.String `tfsdk:"ip_addresses"`
+}
+
+// Status describes the status data model.
+type Status struct {
+ State types.String `tfsdk:"state"`
+}
+
+// Metadata returns the resource type name.
+func (r *ClusterPeersResource) 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 *ClusterPeersResource) 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: "ClusterPeers resource",
+
+ Attributes: map[string]schema.Attribute{
+ "cx_profile_name": schema.StringAttribute{
+ MarkdownDescription: "Connection profile name",
+ Required: true,
+ },
+ "passphrase": schema.StringAttribute{
+ MarkdownDescription: "User generated passphrase for use in authentication",
+ Optional: true,
+ Sensitive: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Name of the peering relationship or name of the remote peer",
+ Optional: true,
+ },
+ "generate_passphrase": schema.BoolAttribute{
+ MarkdownDescription: "When true, ONTAP automatically generates a passphrase to authenticate cluster peers",
+ Optional: true,
+ Validators: []validator.Bool{
+ boolvalidator.ConflictsWith(path.Expressions{
+ path.MatchRoot("passphrase"),
+ }...),
+ },
+ },
+ "remote": schema.SingleNestedAttribute{
+ MarkdownDescription: "Remote cluster details for cluster peer",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addresses": schema.SetAttribute{
+ ElementType: types.StringType,
+ MarkdownDescription: "list of the remote ip addresses",
+ Required: true,
+ },
+ },
+ },
+ "source_details": schema.SingleNestedAttribute{
+ MarkdownDescription: "Source cluster details for cluster peer from remote cluster",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "ip_addresses": schema.SetAttribute{
+ ElementType: types.StringType,
+ MarkdownDescription: "list of the source ip addresses",
+ Required: true,
+ },
+ },
+ },
+ "peer_applications": schema.SetAttribute{
+ ElementType: types.StringType,
+ MarkdownDescription: "SVM peering applications",
+ Optional: true,
+ },
+ "peer_cx_profile_name": schema.StringAttribute{
+ MarkdownDescription: "Peer connection profile name, to be accepted from peer side to make the status OK",
+ Required: true,
+ },
+ "state": schema.StringAttribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "peer_id": schema.StringAttribute{
+ MarkdownDescription: "ClusterPeers destination UUID",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "id": schema.StringAttribute{
+ MarkdownDescription: "ClusterPeers source UUID",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ }
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *ClusterPeersResource) 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.(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),
+ )
+ }
+ r.config.providerConfig = config
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *ClusterPeersResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ClusterPeersResourceModel
+
+ // 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)
+ // we need to defer setting the client until we can read the connection profile name
+ client, err := getRestClient(errorHandler, r.config, data.CxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("read a ClusterPeer resource: %#v", data))
+ var restInfo *interfaces.ClusterPeerGetDataModelONTAP
+ if data.ID.ValueString() != "" {
+ restInfo, err = interfaces.GetClusterPeer(errorHandler, *client, data.ID.ValueString())
+ if err != nil {
+ // error reporting done inside GetClusterPeer
+ return
+ }
+ } else {
+ restInfo, err = interfaces.GetClusterPeerByName(errorHandler, *client, data.Name.ValueString())
+ if err != nil {
+ // error reporting done inside GetClusterPeerByName
+ return
+ }
+ }
+
+ if restInfo == nil {
+ errorHandler.MakeAndReportError("error reading info", "No Cluster Peer found")
+ }
+
+ data.ID = types.StringValue(restInfo.UUID)
+ var ipAddresses []types.String
+ for _, e := range restInfo.Remote.IPAddress {
+ ipAddresses = append(ipAddresses, types.StringValue(e))
+ }
+ if data.Remote == nil {
+ data.Remote = &Remote{}
+ }
+ data.Remote.IPAddresses = ipAddresses
+ data.State = types.StringValue(restInfo.Authentication.State)
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+// Create a resource and retrieve UUID
+func (r *ClusterPeersResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data *ClusterPeersResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ var body interfaces.ClusterPeersResourceBodyDataModelONTAP
+ errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if !data.Name.IsUnknown() {
+ body.Name = data.Name.ValueString()
+ }
+ if data.PeerApplications != nil {
+ var applications []string
+ for _, e := range data.PeerApplications {
+ applications = append(applications, e.ValueString())
+ }
+ body.PeerApplications = applications
+ }
+ var ipAddresses []string
+ for _, e := range data.Remote.IPAddresses {
+ ipAddresses = append(ipAddresses, e.ValueString())
+ }
+ body.Remote.IPAddress = ipAddresses
+ if !data.GeneratePassphrase.IsUnknown() {
+ body.Authentication.GeneratePassphrase = data.GeneratePassphrase.ValueBool()
+ }
+ if !data.Passphrase.IsUnknown() {
+ body.Authentication.Passphrase = data.Passphrase.ValueString()
+ }
+
+ client, err := getRestClient(errorHandler, r.config, data.CxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+
+ resource, err := interfaces.CreateClusterPeers(errorHandler, *client, body)
+ if err != nil {
+ return
+ }
+
+ data.ID = types.StringValue(resource.UUID)
+ peerClient, err := getRestClient(errorHandler, r.config, data.PeerCxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+ var bodyPeer interfaces.ClusterPeersResourceBodyDataModelONTAP
+ var ipAddressesPeer []string
+ for _, e := range data.SourceDetails.IPAddresses {
+ ipAddressesPeer = append(ipAddressesPeer, e.ValueString())
+ }
+ bodyPeer.Remote.IPAddress = ipAddressesPeer
+ if data.PeerApplications != nil {
+ var applications []string
+ for _, e := range data.PeerApplications {
+ applications = append(applications, e.ValueString())
+ }
+ bodyPeer.PeerApplications = applications
+ }
+ bodyPeer.Authentication.Passphrase = resource.Authentication.Passphrase
+ resourcePeer, err := interfaces.CreateClusterPeers(errorHandler, *peerClient, bodyPeer)
+ if err != nil {
+ return
+ }
+ data.PeerID = types.StringValue(resourcePeer.UUID)
+
+ var restInfo *interfaces.ClusterPeerGetDataModelONTAP
+ restInfo, err = interfaces.GetClusterPeer(errorHandler, *client, data.ID.ValueString())
+ if err != nil {
+ // error reporting done inside GetSVMPeers
+ return
+ }
+ if restInfo == nil {
+ errorHandler.MakeAndReportError("error reading info", "No Cluster Peer found")
+ }
+ data.State = types.StringValue(restInfo.Authentication.State)
+
+ tflog.Trace(ctx, "created a resource")
+
+ // 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 *ClusterPeersResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state, plan *ClusterPeersResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ // Read Terraform state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ client, err := getRestClient(errorHandler, r.config, state.CxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+
+ isEqual := reflect.DeepEqual(plan.Remote.IPAddresses, state.Remote.IPAddresses)
+
+ if plan.Remote.IPAddresses != nil && !isEqual {
+ var ipAddresses []string
+ for _, e := range plan.Remote.IPAddresses {
+ ipAddresses = append(ipAddresses, e.ValueString())
+ }
+ var body interfaces.ClusterPeersResourceBodyDataModelONTAP
+ body.Remote.IPAddress = ipAddresses
+ err = interfaces.UpdateClusterPeers(errorHandler, *client, body, plan.ID.ValueString())
+ if err != nil {
+ return
+ }
+ }
+
+ restInfo, err := interfaces.GetClusterPeer(errorHandler, *client, plan.ID.ValueString())
+ if err != nil {
+ // error reporting done inside GetClusterPeer
+ return
+ }
+
+ plan.State = types.StringValue(restInfo.Authentication.State)
+ var ipAddresses []types.String
+ for _, e := range restInfo.Remote.IPAddress {
+ ipAddresses = append(ipAddresses, types.StringValue(e))
+ }
+ if plan.Remote == nil {
+ plan.Remote = &Remote{}
+ }
+ plan.Remote.IPAddresses = ipAddresses
+
+ tflog.Debug(ctx, fmt.Sprintf("updated svm peer resource: UUID=%s", plan.ID))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *ClusterPeersResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data *ClusterPeersResourceModel
+
+ // 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)
+ client, err := getRestClient(errorHandler, r.config, data.CxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+
+ if data.ID.IsNull() {
+ errorHandler.MakeAndReportError("UUID is null", "cluster_peers UUID is null")
+ return
+ }
+
+ err = interfaces.DeleteClusterPeers(errorHandler, *client, data.ID.ValueString())
+ if err != nil {
+ return
+ }
+
+ // Delete remote peer
+ peerClient, err := getRestClient(errorHandler, r.config, data.PeerCxProfileName)
+ if err != nil {
+ // error reporting done inside NewClient
+ return
+ }
+
+ if data.ID.IsNull() {
+ errorHandler.MakeAndReportError("UUID is null", "cluster_peers UUID is null")
+ return
+ }
+
+ err = interfaces.DeleteClusterPeers(errorHandler, *peerClient, data.PeerID.ValueString())
+ if err != nil {
+ return
+ }
+
+ tflog.Trace(ctx, "deleted a resource")
+
+}
+
+// ImportState imports a resource using ID from terraform import command by calling the Read method.
+func (r *ClusterPeersResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ idParts := strings.Split(req.ID, ",")
+
+ if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
+ resp.Diagnostics.AddError(
+ "Unexpected Import Identifier",
+ fmt.Sprintf("Expected import identifier with format: 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("cx_profile_name"), idParts[1])...)
+}
diff --git a/internal/provider/cluster_peers_resource_test.go b/internal/provider/cluster_peers_resource_test.go
new file mode 100644
index 00000000..bf63f958
--- /dev/null
+++ b/internal/provider/cluster_peers_resource_test.go
@@ -0,0 +1,83 @@
+package provider
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAccClusterPeersResource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create svm peer and read
+ {
+ Config: testAccClusterPeersResourceConfig("10.193.180.110", "10.193.176.189"),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("netapp-ontap_cluster_peers_resource.example", "remote.ip_addresses.0", "10.193.180.110"),
+ ),
+ },
+ // Update applications
+ {
+ Config: testAccClusterPeersResourceConfig("10.193.180.109", "10.193.176.189"),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("netapp-ontap_cluster_peers_resource.example", "remote.ip_addresses.0", "10.193.180.109"),
+ ),
+ },
+ // Import and read
+ {
+ ResourceName: "netapp-ontap_cluster_peers_resource.example",
+ ImportState: true,
+ ImportStateId: fmt.Sprintf("%s,%s", "vinaykuscluster-1", "cluster4"),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("netapp-ontap_cluster_peers_resource.example", "name", "vinaykuscluster-1"),
+ ),
+ },
+ },
+ })
+}
+func testAccClusterPeersResourceConfig(remotIP, sourceIP string) string {
+ host := os.Getenv("TF_ACC_NETAPP_HOST2")
+ admin := os.Getenv("TF_ACC_NETAPP_USER")
+ password := os.Getenv("TF_ACC_NETAPP_PASS")
+ host2 := os.Getenv("TF_ACC_NETAPP_HOST")
+ if host == "" || admin == "" || password == "" {
+ fmt.Println("TF_ACC_NETAPP_HOST2, TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests")
+ os.Exit(1)
+ }
+ return fmt.Sprintf(`
+provider "netapp-ontap" {
+ connection_profiles = [
+ {
+ name = "cluster4"
+ hostname = "%s"
+ username = "%s"
+ password = "%s"
+ validate_certs = false
+ },
+ {
+ name = "cluster3"
+ hostname = "%s"
+ username = "%s"
+ password = "%s"
+ validate_certs = false
+ },
+ ]
+}
+
+resource "netapp-ontap_cluster_peers_resource" "example" {
+ cx_profile_name = "cluster4"
+ remote = {
+ ip_addresses = ["%s"]
+ }
+ source_details = {
+ ip_addresses = ["%s"]
+ }
+ peer_cx_profile_name = "cluster3"
+ passphrase = "12345678"
+ peer_applications = ["snapmirror"]
+}`, host, admin, password, host2, admin, password, remotIP, sourceIP)
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index a1f18d07..5aebd66f 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -148,6 +148,7 @@ func (p *ONTAPProvider) Resources(ctx context.Context) []func() resource.Resourc
NewCifsLocalUserResource,
NewCifsUserGroupPrivilegeResource,
NewClusterLicensingLicenseResource,
+ NewClusterPeersResource,
NewClusterScheduleResource,
NewExampleResource,
NewExportPolicyResource,