From d7358c0f7be92c5e8ed83ca1e9f17d5d7ba80c9f Mon Sep 17 00:00:00 2001 From: Oliver Krause <3621164+okrause@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:18:20 +0100 Subject: [PATCH] Add volume replication support for Google Cloud NetApp Volumes (#9816) * Initial replication commit * Cleanup work - Renamed a lot of files to make clear which resource the belong to - Updated documentation for resource fields - Renamed a few resource fields and changed some types - Disabled the custom code for now. Needs to be discussed first * Update example file * Updated example file * Major updates - Reorganisation of block - Reorganisation of fields to match API documentation - Updated example parameters - Added missing API fields - Improved descriptions - * For replication deletion, stop replication first * Add support for deleting destination volume on replication delete * Make volumes deletable in presence of snapshots. This change will be PRed for volume resource independently. Adding it here while it is not in main. * Improving debug error message * yaml check and format fix * Add wait for mirror to initialize. Required to run destroy shortly after create. * Wait on destroy, not on create * Make deleting a replication more robust - doc improvements - started to implement stop/resume. More work required. - renamed a few files to better reflect what they are good for * adding support for stop/resume * yamlformat and lint * Add force delete to delete volumes with nested snapshots * resource test first version * More changes to make tests solid - Introduced new parameter to wait for mirror_status==MIRRORED - more mirror state reconciliation * Test updates * few cleanups * Make virtual field verifies happy * Minor test improvements * More fine tuning - Remove merge conflict in volume.yaml - make generated test work - make output field work - ignore_read for virtual fields * Resource name change as suggested by @slevenick * Remove snapshot code block and fix typo * Detect manual stop/resume actions * Remove ignore_read for deletion_policy * - Made destinationVolumeParameters immutable. It still requires ignore_read. - removed ignore_read from virtual_fields * destinationVolumeParameters are only evaluated at create. Make the immutable. * Name cleanups and comment improvements * removed comment Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> * tabs to spaces in resource block Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> * Updates to address review comments - make wait_for_mirror also work for stop/resume, additionally to create - convert tabs in test resource blocks to spaces - fix typos * Rewording of comments Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> --------- Co-authored-by: G-NamanGupta Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> --- mmv1/products/netapp/VolumeReplication.yaml | 289 +++++++++++++++++ .../netapp_volume_replication.go.erb | 51 +++ ...tapp_volume_replicaton_mirror_state.go.erb | 20 ++ .../netapp_volume_replication_create.tf.erb | 54 ++++ ...tapp_volume_replication_post_create.go.erb | 7 + ...plication_delete_destination_volume.go.erb | 34 ++ ...app_volume_replication_mirror_state.go.erb | 89 +++++ ...lume_replication_stop_before_delete.go.erb | 33 ++ ...resource_netapp_volume_replication_test.go | 306 ++++++++++++++++++ 9 files changed, 883 insertions(+) create mode 100644 mmv1/products/netapp/VolumeReplication.yaml create mode 100644 mmv1/templates/terraform/constants/netapp_volume_replication.go.erb create mode 100644 mmv1/templates/terraform/custom_flatten/netapp_volume_replicaton_mirror_state.go.erb create mode 100644 mmv1/templates/terraform/examples/netapp_volume_replication_create.tf.erb create mode 100644 mmv1/templates/terraform/post_create/netapp_volume_replication_post_create.go.erb create mode 100644 mmv1/templates/terraform/post_delete/netapp_volume_replication_delete_destination_volume.go.erb create mode 100644 mmv1/templates/terraform/post_update/netapp_volume_replication_mirror_state.go.erb create mode 100644 mmv1/templates/terraform/pre_delete/netapp_volume_replication_stop_before_delete.go.erb create mode 100644 mmv1/third_party/terraform/services/netapp/resource_netapp_volume_replication_test.go diff --git a/mmv1/products/netapp/VolumeReplication.yaml b/mmv1/products/netapp/VolumeReplication.yaml new file mode 100644 index 000000000000..dd7dde9c7f32 --- /dev/null +++ b/mmv1/products/netapp/VolumeReplication.yaml @@ -0,0 +1,289 @@ +# Copyright 2023 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +!ruby/object:Api::Resource +name: "VolumeReplication" +description: | + Volume replication creates an asynchronous mirror of a volume in a different location. This capability + lets you use the replicated volume for critical application activity in case of a location-wide outage + or disaster. + + A new destination volume is created as part of the replication resource. It's content is updated on a + schedule with content of the source volume. It can be used as a read-only copy while the mirror is + enabled, or as an independent read-write volume while the mirror is stopped. A destination volume will + also contain the snapshots of the source volume. Resuming a mirror will overwrite all changes on the + destination volume with the content of the source volume. While is mirror is enabled, all configuration + changes done to source or destination volumes are automatically done to both. Please note that the + destination volume is not a resource managed by Terraform. + + Reversing the replication direction is not supported through the provider. +references: !ruby/object:Api::Resource::ReferenceLinks + guides: + "Documentation": "https://cloud.google.com/netapp/volumes/docs/protect-data/about-volume-replication" + api: "https://cloud.google.com/netapp/volumes/docs/reference/rest/v1/projects.locations.volumes.replications" +base_url: projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications +self_link: projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}} +create_url: projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications?replicationId={{name}} +update_url: projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}} +update_verb: :PATCH +update_mask: true +autogen_async: true +async: !ruby/object:Api::OpAsync + operation: !ruby/object:Api::OpAsync::Operation + base_url: "{{op_id}}" +id_format: "projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}" +import_format: + [ + "projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}", + ] +examples: + - !ruby/object:Provider::Terraform::Examples + name: "netapp_volume_replication_create" + primary_resource_id: "test_replication" + vars: + source_pool_name: "source-pool" + destination_pool_name: "destination-pool" + volume_name: "source-volume" + replication_name: "test-replication" + destination_volume: "destination-volume" + network_name: "test-network" + ignore_read_extra: + - "delete_destination_volume" + - "replication_enabled" + - "force_stopping" + - "wait_for_mirror" + test_vars_overrides: + network_name: 'acctest.BootstrapSharedServiceNetworkingConnection(t, "gcnv-network-config-1", acctest.ServiceNetworkWithParentService("netapp.servicenetworking.goog"))' +parameters: + - !ruby/object:Api::Type::String + name: "location" + required: true + immutable: true + url_param_only: true + description: | + Name of region for this resource. The resource needs to be created in the region of the destination volume. + - !ruby/object:Api::Type::String + name: "volumeName" + description: The name of the existing source volume. + required: true + immutable: true + url_param_only: true + - !ruby/object:Api::Type::String + name: "name" + description: The name of the replication. Needs to be unique per location. + required: true + immutable: true + url_param_only: true +properties: + - !ruby/object:Api::Type::Enum + name: "state" + description: | + Indicates the state of replication resource. State of the mirror itself is indicated in mirrorState. + values: + - :STATE_UNSPECIFIED + - :CREATING + - :READY + - :UPDATING + - :DELETING + - :ERROR + output: true + - !ruby/object:Api::Type::String + name: "stateDetails" + description: | + State details of the replication resource. + output: true + - !ruby/object:Api::Type::Enum + name: "role" + description: | + Reverting a replication can swap source and destination volume roles. This field indicates if the `location` hosts + the source or destination volume. For resume and revert and resume operations it is critical to understand + which volume is the source volume, since it will overwrite changes done to the destination volume. + values: + - :REPLICATION_ROLE_UNSPECIFIED + - :SOURCE + - :DESTINATION + output: true + - !ruby/object:Api::Type::Enum + name: "replicationSchedule" + description: | + Specifies the replication interval. + values: + - :EVERY_10_MINUTES + - :HOURLY + - :DAILY + required: true + - !ruby/object:Api::Type::Enum + name: "mirrorState" + description: | + Indicates the state of the mirror between source and destination volumes. Depending on the amount of data + in your source volume, PREPARING phase can take hours or days. mirrorState = MIRRORED indicates your baseline + transfer ended and destination volume became accessible read-only. TRANSFERRING means a MIRRORED volume + currently receives an update. Updated every 5 minutes. + values: + - :MIRROR_STATE_UNSPECIFIED + - :PREPARING + - :MIRRORED + - :STOPPED + - :TRANSFERRING + custom_flatten: templates/terraform/custom_flatten/netapp_volume_replicaton_mirror_state.go.erb + output: true + - !ruby/object:Api::Type::String + name: "createTime" + description: | + Create time of the active directory. A timestamp in RFC3339 UTC "Zulu" format. Examples: "2023-06-22T09:13:01.617Z". + output: true + - !ruby/object:Api::Type::String + name: "destinationVolume" + description: | + Full resource name of destination volume with format: `projects/{{project}}/locations/{{location}}/volumes/{{volumeId}}` + output: true + - !ruby/object:Api::Type::NestedObject + name: "transferStats" + description: |- + Replication transfer statistics. All statistics are updated every 5 minutes. + output: true + properties: + - !ruby/object:Api::Type::String + name: "transferBytes" + description: | + Number of bytes transferred so far in current transfer. + output: true + - !ruby/object:Api::Type::String + name: "totalTransferDuration" + description: | + Total time taken so far during current transfer. + output: true + - !ruby/object:Api::Type::String + name: "lastTransferBytes" + description: | + Size of last completed transfer in bytes. + output: true + - !ruby/object:Api::Type::String + name: "lastTransferDuration" + description: | + Time taken during last completed transfer. + output: true + - !ruby/object:Api::Type::String + name: "lagDuration" + description: | + The elapsed time since the creation of the snapshot on the source volume that was last replicated + to the destination volume. Lag time represents the difference in age of the destination volume + data in relation to the source volume data. + output: true + - !ruby/object:Api::Type::String + name: "updateTime" + description: | + Time when progress was updated last. A timestamp in RFC3339 UTC "Zulu" format. Examples: "2023-06-22T09:13:01.617Z". + output: true + - !ruby/object:Api::Type::String + name: "lastTransferEndTime" + description: | + Time when last transfer completed. A timestamp in RFC3339 UTC "Zulu" format. Examples: "2023-06-22T09:13:01.617Z". + output: true + - !ruby/object:Api::Type::String + name: "lastTransferError" + description: | + A message describing the cause of the last transfer failure. + output: true + - !ruby/object:Api::Type::KeyValueLabels + name: "labels" + description: | + Labels as key value pairs. Example: `{ "owner": "Bob", "department": "finance", "purpose": "testing" }` + - !ruby/object:Api::Type::NestedObject + name: "destinationVolumeParameters" + description: |- + Destination volume parameters. + # destinationVolumeParameters is only used on CREATE. Will not be returned on READ. + ignore_read: true + properties: + - !ruby/object:Api::Type::String + name: "storagePool" + description: | + Name of an existing storage pool for the destination volume with format: `projects/{{project}}/locations/{{location}}/storagePools/{{poolId}}` + immutable: true + required: true + - !ruby/object:Api::Type::String + name: "volumeId" + description: | + Name for the destination volume to be created. If not specified, the name of the source volume will be used. + immutable: true + default_from_api: true + - !ruby/object:Api::Type::String + name: "shareName" + description: | + Share name for destination volume. If not specified, name of source volume's share name will be used. + immutable: true + default_from_api: true + - !ruby/object:Api::Type::String + name: "description" + description: | + Description for the destination volume. + immutable: true + - !ruby/object:Api::Type::String + name: "sourceVolume" + description: | + Full resource name of source volume with format: `projects/{{project}}/locations/{{location}}/volumes/{{volumeId}}` + output: true + - !ruby/object:Api::Type::Boolean + name: "healthy" + description: | + Condition of the relationship. Can be one of the following: + - true: The replication relationship is healthy. It has not missed the most recent scheduled transfer. + - false: The replication relationship is not healthy. It has missed the most recent scheduled transfer. + output: true + - !ruby/object:Api::Type::String + name: "description" + description: | + An description of this resource. +virtual_fields: + - !ruby/object:Api::Type::Boolean + name: "delete_destination_volume" + description: | + A destination volume is created as part of replication creation. The destination volume will not became + under Terraform management unless you import it manually. If you delete the replication, this volume + will remain. + Setting this parameter to true will delete the *current* destination volume when destroying the + replication. If you reversed the replication direction, this will be your former source volume! + For production use, it is recommended to keep this parameter false to avoid accidental volume + deletion. Handle with care. Default is false. + default_value: false + - !ruby/object:Api::Type::Boolean + name: "replication_enabled" + description: | + Set to false to stop/break the mirror. Stopping the mirror makes the destination volume read-write + and act independently from the source volume. + Set to true to enable/resume the mirror. WARNING: Resuming a mirror overwrites any changes + done to the destination volume with the content of the source volume. + default_value: true + - !ruby/object:Api::Type::Boolean + name: "force_stopping" + description: | + Only replications with mirror_state=MIRRORED can be stopped. A replication in mirror_state=TRANSFERRING + currently receives an update and stopping the update might be undesirable. Set this parameter to true + to stop anyway. All data transferred to the destination will be discarded and content of destination + volume will remain at the state of the last successful update. Default is false. + default_value: false + - !ruby/object:Api::Type::Boolean + name: "wait_for_mirror" + description: | + Replication resource state is independent of mirror_state. With enough data, it can take many hours + for mirror_state to reach MIRRORED. If you want Terraform to wait for the mirror to finish on + create/stop/resume operations, set this parameter to true. Default is false. + default_value: false +custom_code: !ruby/object:Provider::Terraform::CustomCode + constants: templates/terraform/constants/netapp_volume_replication.go.erb + post_create: templates/terraform/post_create/netapp_volume_replication_post_create.go.erb + pre_delete: templates/terraform/pre_delete/netapp_volume_replication_stop_before_delete.go.erb + post_delete: templates/terraform/post_delete/netapp_volume_replication_delete_destination_volume.go.erb + post_update: templates/terraform/post_update/netapp_volume_replication_mirror_state.go.erb diff --git a/mmv1/templates/terraform/constants/netapp_volume_replication.go.erb b/mmv1/templates/terraform/constants/netapp_volume_replication.go.erb new file mode 100644 index 000000000000..10ec7b806253 --- /dev/null +++ b/mmv1/templates/terraform/constants/netapp_volume_replication.go.erb @@ -0,0 +1,51 @@ +// Custom function to wait for mirrorState target states +func NetAppVolumeReplicationWaitForMirror(d *schema.ResourceData, meta interface{}, targetState string) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{NetappBasePath}}projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for volume replication: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + for { + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("NetappVolumeReplication %q", d.Id())) + } + + log.Printf("[DEBUG] waiting for mirrorState. actual: %v, target: %v", res["mirrorState"], targetState) + + if res["mirrorState"] == targetState { + break + } + + time.Sleep(30 * time.Second) + // This method can potentially run for days, e.g. when setting up a replication for a source volume + // with dozens of TiB of data. Timeout handling yes/no? + } + + return nil +} \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_flatten/netapp_volume_replicaton_mirror_state.go.erb b/mmv1/templates/terraform/custom_flatten/netapp_volume_replicaton_mirror_state.go.erb new file mode 100644 index 000000000000..90881e839b6e --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/netapp_volume_replicaton_mirror_state.go.erb @@ -0,0 +1,20 @@ +func flatten<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Actual state of replication_enabled depends on mirrorState. let's update it. + // This is to pickup manual user STOP/RESUME operations on the replication. + if v == nil { + return v + } + + if v.(string) == "STOPPED" { + if err := d.Set("replication_enabled", false); err != nil { + return fmt.Errorf("Error setting replication_enabled: %s", err) + } + } else { + if err := d.Set("replication_enabled", true); err != nil { + return fmt.Errorf("Error setting replication_enabled: %s", err) + } + } + log.Printf("[DEBUG] value of replication_state : %v", d.Get("replication_enabled")) + + return v +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/netapp_volume_replication_create.tf.erb b/mmv1/templates/terraform/examples/netapp_volume_replication_create.tf.erb new file mode 100644 index 000000000000..07b69c542b2b --- /dev/null +++ b/mmv1/templates/terraform/examples/netapp_volume_replication_create.tf.erb @@ -0,0 +1,54 @@ + +data "google_compute_network" "default" { + name = "<%= ctx[:vars]['network_name'] %>" +} + +resource "google_netapp_storage_pool" "source_pool" { + name = "<%= ctx[:vars]['source_pool_name'] %>" + location = "us-central1" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_storage_pool" "destination_pool" { + name = "<%= ctx[:vars]['destination_pool_name'] %>" + location = "us-west2" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_volume" "source_volume" { + location = google_netapp_storage_pool.source_pool.location + name = "<%= ctx[:vars]['volume_name'] %>" + capacity_gib = 100 + share_name = "<%= ctx[:vars]['volume_name'] %>" + storage_pool = google_netapp_storage_pool.source_pool.name + protocols = [ + "NFSV3" + ] + deletion_policy = "FORCE" +} + +resource "google_netapp_volume_replication" "<%= ctx[:primary_resource_id] %>" { + depends_on = [google_netapp_volume.source_volume] + location = google_netapp_volume.source_volume.location + volume_name = google_netapp_volume.source_volume.name + name = "<%= ctx[:vars]['replication_name'] %>" + replication_schedule = "EVERY_10_MINUTES" + description = "This is a replication resource" + destination_volume_parameters { + storage_pool = google_netapp_storage_pool.destination_pool.id + volume_id = "<%= ctx[:vars]['destination_volume'] %>" + # Keeping the share_name of source and destination the same + # simplifies implementing client failover concepts + share_name = "<%= ctx[:vars]['volume_name'] %>" + description = "This is a replicated volume" + } + # WARNING: Setting delete_destination_volume to true, will delete the + # CURRENT destination volume if the replication is deleted. Omit the field + # or set delete_destination_volume=false to avoid accidental volume deletion. + delete_destination_volume = true + wait_for_mirror = true +} diff --git a/mmv1/templates/terraform/post_create/netapp_volume_replication_post_create.go.erb b/mmv1/templates/terraform/post_create/netapp_volume_replication_post_create.go.erb new file mode 100644 index 000000000000..b2d44a9c063e --- /dev/null +++ b/mmv1/templates/terraform/post_create/netapp_volume_replication_post_create.go.erb @@ -0,0 +1,7 @@ +if d.Get("wait_for_mirror").(bool) == true { + // Wait for mirrorState=MIRRORED before treating the resource as created + err = NetAppVolumeReplicationWaitForMirror(d, meta, "MIRRORED") + if err != nil { + return fmt.Errorf("Error waiting for volume replication to reach mirror_state==MIRRORED: %s", err) + } +} \ No newline at end of file diff --git a/mmv1/templates/terraform/post_delete/netapp_volume_replication_delete_destination_volume.go.erb b/mmv1/templates/terraform/post_delete/netapp_volume_replication_delete_destination_volume.go.erb new file mode 100644 index 000000000000..5a14f1fb407c --- /dev/null +++ b/mmv1/templates/terraform/post_delete/netapp_volume_replication_delete_destination_volume.go.erb @@ -0,0 +1,34 @@ +// A replication CREATE also created a destination volume +// A user can chooses to delete the destination volume after deleting the replication +if d.Get("delete_destination_volume").(bool) == true { + log.Printf("[DEBUG] delete_destination_volume is true. Deleting destination volume %v", d.Get("destination_volume")) + destination_volume := d.Get("destination_volume").(string) + del_url, err := tpgresource.ReplaceVars(d, config, "{{NetappBasePath}}"+destination_volume+"?force=true") + if err != nil { + return err + } + + var obj map[string]interface{} + res_del, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: del_url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "Volume") + } + + err = NetappOperationWaitTime( + config, res_del, project, "Deleting destination volume", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting destination Volume %q: %#v", destination_volume, res_del) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/post_update/netapp_volume_replication_mirror_state.go.erb b/mmv1/templates/terraform/post_update/netapp_volume_replication_mirror_state.go.erb new file mode 100644 index 000000000000..57a7146702e5 --- /dev/null +++ b/mmv1/templates/terraform/post_update/netapp_volume_replication_mirror_state.go.erb @@ -0,0 +1,89 @@ +// Manage stopping and resuming a mirror + +var obj2 map[string]interface{} +do_change := false +var action string +var targetState string +// state transitions +// there can be a glitch is a transfer starts/ends between reading mirrorState +// and sending the action. This will be very rare. No workaround. +if d.Get("replication_enabled").(bool) == true { + switch d.Get("mirror_state").(string) { + case "STOPPED": + // replication_enabled==true, mirrorState==STOPPED -> resume + action = "resume" + targetState = "MIRRORED" + do_change = true + default: + // replication_enabled==true, mirrorState!=STOPPED -> NOOP + do_change = false + } +} else { + switch d.Get("mirror_state").(string) { + case "MIRRORED": + // replication_enabled==false, mirrorState==MIRRORED -> stop + action = "stop" + targetState = "STOPPED" + do_change = true + case "TRANSFERRING": + // replication_enabled==false, mirrorState==TRANSFERRING -> force stop + // User needs to add force_stopping = true, otherwise will receive error + action = "stop" + targetState = "STOPPED" + do_change = true + case "PREPARING": + // replication_enabled==false, mirrorState==PREPARING -> stop + // Currently cannot be stopped. User will receive following error: + // Error code 3, message: invalid request error: "Replication in preparing state. Please wait until replication is in 'READY' STATE and try again later.". + // User needs to wait until mirrorState=MIRRORED + action = "stop" + targetState = "STOPPED" + do_change = true + default: + // replication_enabled==false, mirrorState==STOPPED -> NOOP + do_change = false + } + + if do_change == true && d.Get("force_stopping").(bool) == true { + obj2 = map[string]interface{}{ + "force": true, + } + } +} + +if do_change { + // We need to send STOP/RESUME API calls + rawurl, err := tpgresource.ReplaceVars(d, config, "{{NetappBasePath}}projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}:"+action) + if err != nil { + return err + } + + res2, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: rawurl, + UserAgent: userAgent, + Body: obj2, + Timeout: d.Timeout(schema.TimeoutUpdate), + }) + if err != nil { + return fmt.Errorf("Error stopping/resuming replication %q: %s", d.Id(), err) + } + + err = NetappOperationWaitTime( + config, res2, project, "volume replication "+action, userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + // If user specified to wait for mirror operations, wait to reach target state + if d.Get("wait_for_mirror").(bool) == true { + err = NetAppVolumeReplicationWaitForMirror(d, meta, targetState) + if err != nil { + return fmt.Errorf("Error waiting for volume replication to reach mirror_state==%s: %s", targetState, err) + } + } +} diff --git a/mmv1/templates/terraform/pre_delete/netapp_volume_replication_stop_before_delete.go.erb b/mmv1/templates/terraform/pre_delete/netapp_volume_replication_stop_before_delete.go.erb new file mode 100644 index 000000000000..4e5c188acf2a --- /dev/null +++ b/mmv1/templates/terraform/pre_delete/netapp_volume_replication_stop_before_delete.go.erb @@ -0,0 +1,33 @@ +// A replication can only be deleted if mirrorState==STOPPED +// We are about to delete the replication and need to stop the mirror before. +// FYI: Stopping a PREPARING mirror currently doesn't work. User have to wait until +// mirror reaches MIRRORED. +if d.Get("mirror_state") != "STOPPED" { + rawurl, err := tpgresource.ReplaceVars(d, config, "{{NetappBasePath}}projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}:stop") + if err != nil { + return err + } + + reso, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: rawurl, + UserAgent: userAgent, + // We delete anyway, so lets always use force stop + Body: map[string]interface{}{ + "force": true, + }, + Timeout: d.Timeout(schema.TimeoutUpdate), + }) + if err != nil { + return fmt.Errorf("Error stopping volume replication %q before deleting it: %s", d.Id(), err) + } + + err = NetappOperationWaitTime( + config, reso, project, "Deleting volume replication", userAgent, + d.Timeout(schema.TimeoutDelete)) + if err != nil { + return err + } +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/services/netapp/resource_netapp_volume_replication_test.go b/mmv1/third_party/terraform/services/netapp/resource_netapp_volume_replication_test.go new file mode 100644 index 000000000000..e663c41e7274 --- /dev/null +++ b/mmv1/third_party/terraform/services/netapp/resource_netapp_volume_replication_test.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package netapp_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccNetappVolumeReplication_netappVolumeReplicationCreateExample_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "network_name": acctest.BootstrapSharedServiceNetworkingConnection(t, "gcnv-network-config-1", acctest.ServiceNetworkWithParentService("netapp.servicenetworking.goog")), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckNetappVolumeReplicationDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_basic(context), + }, + { + ResourceName: "google_netapp_volume_replication.test_replication", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"destination_volume_parameters", "location", "volume_name", "name", "delete_destination_volume", "replication_enabled", "force_stopping", "wait_for_mirror", "labels", "terraform_labels"}, + }, + { + Config: testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_stop(context), + }, + { + ResourceName: "google_netapp_volume_replication.test_replication", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"destination_volume_parameters", "location", "volume_name", "name", "delete_destination_volume", "replication_enabled", "force_stopping", "wait_for_mirror", "labels", "terraform_labels"}, + }, + { + Config: testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_resume(context), + }, + { + ResourceName: "google_netapp_volume_replication.test_replication", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"destination_volume_parameters", "location", "volume_name", "name", "delete_destination_volume", "replication_enabled", "force_stopping", "wait_for_mirror", "labels", "terraform_labels"}, + }, + { + Config: testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_update(context), + }, + { + ResourceName: "google_netapp_volume_replication.test_replication", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"destination_volume_parameters", "location", "volume_name", "name", "delete_destination_volume", "replication_enabled", "force_stopping", "wait_for_mirror", "labels", "terraform_labels"}, + }, + }, + }) +} + +// Basic replication +func testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_network" "default" { + name = "%{network_name}" +} + +resource "google_netapp_storage_pool" "source_pool" { + name = "tf-test-source-pool%{random_suffix}" + location = "us-central1" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_storage_pool" "destination_pool" { + name = "tf-test-destination-pool%{random_suffix}" + location = "us-west2" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_volume" "source_volume" { + location = google_netapp_storage_pool.source_pool.location + name = "tf-test-source-volume%{random_suffix}" + capacity_gib = 100 + share_name = "tf-test-source-volume%{random_suffix}" + storage_pool = google_netapp_storage_pool.source_pool.name + protocols = [ + "NFSV3" + ] + deletion_policy = "FORCE" +} + +resource "google_netapp_volume_replication" "test_replication" { + depends_on = [google_netapp_volume.source_volume] + location = google_netapp_volume.source_volume.location + volume_name = google_netapp_volume.source_volume.name + name = "tf-test-test-replication%{random_suffix}" + replication_schedule = "EVERY_10_MINUTES" + destination_volume_parameters { + storage_pool = google_netapp_storage_pool.destination_pool.id + volume_id = "tf-test-destination-volume%{random_suffix}" + # Keeping the share_name of source and destination the same + # simplifies implementing client failover concepts + share_name = "tf-test-source-volume%{random_suffix}" + description = "This is a replicated volume" + } + delete_destination_volume = true + wait_for_mirror = true +} +`, context) +} + +// Update parameters +func testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_update(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_network" "default" { + name = "%{network_name}" +} + +resource "google_netapp_storage_pool" "source_pool" { + name = "tf-test-source-pool%{random_suffix}" + location = "us-central1" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_storage_pool" "destination_pool" { + name = "tf-test-destination-pool%{random_suffix}" + location = "us-west2" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_volume" "source_volume" { + location = google_netapp_storage_pool.source_pool.location + name = "tf-test-source-volume%{random_suffix}" + capacity_gib = 100 + share_name = "tf-test-source-volume%{random_suffix}" + storage_pool = google_netapp_storage_pool.source_pool.name + protocols = [ + "NFSV3" + ] + deletion_policy = "FORCE" +} + +resource "google_netapp_volume_replication" "test_replication" { + depends_on = [google_netapp_volume.source_volume] + location = google_netapp_volume.source_volume.location + volume_name = google_netapp_volume.source_volume.name + name = "tf-test-test-replication%{random_suffix}" + replication_schedule = "EVERY_10_MINUTES" + description = "This is a replication resource" + labels = { + key = "test" + value = "replication" + } + destination_volume_parameters { + storage_pool = google_netapp_storage_pool.destination_pool.id + volume_id = "tf-test-destination-volume%{random_suffix}" + # Keeping the share_name of source and destination the same + # simplifies implementing client failover concepts + share_name = "tf-test-source-volume%{random_suffix}" + description = "This is a replicated volume" + } + replication_enabled = true + delete_destination_volume = true + force_stopping = true + wait_for_mirror = true +} +`, context) +} + +// Stop replication +func testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_stop(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_network" "default" { + name = "%{network_name}" +} + +resource "google_netapp_storage_pool" "source_pool" { + name = "tf-test-source-pool%{random_suffix}" + location = "us-central1" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_storage_pool" "destination_pool" { + name = "tf-test-destination-pool%{random_suffix}" + location = "us-west2" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_volume" "source_volume" { + location = google_netapp_storage_pool.source_pool.location + name = "tf-test-source-volume%{random_suffix}" + capacity_gib = 100 + share_name = "tf-test-source-volume%{random_suffix}" + storage_pool = google_netapp_storage_pool.source_pool.name + protocols = [ + "NFSV3" + ] + deletion_policy = "FORCE" +} + +resource "google_netapp_volume_replication" "test_replication" { + depends_on = [google_netapp_volume.source_volume] + location = google_netapp_volume.source_volume.location + volume_name = google_netapp_volume.source_volume.name + name = "tf-test-test-replication%{random_suffix}" + replication_schedule = "EVERY_10_MINUTES" + description = "This is a replication resource" + labels = { + key = "test" + value = "replication2" + } + destination_volume_parameters { + storage_pool = google_netapp_storage_pool.destination_pool.id + volume_id = "tf-test-destination-volume%{random_suffix}" + # Keeping the share_name of source and destination the same + # simplifies implementing client failover concepts + share_name = "tf-test-source-volume%{random_suffix}" + description = "This is a replicated volume" + } + replication_enabled = false + delete_destination_volume = true + force_stopping = true + wait_for_mirror = true +} +`, context) +} + +// resume replication +func testAccNetappVolumeReplication_netappVolumeReplicationCreateExample_resume(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_network" "default" { + name = "%{network_name}" +} + +resource "google_netapp_storage_pool" "source_pool" { + name = "tf-test-source-pool%{random_suffix}" + location = "us-central1" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_storage_pool" "destination_pool" { + name = "tf-test-destination-pool%{random_suffix}" + location = "us-west2" + service_level = "PREMIUM" + capacity_gib = 2048 + network = data.google_compute_network.default.id +} + +resource "google_netapp_volume" "source_volume" { + location = google_netapp_storage_pool.source_pool.location + name = "tf-test-source-volume%{random_suffix}" + capacity_gib = 100 + share_name = "tf-test-source-volume%{random_suffix}" + storage_pool = google_netapp_storage_pool.source_pool.name + protocols = [ + "NFSV3" + ] + deletion_policy = "FORCE" +} + +resource "google_netapp_volume_replication" "test_replication" { + depends_on = [google_netapp_volume.source_volume] + location = google_netapp_volume.source_volume.location + volume_name = google_netapp_volume.source_volume.name + name = "tf-test-test-replication%{random_suffix}" + replication_schedule = "HOURLY" + description = "This is a replication resource" + labels = { + key = "test" + value = "replication2" + } + destination_volume_parameters { + storage_pool = google_netapp_storage_pool.destination_pool.id + volume_id = "tf-test-destination-volume%{random_suffix}" + # Keeping the share_name of source and destination the same + # simplifies implementing client failover concepts + share_name = "tf-test-source-volume%{random_suffix}" + description = "This is a replicated volume" + } + replication_enabled = true + delete_destination_volume = true + force_stopping = true + wait_for_mirror = true +} +`, context) +}