From cbc6e833bbbc7ca21403974288deeb9a6d9bf835 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 5 May 2023 15:07:45 +0800 Subject: [PATCH 1/4] `azurerm_site_recovery_replication_recovery_plan`: support `azure_to_azure_settings` Signed-off-by: ziyeqf <51212351+ziyeqf@users.noreply.github.com> --- ...very_replication_recovery_plan_resource.go | 100 ++++++++++++++++-- ...replication_recovery_plan_resource_test.go | 46 ++++++++ ...ry_replication_recovery_plan.html.markdown | 25 ++++- 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go index 4ab848730e20..6770169d0de1 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go @@ -6,7 +6,9 @@ import ( "regexp" "time" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/edgezones" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationfabrics" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationrecoveryplans" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -19,11 +21,12 @@ import ( ) type SiteRecoveryReplicationRecoveryPlanModel struct { - Name string `tfschema:"name"` - RecoveryGroup []RecoveryGroupModel `tfschema:"recovery_group"` - RecoveryVaultId string `tfschema:"recovery_vault_id"` - SourceRecoveryFabricId string `tfschema:"source_recovery_fabric_id"` - TargetRecoveryFabricId string `tfschema:"target_recovery_fabric_id"` + Name string `tfschema:"name"` + RecoveryGroup []RecoveryGroupModel `tfschema:"recovery_group"` + RecoveryVaultId string `tfschema:"recovery_vault_id"` + SourceRecoveryFabricId string `tfschema:"source_recovery_fabric_id"` + TargetRecoveryFabricId string `tfschema:"target_recovery_fabric_id"` + A2ASettings []ReplicationRecoveryPlanA2ASpecificInputModel `tfschema:"azure_to_azure_settings"` } type RecoveryGroupModel struct { @@ -44,6 +47,13 @@ type ActionModel struct { ScriptPath string `tfschema:"script_path"` } +type ReplicationRecoveryPlanA2ASpecificInputModel struct { + PrimaryZone string `tfschema:"primary_zone"` + RecoveryZone string `tfschema:"recovery_zone"` + PrimaryEdgeZone string `tfschema:"primary_edge_zone"` + RecoveryEdgeZone string `tfschema:"recovery_edge_zone"` +} + type SiteRecoveryReplicationRecoveryPlanResource struct{} var _ sdk.ResourceWithUpdate = SiteRecoveryReplicationRecoveryPlanResource{} @@ -116,20 +126,22 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Arguments() map[string]*plu "pre_action": { Type: pluginsdk.TypeSet, Optional: true, - Elem: schemaAction(), + Elem: replicationRecoveryPlanActionSchema(), }, "post_action": { Type: pluginsdk.TypeSet, Optional: true, - Elem: schemaAction(), + Elem: replicationRecoveryPlanActionSchema(), }, }, }, }, + + "azure_to_azure_settings": replicationRecoveryPlanA2ASchema(), } } -func schemaAction() *pluginsdk.Resource { +func replicationRecoveryPlanActionSchema() *pluginsdk.Resource { return &pluginsdk.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -199,6 +211,53 @@ func schemaAction() *pluginsdk.Resource { } } +func replicationRecoveryPlanA2ASchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "primary_zone": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + RequiredWith: []string{"azure_to_azure_settings.0.recovery_zone"}, + }, + + "recovery_zone": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + RequiredWith: []string{"azure_to_azure_settings.0.primary_zone"}, + }, + + "primary_edge_zone": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + StateFunc: edgezones.StateFunc, + DiffSuppressFunc: edgezones.DiffSuppressFunc, + RequiredWith: []string{"azure_to_azure_settings.0.recovery_edge_zone"}, + }, + + "recovery_edge_zone": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + StateFunc: edgezones.StateFunc, + DiffSuppressFunc: edgezones.DiffSuppressFunc, + RequiredWith: []string{"azure_to_azure_settings.0.primary_edge_zone"}, + }, + }, + }, + } +} + func (r SiteRecoveryReplicationRecoveryPlanResource) Attributes() map[string]*schema.Schema { return map[string]*schema.Schema{} } @@ -250,6 +309,17 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Create() sdk.ResourceFunc { }, } + if model.A2ASettings != nil && len(model.A2ASettings) == 1 { + parameters.Properties.ProviderSpecificInput = pointer.To([]replicationrecoveryplans.RecoveryPlanProviderSpecificInput{ + replicationrecoveryplans.RecoveryPlanA2AInput{ + PrimaryZone: &model.A2ASettings[0].PrimaryZone, + RecoveryZone: &model.A2ASettings[0].RecoveryZone, + PrimaryExtendedLocation: expandEdgeZone(model.A2ASettings[0].PrimaryEdgeZone), + RecoveryExtendedLocation: expandEdgeZone(model.A2ASettings[0].RecoveryEdgeZone), + }, + }) + } + err = client.CreateThenPoll(ctx, id, parameters) if err != nil { return fmt.Errorf("creating site recovery replication plan %q: %+v", id, err) @@ -300,10 +370,22 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Read() sdk.ResourceFunc { if prop.RecoveryFabricId != nil { state.TargetRecoveryFabricId = handleAzureSdkForGoBug2824(*prop.RecoveryFabricId) } - if group := prop.Groups; group != nil { state.RecoveryGroup = flattenRecoveryGroups(*group) } + if details := prop.ProviderSpecificDetails; details != nil && len(*details) > 0 { + detail := pointer.From(details)[0] + if a2a, ok := detail.(replicationrecoveryplans.RecoveryPlanA2ADetails); ok { + state.A2ASettings = []ReplicationRecoveryPlanA2ASpecificInputModel{ + { + PrimaryZone: pointer.From(a2a.PrimaryZone), + RecoveryZone: pointer.From(a2a.RecoveryZone), + PrimaryEdgeZone: flattenEdgeZone(a2a.PrimaryExtendedLocation), + RecoveryEdgeZone: flattenEdgeZone(a2a.RecoveryExtendedLocation), + }, + } + } + } } return metadata.Encode(&state) diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go index 0f8e34300a65..f6537d743037 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go @@ -61,6 +61,21 @@ func TestAccSiteRecoveryReplicationRecoveryPlan_withPostActions(t *testing.T) { }) } +func TestAccSiteRecoveryReplicationRecoveryPlan_withZones(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_site_recovery_replication_recovery_plan", "test") + r := SiteRecoveryReplicationRecoveryPlan{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withZones(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (SiteRecoveryReplicationRecoveryPlan) template(data acceptance.TestData) string { tags := "" if strings.HasPrefix(strings.ToLower(data.Client().SubscriptionID), "85b3dbca") { @@ -378,6 +393,37 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } +func (r SiteRecoveryReplicationRecoveryPlan) withZones(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_site_recovery_replication_recovery_plan" "test" { + name = "acctest-%[2]d" + recovery_vault_id = azurerm_recovery_services_vault.test.id + source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id + target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id + + recovery_group { + type = "Boot" + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] + } + + recovery_group { + type = "Failover" + } + + recovery_group { + type = "Shutdown" + } + + azure_to_azure_settings { + primary_zone = "1" + recovery_zone = "2" + } +} +`, r.template(data), data.RandomInteger) +} + func (r SiteRecoveryReplicationRecoveryPlan) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := replicationrecoveryplans.ParseReplicationRecoveryPlanID(state.ID) if err != nil { diff --git a/website/docs/r/site_recovery_replication_recovery_plan.html.markdown b/website/docs/r/site_recovery_replication_recovery_plan.html.markdown index a4af67ad5dfe..627cd9df753d 100644 --- a/website/docs/r/site_recovery_replication_recovery_plan.html.markdown +++ b/website/docs/r/site_recovery_replication_recovery_plan.html.markdown @@ -3,12 +3,12 @@ subcategory: "Recovery Services" layout: "azurerm" page_title: "Azure Resource Manager: azurerm_site_recovery_replication_recovery_plan" description: |- - Manages an Azure Site Recovery Plan within a Recovery Services vault. + Manages an Site Recovery Replication Recovery Plan within a Recovery Services vault. --- # azurerm_site_recovery_replication_recovery_plan -Manages an Azure Site Recovery Plan within a Recovery Services vault. A recovery plan gathers machines into recovery groups for the purpose of failover. +Manages an Site Recovery Replication Recovery Plan within a Recovery Services vault. A recovery plan gathers machines into recovery groups for the purpose of failover. ## Example Usage @@ -78,13 +78,15 @@ The following arguments are supported: * `target_recovery_fabric_id` - (Required) ID of target fabric to recover. Changing this forces a new Replication Plan to be created. -* `recovery_group` - (Optional) Three or more `recovery_group` block. +* `recovery_group` - (Optional) Three or more `recovery_group` block defined as below. + +* `azure_to_azure_settings` - (Optional) An `azure_to_azure_settings` block defined as block. --- A `recovery_group` block supports the following: -* `type` - (Required) The Recovery Plan Group Type. Possible values are `Boot`, `Failover` and `Shutdown`. +* `type` - (Required) The Recovery Plan Group Type. Possible values are `Boot`, `Failover` and `Shutdown`. * `replicated_protected_items` - (Optional) (required) one or more id of protected VM. @@ -120,6 +122,21 @@ An `action` block supports the following: -> **NOTE:** This property is required when `type` is set to `ScriptActionDetails`. +--- + +An `azure_to_azure_settings` block supports the following: + +* `primary_zone` - (Optional) The Availability Zone in which the VM is located. Changing this forces a new Site Recovery Replication Recovery Plan to be created. + +* `recovery_zone` - (Optional) The Availability Zone in which the VM is recovered. Changing this forces a new Site Recovery Replication Recovery Plan to be created. + +-> **Note:** `primary_zone` and `recovery_zone` must be specified together. + +* `primary_edge_zone` - (Optional) The Edge Zone within the Azure Region where the VM exists. Changing this forces a new Site Recovery Replication Recovery Plan to be created. + +* `recovery_edge_zone` - (Optional) The Edge Zone within the Azure Region where the VM is recovered. Changing this forces a new Site Recovery Replication Recovery Plan to be created. + +-> **Note:** `primary_edge_zone` and `recovery_edge_zone` must be specified together. ## Attributes Reference From e1d7ec26c89a937da53c024c71553413d080957b Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Sat, 6 May 2023 10:00:19 +0800 Subject: [PATCH 2/4] add test case `TestAccSiteRecoveryReplicationRecoveryPlan_withEdgeZones` Signed-off-by: ziyeqf <51212351+ziyeqf@users.noreply.github.com> --- ...replication_recovery_plan_resource_test.go | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go index f6537d743037..250f0533c350 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource_test.go @@ -76,6 +76,21 @@ func TestAccSiteRecoveryReplicationRecoveryPlan_withZones(t *testing.T) { }) } +func TestAccSiteRecoveryReplicationRecoveryPlan_withEdgeZones(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_site_recovery_replication_recovery_plan", "test") + r := SiteRecoveryReplicationRecoveryPlan{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withEdgeZones(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (SiteRecoveryReplicationRecoveryPlan) template(data acceptance.TestData) string { tags := "" if strings.HasPrefix(strings.ToLower(data.Client().SubscriptionID), "85b3dbca") { @@ -424,6 +439,44 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { `, r.template(data), data.RandomInteger) } +func (r SiteRecoveryReplicationRecoveryPlan) withEdgeZones(data acceptance.TestData) string { + // WestUS has an edge zone available - so hard-code to that + data.Locations.Primary = "westus" + + return fmt.Sprintf(` +%s + +data "azurerm_extended_locations" "test" { + location = azurerm_resource_group.test.location +} + +resource "azurerm_site_recovery_replication_recovery_plan" "test" { + name = "acctest-%[2]d" + recovery_vault_id = azurerm_recovery_services_vault.test.id + source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id + target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id + + recovery_group { + type = "Boot" + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] + } + + recovery_group { + type = "Failover" + } + + recovery_group { + type = "Shutdown" + } + + azure_to_azure_settings { + primary_edge_zone = data.azurerm_extended_locations.test.extended_locations[0] + recovery_edge_zone = data.azurerm_extended_locations.test.extended_locations[0] + } +} +`, r.template(data), data.RandomInteger) +} + func (r SiteRecoveryReplicationRecoveryPlan) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := replicationrecoveryplans.ParseReplicationRecoveryPlanID(state.ID) if err != nil { From 0af674d9ce502de1d24ed1319708946486845acc Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 12 May 2023 10:07:12 +0800 Subject: [PATCH 3/4] update per comments Signed-off-by: ziyeqf <51212351+ziyeqf@users.noreply.github.com> --- ...very_replication_recovery_plan_resource.go | 50 ++++++++++++------- ...ry_replication_recovery_plan.html.markdown | 4 +- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go index 6770169d0de1..d5ceb43312a5 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go @@ -310,14 +310,7 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Create() sdk.ResourceFunc { } if model.A2ASettings != nil && len(model.A2ASettings) == 1 { - parameters.Properties.ProviderSpecificInput = pointer.To([]replicationrecoveryplans.RecoveryPlanProviderSpecificInput{ - replicationrecoveryplans.RecoveryPlanA2AInput{ - PrimaryZone: &model.A2ASettings[0].PrimaryZone, - RecoveryZone: &model.A2ASettings[0].RecoveryZone, - PrimaryExtendedLocation: expandEdgeZone(model.A2ASettings[0].PrimaryEdgeZone), - RecoveryExtendedLocation: expandEdgeZone(model.A2ASettings[0].RecoveryEdgeZone), - }, - }) + parameters.Properties.ProviderSpecificInput = expandA2ASettings(model.A2ASettings[0]) } err = client.CreateThenPoll(ctx, id, parameters) @@ -374,17 +367,7 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Read() sdk.ResourceFunc { state.RecoveryGroup = flattenRecoveryGroups(*group) } if details := prop.ProviderSpecificDetails; details != nil && len(*details) > 0 { - detail := pointer.From(details)[0] - if a2a, ok := detail.(replicationrecoveryplans.RecoveryPlanA2ADetails); ok { - state.A2ASettings = []ReplicationRecoveryPlanA2ASpecificInputModel{ - { - PrimaryZone: pointer.From(a2a.PrimaryZone), - RecoveryZone: pointer.From(a2a.RecoveryZone), - PrimaryEdgeZone: flattenEdgeZone(a2a.PrimaryExtendedLocation), - RecoveryEdgeZone: flattenEdgeZone(a2a.RecoveryExtendedLocation), - }, - } - } + state.A2ASettings = flattenRecoveryPlanProviderSpecficInput(details) } } @@ -518,6 +501,17 @@ func expandRecoverGroup(input []RecoveryGroupModel) ([]replicationrecoveryplans. return output, nil } +func expandA2ASettings(input ReplicationRecoveryPlanA2ASpecificInputModel) *[]replicationrecoveryplans.RecoveryPlanProviderSpecificInput { + return &[]replicationrecoveryplans.RecoveryPlanProviderSpecificInput{ + replicationrecoveryplans.RecoveryPlanA2AInput{ + PrimaryZone: pointer.To(input.PrimaryZone), + RecoveryZone: pointer.To(input.RecoveryZone), + PrimaryExtendedLocation: expandEdgeZone(input.PrimaryEdgeZone), + RecoveryExtendedLocation: expandEdgeZone(input.RecoveryEdgeZone), + }, + } +} + func validateRecoverGroup(input []RecoveryGroupModel) (bool, error) { bootCount := 0 shutdownCount := 0 @@ -625,3 +619,21 @@ func flattenRecoveryPlanActions(input *[]replicationrecoveryplans.RecoveryPlanAc } return actionOutputs } + +func flattenRecoveryPlanProviderSpecficInput(input *[]replicationrecoveryplans.RecoveryPlanProviderSpecificDetails) []ReplicationRecoveryPlanA2ASpecificInputModel { + output := make([]ReplicationRecoveryPlanA2ASpecificInputModel, 0) + for _, providerSpecificInput := range *input { + switch providerSpecificInput.(type) { + case replicationrecoveryplans.RecoveryPlanA2AInput: + a2aInput := providerSpecificInput.(replicationrecoveryplans.RecoveryPlanA2AInput) + o := ReplicationRecoveryPlanA2ASpecificInputModel{ + PrimaryZone: pointer.From(a2aInput.PrimaryZone), + RecoveryZone: pointer.From(a2aInput.RecoveryZone), + PrimaryEdgeZone: flattenEdgeZone(a2aInput.PrimaryExtendedLocation), + RecoveryEdgeZone: flattenEdgeZone(a2aInput.RecoveryExtendedLocation), + } + output = append(output, o) + } + } + return output +} diff --git a/website/docs/r/site_recovery_replication_recovery_plan.html.markdown b/website/docs/r/site_recovery_replication_recovery_plan.html.markdown index 627cd9df753d..00abeb64403b 100644 --- a/website/docs/r/site_recovery_replication_recovery_plan.html.markdown +++ b/website/docs/r/site_recovery_replication_recovery_plan.html.markdown @@ -3,12 +3,12 @@ subcategory: "Recovery Services" layout: "azurerm" page_title: "Azure Resource Manager: azurerm_site_recovery_replication_recovery_plan" description: |- - Manages an Site Recovery Replication Recovery Plan within a Recovery Services vault. + Manages a Site Recovery Replication Recovery Plan within a Recovery Services vault. --- # azurerm_site_recovery_replication_recovery_plan -Manages an Site Recovery Replication Recovery Plan within a Recovery Services vault. A recovery plan gathers machines into recovery groups for the purpose of failover. +Manages a Site Recovery Replication Recovery Plan within a Recovery Services vault. A recovery plan gathers machines into recovery groups for the purpose of failover. ## Example Usage From adc51859a78e23f196037c7d094ff83a5c387b47 Mon Sep 17 00:00:00 2001 From: ziyeqf <51212351+ziyeqf@users.noreply.github.com> Date: Fri, 12 May 2023 14:05:43 +0800 Subject: [PATCH 4/4] golint Signed-off-by: ziyeqf <51212351+ziyeqf@users.noreply.github.com> --- .../site_recovery_replication_recovery_plan_resource.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go index d5ceb43312a5..8a76d0046d76 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go @@ -623,9 +623,7 @@ func flattenRecoveryPlanActions(input *[]replicationrecoveryplans.RecoveryPlanAc func flattenRecoveryPlanProviderSpecficInput(input *[]replicationrecoveryplans.RecoveryPlanProviderSpecificDetails) []ReplicationRecoveryPlanA2ASpecificInputModel { output := make([]ReplicationRecoveryPlanA2ASpecificInputModel, 0) for _, providerSpecificInput := range *input { - switch providerSpecificInput.(type) { - case replicationrecoveryplans.RecoveryPlanA2AInput: - a2aInput := providerSpecificInput.(replicationrecoveryplans.RecoveryPlanA2AInput) + if a2aInput, ok := providerSpecificInput.(replicationrecoveryplans.RecoveryPlanA2ADetails); ok { o := ReplicationRecoveryPlanA2ASpecificInputModel{ PrimaryZone: pointer.From(a2aInput.PrimaryZone), RecoveryZone: pointer.From(a2aInput.RecoveryZone),