diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source.go index 422a71fb687f..58a40e5b047b 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationrecoveryplans" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -18,6 +19,40 @@ import ( type SiteRecoveryReplicationRecoveryPlanDataSource struct{} +type SiteRecoveryReplicationRecoveryPlanDataSourceModel struct { + Name string `tfschema:"name"` + RecoveryGroup []RecoveryGroupDataSourceModel `tfschema:"recovery_group"` + RecoveryVaultId string `tfschema:"recovery_vault_id"` + SourceRecoveryFabricId string `tfschema:"source_recovery_fabric_id"` + TargetRecoveryFabricId string `tfschema:"target_recovery_fabric_id"` + A2ASettings []ReplicationRecoveryPlanA2ASpecificInputDataSourceModel `tfschema:"azure_to_azure_settings"` +} + +type RecoveryGroupDataSourceModel struct { + GroupType string `tfschema:"type"` + PostAction []ActionDataSourceModel `tfschema:"post_action"` + PreAction []ActionDataSourceModel `tfschema:"pre_action"` + ReplicatedProtectedItems []string `tfschema:"replicated_protected_items"` +} + +type ActionDataSourceModel struct { + ActionDetailType string `tfschema:"type"` + FabricLocation string `tfschema:"fabric_location"` + FailOverDirections []string `tfschema:"fail_over_directions"` + FailOverTypes []string `tfschema:"fail_over_types"` + ManualActionInstruction string `tfschema:"manual_action_instruction"` + Name string `tfschema:"name"` + RunbookId string `tfschema:"runbook_id"` + ScriptPath string `tfschema:"script_path"` +} + +type ReplicationRecoveryPlanA2ASpecificInputDataSourceModel struct { + PrimaryZone string `tfschema:"primary_zone"` + RecoveryZone string `tfschema:"recovery_zone"` + PrimaryEdgeZone string `tfschema:"primary_edge_zone"` + RecoveryEdgeZone string `tfschema:"recovery_edge_zone"` +} + var _ sdk.DataSource = SiteRecoveryReplicationRecoveryPlanDataSource{} func (r SiteRecoveryReplicationRecoveryPlanDataSource) ResourceType() string { @@ -63,7 +98,7 @@ func (r SiteRecoveryReplicationRecoveryPlanDataSource) Read() sdk.ResourceFunc { return fmt.Errorf("making Read request on site recovery replication plan %s : model is nil", id.String()) } - state := SiteRecoveryReplicationRecoveryPlanModel{ + state := SiteRecoveryReplicationRecoveryPlanDataSourceModel{ Name: id.ReplicationRecoveryPlanName, RecoveryVaultId: vaultId.ID(), } @@ -77,11 +112,11 @@ func (r SiteRecoveryReplicationRecoveryPlanDataSource) Read() sdk.ResourceFunc { } if group := prop.Groups; group != nil { - state.RecoveryGroup = flattenRecoveryGroups(*group) + state.RecoveryGroup = flattenDataSourceRecoveryGroups(*group) } if details := prop.ProviderSpecificDetails; details != nil && len(*details) > 0 { - state.A2ASettings = flattenRecoveryPlanProviderSpecficInput(details) + state.A2ASettings = flattenDataSourceRecoveryPlanProviderSpecficInput(details) } } @@ -89,7 +124,65 @@ func (r SiteRecoveryReplicationRecoveryPlanDataSource) Read() sdk.ResourceFunc { return metadata.Encode(&state) }, } +} +func flattenDataSourceRecoveryGroups(input []replicationrecoveryplans.RecoveryPlanGroup) []RecoveryGroupDataSourceModel { + output := make([]RecoveryGroupDataSourceModel, 0) + for _, groupItem := range input { + recoveryGroupOutput := RecoveryGroupDataSourceModel{} + recoveryGroupOutput.GroupType = string(groupItem.GroupType) + if groupItem.ReplicationProtectedItems != nil { + recoveryGroupOutput.ReplicatedProtectedItems = flattenRecoveryPlanProtectedItems(groupItem.ReplicationProtectedItems) + } + if groupItem.StartGroupActions != nil { + recoveryGroupOutput.PreAction = flattenDataSourceRecoveryPlanActions(groupItem.StartGroupActions) + } + if groupItem.EndGroupActions != nil { + recoveryGroupOutput.PostAction = flattenDataSourceRecoveryPlanActions(groupItem.EndGroupActions) + } + output = append(output, recoveryGroupOutput) + } + return output +} + +func flattenDataSourceRecoveryPlanActions(input *[]replicationrecoveryplans.RecoveryPlanAction) []ActionDataSourceModel { + actionOutputs := make([]ActionDataSourceModel, 0) + for _, action := range *input { + actionOutput := ActionDataSourceModel{ + Name: action.ActionName, + } + switch detail := action.CustomDetails.(type) { + case replicationrecoveryplans.RecoveryPlanAutomationRunbookActionDetails: + actionOutput.ActionDetailType = "AutomationRunbookActionDetails" + actionOutput.FabricLocation = string(detail.FabricLocation) + if detail.RunbookId != nil { + actionOutput.RunbookId = *detail.RunbookId + } + case replicationrecoveryplans.RecoveryPlanManualActionDetails: + actionOutput.ActionDetailType = "ManualActionDetails" + if detail.Description != nil { + actionOutput.ManualActionInstruction = *detail.Description + } + case replicationrecoveryplans.RecoveryPlanScriptActionDetails: + actionOutput.ActionDetailType = "ScriptActionDetails" + actionOutput.ScriptPath = detail.Path + actionOutput.FabricLocation = string(detail.FabricLocation) + } + + failoverDirections := make([]string, 0) + for _, failoverDirection := range action.FailoverDirections { + failoverDirections = append(failoverDirections, string(failoverDirection)) + } + + failoverTypes := make([]string, 0) + for _, failoverType := range action.FailoverTypes { + failoverTypes = append(failoverTypes, string(failoverType)) + } + actionOutput.FailOverDirections = failoverDirections + actionOutput.FailOverTypes = failoverTypes + actionOutputs = append(actionOutputs, actionOutput) + } + return actionOutputs } func (r SiteRecoveryReplicationRecoveryPlanDataSource) Arguments() map[string]*pluginsdk.Schema { @@ -108,6 +201,22 @@ func (r SiteRecoveryReplicationRecoveryPlanDataSource) Arguments() map[string]*p } } +func flattenDataSourceRecoveryPlanProviderSpecficInput(input *[]replicationrecoveryplans.RecoveryPlanProviderSpecificDetails) []ReplicationRecoveryPlanA2ASpecificInputDataSourceModel { + output := make([]ReplicationRecoveryPlanA2ASpecificInputDataSourceModel, 0) + for _, providerSpecificInput := range *input { + if a2aInput, ok := providerSpecificInput.(replicationrecoveryplans.RecoveryPlanA2ADetails); ok { + o := ReplicationRecoveryPlanA2ASpecificInputDataSourceModel{ + PrimaryZone: pointer.From(a2aInput.PrimaryZone), + RecoveryZone: pointer.From(a2aInput.RecoveryZone), + PrimaryEdgeZone: flattenEdgeZone(a2aInput.PrimaryExtendedLocation), + RecoveryEdgeZone: flattenEdgeZone(a2aInput.RecoveryExtendedLocation), + } + output = append(output, o) + } + } + return output +} + func (r SiteRecoveryReplicationRecoveryPlanDataSource) Attributes() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{ "source_recovery_fabric_id": { diff --git a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source_test.go b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source_test.go index 8e4893a2b97d..b69232f734a1 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source_test.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_data_source_test.go @@ -25,9 +25,6 @@ func TestAccDataSourceSiteRecoveryReplicationRecoveryPlan_basic(t *testing.T) { check.That(data.ResourceName).Key("recovery_vault_id").Exists(), check.That(data.ResourceName).Key("source_recovery_fabric_id").Exists(), check.That(data.ResourceName).Key("target_recovery_fabric_id").Exists(), - check.That(data.ResourceName).Key("recovery_group.0.type").HasValue("Boot"), - check.That(data.ResourceName).Key("recovery_group.1.type").HasValue("Failover"), - check.That(data.ResourceName).Key("recovery_group.2.type").HasValue("Shutdown"), ), }, }) @@ -45,9 +42,6 @@ func TestAccDataSourceSiteRecoveryReplicationRecoveryPlan_withZones(t *testing.T check.That(data.ResourceName).Key("recovery_vault_id").Exists(), check.That(data.ResourceName).Key("source_recovery_fabric_id").Exists(), check.That(data.ResourceName).Key("target_recovery_fabric_id").Exists(), - check.That(data.ResourceName).Key("recovery_group.0.type").HasValue("Boot"), - check.That(data.ResourceName).Key("recovery_group.1.type").HasValue("Failover"), - check.That(data.ResourceName).Key("recovery_group.2.type").HasValue("Shutdown"), check.That(data.ResourceName).Key("azure_to_azure_settings.0.primary_zone").HasValue("1"), check.That(data.ResourceName).Key("azure_to_azure_settings.0.recovery_zone").HasValue("2"), ), 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 61159cab03f2..13b1d43a167b 100644 --- a/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go +++ b/internal/services/recoveryservices/site_recovery_replication_recovery_plan_resource.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" @@ -25,6 +26,9 @@ import ( type SiteRecoveryReplicationRecoveryPlanModel struct { Name string `tfschema:"name"` + ShutdownRecoveryGroup []GenericRecoveryGroupModel `tfschema:"shutdown_recovery_group"` + FailoverRecoveryGroup []GenericRecoveryGroupModel `tfschema:"failover_recovery_group"` + BootRecoveryGroup []BootRecoveryGroupModel `tfschema:"boot_recovery_group"` RecoveryGroup []RecoveryGroupModel `tfschema:"recovery_group"` RecoveryVaultId string `tfschema:"recovery_vault_id"` SourceRecoveryFabricId string `tfschema:"source_recovery_fabric_id"` @@ -32,6 +36,17 @@ type SiteRecoveryReplicationRecoveryPlanModel struct { A2ASettings []ReplicationRecoveryPlanA2ASpecificInputModel `tfschema:"azure_to_azure_settings"` } +type GenericRecoveryGroupModel struct { + PostAction []ActionModel `tfschema:"post_action"` + PreAction []ActionModel `tfschema:"pre_action"` +} + +type BootRecoveryGroupModel struct { + PostAction []ActionModel `tfschema:"post_action"` + PreAction []ActionModel `tfschema:"pre_action"` + ReplicatedProtectedItems []string `tfschema:"replicated_protected_items"` +} + type RecoveryGroupModel struct { GroupType string `tfschema:"type"` PostAction []ActionModel `tfschema:"post_action"` @@ -74,7 +89,7 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) IDValidationFunc() pluginsd } func (r SiteRecoveryReplicationRecoveryPlanResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + schema := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -103,10 +118,104 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Arguments() map[string]*plu ValidateFunc: replicationfabrics.ValidateReplicationFabricID, }, - "recovery_group": { - Type: pluginsdk.TypeSet, - Optional: true, - MinItems: 3, + // lintignore:S013 + "shutdown_recovery_group": { + Type: pluginsdk.TypeList, + Required: features.FourPointOhBeta(), + Optional: !features.FourPointOhBeta(), + Computed: !features.FourPointOhBeta(), + MinItems: 1, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "pre_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + + "post_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + }, + }, + }, + + // lintignore:S013 + "failover_recovery_group": { + Type: pluginsdk.TypeList, + Required: features.FourPointOhBeta(), + Optional: !features.FourPointOhBeta(), + Computed: !features.FourPointOhBeta(), + MinItems: 1, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "pre_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + + "post_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + }, + }, + }, + + // lintignore:S013 + "boot_recovery_group": { + Type: pluginsdk.TypeList, + Required: features.FourPointOhBeta(), + Optional: !features.FourPointOhBeta(), + Computed: !features.FourPointOhBeta(), + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "replicated_protected_items": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: azure.ValidateResourceID, + }, + }, + + "pre_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + + "post_action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: replicationRecoveryPlanActionSchema(), + }, + }, + }, + }, + + "azure_to_azure_settings": replicationRecoveryPlanA2ASchema(), + } + + if !features.FourPointOhBeta() { + schema["recovery_group"] = &pluginsdk.Schema{ + Deprecated: "the `recovery_group` block has been deprecated in favour of the `shutdown_recovery_group`, `failover_recovery_group` and `boot_recovery_group` and will be removed in version 4.0 of the provider.", + Type: pluginsdk.TypeSet, + Optional: true, + Computed: true, + MinItems: 3, + ConflictsWith: []string{ + "shutdown_recovery_group", + "failover_recovery_group", + "boot_recovery_group", + }, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "type": { @@ -138,12 +247,13 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Arguments() map[string]*plu }, }, }, - }, - - "azure_to_azure_settings": replicationRecoveryPlanA2ASchema(), + } } + + return schema } +// we do not split action into three different schema because all actions should keep the order from user. func replicationRecoveryPlanActionSchema() *pluginsdk.Resource { return &pluginsdk.Resource{ Schema: map[string]*schema.Schema{ @@ -302,9 +412,17 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Create() sdk.ResourceFunc { // FailoverDeploymentModelClassic is used for other cloud service back up to Azure. deploymentModel := replicationrecoveryplans.FailoverDeploymentModelResourceManager - groupValue, err := expandRecoverGroup(model.RecoveryGroup) - if err != nil { - return fmt.Errorf("when expanding recovery group: %s", err) + var groupValue []replicationrecoveryplans.RecoveryPlanGroup + if len(model.RecoveryGroup) > 0 { + groupValue, err = expandRecoveryGroup(model.RecoveryGroup) + if err != nil { + return fmt.Errorf("expanding recovery group: %+v", err) + } + } else { + groupValue, err = expandRecoveryGroupNew(model.ShutdownRecoveryGroup, model.FailoverRecoveryGroup, model.BootRecoveryGroup) + if err != nil { + return fmt.Errorf("expanding recovery group: %+v", err) + } } parameters := replicationrecoveryplans.CreateRecoveryPlanInput{ @@ -372,6 +490,7 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Read() sdk.ResourceFunc { } if group := prop.Groups; group != nil { state.RecoveryGroup = flattenRecoveryGroups(*group) + state.ShutdownRecoveryGroup, state.FailoverRecoveryGroup, state.BootRecoveryGroup = flattenRecoveryGroupsNew(*group) } if details := prop.ProviderSpecificDetails; details != nil && len(*details) > 0 { state.A2ASettings = flattenRecoveryPlanProviderSpecficInput(details) @@ -398,7 +517,7 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Update() sdk.ResourceFunc { return fmt.Errorf("parse Site reocvery replication plan id: %+v", err) } - recoveryPlanGroup, err := expandRecoverGroup(model.RecoveryGroup) + recoveryPlanGroup, err := expandRecoveryGroup(model.RecoveryGroup) if err != nil { return fmt.Errorf("when expanding recovery group: %s", err) } @@ -440,14 +559,14 @@ func (r SiteRecoveryReplicationRecoveryPlanResource) Delete() sdk.ResourceFunc { } } -func expandRecoverGroup(input []RecoveryGroupModel) ([]replicationrecoveryplans.RecoveryPlanGroup, error) { +// TODO: deprecated, remove in v4.0 +func expandRecoveryGroup(input []RecoveryGroupModel) ([]replicationrecoveryplans.RecoveryPlanGroup, error) { output := make([]replicationrecoveryplans.RecoveryPlanGroup, 0) - if pass, err := validateRecoverGroup(input); !pass { + if pass, err := validateRecoveryGroup(input); !pass { return output, err } for _, group := range input { - protectedItems := make([]replicationrecoveryplans.RecoveryPlanProtectedItem, 0) for _, protectedItem := range group.ReplicatedProtectedItems { protectedItems = append(protectedItems, replicationrecoveryplans.RecoveryPlanProtectedItem{ @@ -455,56 +574,116 @@ func expandRecoverGroup(input []RecoveryGroupModel) ([]replicationrecoveryplans. }) } - preActions := make([]replicationrecoveryplans.RecoveryPlanAction, 0) - for _, preActionInput := range group.PreAction { + preActions, err := expandAction(group.PreAction) + if err != nil { + return output, err + } + postActions, err := expandAction(group.PostAction) + if err != nil { + return output, err + } - failoverDirections := make([]replicationrecoveryplans.PossibleOperationsDirections, 0) - for _, direction := range preActionInput.FailOverDirections { - failoverDirections = append(failoverDirections, replicationrecoveryplans.PossibleOperationsDirections(direction)) - } + output = append(output, replicationrecoveryplans.RecoveryPlanGroup{ + GroupType: replicationrecoveryplans.RecoveryPlanGroupType(group.GroupType), + ReplicationProtectedItems: &protectedItems, + StartGroupActions: &preActions, + EndGroupActions: &postActions, + }) - failoverTypes := make([]replicationrecoveryplans.ReplicationProtectedItemOperation, 0) - for _, failoveType := range preActionInput.FailOverTypes { - failoverTypes = append(failoverTypes, replicationrecoveryplans.ReplicationProtectedItemOperation(failoveType)) - } + } + return output, nil +} - preActions = append(preActions, replicationrecoveryplans.RecoveryPlanAction{ - ActionName: preActionInput.Name, - FailoverDirections: failoverDirections, - FailoverTypes: failoverTypes, - CustomDetails: expandActionDetail(preActionInput), - }) +func expandRecoveryGroupNew(shutdown []GenericRecoveryGroupModel, failover []GenericRecoveryGroupModel, boot []BootRecoveryGroupModel) ([]replicationrecoveryplans.RecoveryPlanGroup, error) { + output := make([]replicationrecoveryplans.RecoveryPlanGroup, 0) + + for _, group := range shutdown { + preActions, err := expandAction(group.PreAction) + if err != nil { + return output, err + } + postActions, err := expandAction(group.PostAction) + if err != nil { + return output, err } - postActions := make([]replicationrecoveryplans.RecoveryPlanAction, 0) - for _, postActionInput := range group.PostAction { + output = append(output, replicationrecoveryplans.RecoveryPlanGroup{ + GroupType: replicationrecoveryplans.RecoveryPlanGroupTypeShutdown, + StartGroupActions: &preActions, + EndGroupActions: &postActions, + }) + } - failoverDirections := make([]replicationrecoveryplans.PossibleOperationsDirections, 0) - for _, direction := range postActionInput.FailOverDirections { - failoverDirections = append(failoverDirections, replicationrecoveryplans.PossibleOperationsDirections(direction)) - } + for _, group := range failover { + preActions, err := expandAction(group.PreAction) + if err != nil { + return output, err + } + postActions, err := expandAction(group.PostAction) + if err != nil { + return output, err + } - failoverTypes := make([]replicationrecoveryplans.ReplicationProtectedItemOperation, 0) - for _, failoveType := range postActionInput.FailOverTypes { - failoverTypes = append(failoverTypes, replicationrecoveryplans.ReplicationProtectedItemOperation(failoveType)) - } + output = append(output, replicationrecoveryplans.RecoveryPlanGroup{ + GroupType: replicationrecoveryplans.RecoveryPlanGroupTypeFailover, + StartGroupActions: &preActions, + EndGroupActions: &postActions, + }) + } - postActions = append(postActions, replicationrecoveryplans.RecoveryPlanAction{ - ActionName: postActionInput.Name, - FailoverDirections: failoverDirections, - FailoverTypes: failoverTypes, - CustomDetails: expandActionDetail(postActionInput), + for _, group := range boot { + protectedItems := make([]replicationrecoveryplans.RecoveryPlanProtectedItem, 0) + for _, protectedItem := range group.ReplicatedProtectedItems { + protectedItems = append(protectedItems, replicationrecoveryplans.RecoveryPlanProtectedItem{ + Id: pointer.To(protectedItem), }) } + preActions, err := expandAction(group.PreAction) + if err != nil { + return output, err + } + postActions, err := expandAction(group.PostAction) + if err != nil { + return output, err + } + output = append(output, replicationrecoveryplans.RecoveryPlanGroup{ - GroupType: replicationrecoveryplans.RecoveryPlanGroupType(group.GroupType), + GroupType: replicationrecoveryplans.RecoveryPlanGroupTypeBoot, ReplicationProtectedItems: &protectedItems, StartGroupActions: &preActions, EndGroupActions: &postActions, }) + } + + return output, nil +} + +func expandAction(input []ActionModel) ([]replicationrecoveryplans.RecoveryPlanAction, error) { + output := make([]replicationrecoveryplans.RecoveryPlanAction, 0) + for _, action := range input { + failoverDirections := make([]replicationrecoveryplans.PossibleOperationsDirections, 0) + for _, direction := range action.FailOverDirections { + failoverDirections = append(failoverDirections, replicationrecoveryplans.PossibleOperationsDirections(direction)) + } + + failoverTypes := make([]replicationrecoveryplans.ReplicationProtectedItemOperation, 0) + for _, failoverType := range action.FailOverTypes { + failoverTypes = append(failoverTypes, replicationrecoveryplans.ReplicationProtectedItemOperation(failoverType)) + } + if action.ActionDetailType == "ManualActionDetails" && action.FabricLocation != "" { + return nil, fmt.Errorf("`fabric_location` must not be specified for `recovery_group` with `ManualActionDetails` type.") + } + + output = append(output, replicationrecoveryplans.RecoveryPlanAction{ + ActionName: action.Name, + FailoverDirections: failoverDirections, + FailoverTypes: failoverTypes, + CustomDetails: expandActionDetail(action), + }) } + return output, nil } @@ -519,7 +698,7 @@ func expandA2ASettings(input ReplicationRecoveryPlanA2ASpecificInputModel) *[]re } } -func validateRecoverGroup(input []RecoveryGroupModel) (bool, error) { +func validateRecoveryGroup(input []RecoveryGroupModel) (bool, error) { bootCount := 0 shutdownCount := 0 failoverCount := 0 @@ -527,9 +706,14 @@ func validateRecoverGroup(input []RecoveryGroupModel) (bool, error) { if group.GroupType == string(replicationrecoveryplans.RecoveryPlanGroupTypeBoot) { bootCount += 1 } + if group.GroupType == string(replicationrecoveryplans.RecoveryPlanGroupTypeFailover) { failoverCount += 1 + if len(group.ReplicatedProtectedItems) > 0 { + return false, fmt.Errorf("`replicated_protected_items` must not be specified for `recovery_group` with `Failover` type.") + } } + if group.GroupType == string(replicationrecoveryplans.RecoveryPlanGroupTypeShutdown) { shutdownCount += 1 if len(group.ReplicatedProtectedItems) > 0 { @@ -544,6 +728,7 @@ func validateRecoverGroup(input []RecoveryGroupModel) (bool, error) { } } + if bootCount == 0 || shutdownCount == 0 || failoverCount == 0 { return false, fmt.Errorf("every group type needs at least one recovery group") } @@ -569,6 +754,46 @@ func flattenRecoveryGroups(input []replicationrecoveryplans.RecoveryPlanGroup) [ return output } +func flattenRecoveryGroupsNew(input []replicationrecoveryplans.RecoveryPlanGroup) (shutdown []GenericRecoveryGroupModel, failover []GenericRecoveryGroupModel, boot []BootRecoveryGroupModel) { + shutdown = make([]GenericRecoveryGroupModel, 0) + failover = make([]GenericRecoveryGroupModel, 0) + boot = make([]BootRecoveryGroupModel, 0) + for _, groupItem := range input { + switch groupItem.GroupType { + case replicationrecoveryplans.RecoveryPlanGroupTypeShutdown: + o := GenericRecoveryGroupModel{} + if groupItem.StartGroupActions != nil { + o.PreAction = flattenRecoveryPlanActions(groupItem.StartGroupActions) + } + if groupItem.EndGroupActions != nil { + o.PostAction = flattenRecoveryPlanActions(groupItem.EndGroupActions) + } + shutdown = append(shutdown, o) + case replicationrecoveryplans.RecoveryPlanGroupTypeFailover: + o := GenericRecoveryGroupModel{} + if groupItem.StartGroupActions != nil { + o.PreAction = flattenRecoveryPlanActions(groupItem.StartGroupActions) + } + if groupItem.EndGroupActions != nil { + o.PostAction = flattenRecoveryPlanActions(groupItem.EndGroupActions) + } + failover = append(failover, o) + case replicationrecoveryplans.RecoveryPlanGroupTypeBoot: + o := BootRecoveryGroupModel{} + o.ReplicatedProtectedItems = flattenRecoveryPlanProtectedItems(groupItem.ReplicationProtectedItems) + if groupItem.StartGroupActions != nil { + o.PreAction = flattenRecoveryPlanActions(groupItem.StartGroupActions) + } + if groupItem.EndGroupActions != nil { + o.PostAction = flattenRecoveryPlanActions(groupItem.EndGroupActions) + } + boot = append(boot, o) + } + } + + return shutdown, failover, boot +} + func expandActionDetail(input ActionModel) (output replicationrecoveryplans.RecoveryPlanActionDetails) { switch input.ActionDetailType { case "AutomationRunbookActionDetails": @@ -591,8 +816,10 @@ func expandActionDetail(input ActionModel) (output replicationrecoveryplans.Reco func flattenRecoveryPlanProtectedItems(input *[]replicationrecoveryplans.RecoveryPlanProtectedItem) []string { protectedItemOutputs := make([]string, 0) - for _, protectedItem := range *input { - protectedItemOutputs = append(protectedItemOutputs, handleAzureSdkForGoBug2824(*protectedItem.Id)) + if input != nil { + for _, protectedItem := range *input { + protectedItemOutputs = append(protectedItemOutputs, handleAzureSdkForGoBug2824(*protectedItem.Id)) + } } return protectedItemOutputs } 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 c4f917eb1c79..6d4da320c80e 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 @@ -75,10 +75,10 @@ func TestAccSiteRecoveryReplicationRecoveryPlan_withMultiActions(t *testing.T) { Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), // to check the actions are in the correct order - check.That(data.ResourceName).Key("recovery_group.0.pre_action.0.name").HasValue("testPreAction1"), - check.That(data.ResourceName).Key("recovery_group.0.pre_action.1.name").HasValue("testPreAction2"), - check.That(data.ResourceName).Key("recovery_group.0.post_action.0.name").HasValue("testPostAction1"), - check.That(data.ResourceName).Key("recovery_group.0.post_action.1.name").HasValue("testPostAction2"), + check.That(data.ResourceName).Key("boot_recovery_group.0.pre_action.0.name").HasValue("testPreAction1"), + check.That(data.ResourceName).Key("boot_recovery_group.0.pre_action.1.name").HasValue("testPreAction2"), + check.That(data.ResourceName).Key("boot_recovery_group.0.post_action.0.name").HasValue("testPostAction1"), + check.That(data.ResourceName).Key("boot_recovery_group.0.post_action.1.name").HasValue("testPostAction2"), ), }, data.ImportStep(), @@ -115,13 +115,30 @@ func TestAccSiteRecoveryReplicationRecoveryPlan_withEdgeZones(t *testing.T) { }) } +func TestAccSiteRecoveryReplicationRecoveryPlan_withMultiBootGroup(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_site_recovery_replication_recovery_plan", "test") + r := SiteRecoveryReplicationRecoveryPlan{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withMultiBootGroup(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("boot_recovery_group.0.replicated_protected_items.#").HasValue("1"), + check.That(data.ResourceName).Key("boot_recovery_group.1.pre_action.0.name").HasValue("testPreAction"), + ), + }, + data.ImportStep(), + }) +} + func TestAccSiteRecoveryReplicationRecoveryPlan_wrongSettings(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_site_recovery_replication_recovery_plan", "test") r := SiteRecoveryReplicationRecoveryPlan{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.wrongSettings(data), + Config: r.wrongSettingsWithDeprecatedGroup(data), ExpectError: regexp.MustCompile("`replicated_protected_items` must not be specified for `recovery_group` with `Shutdown` type."), }, }) @@ -372,18 +389,14 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { 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] - } + shutdown_recovery_group {} - recovery_group { - type = "Failover" - } + failover_recovery_group {} - recovery_group { - type = "Shutdown" + boot_recovery_group { + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] } + } `, r.template(data), data.RandomInteger) } @@ -398,8 +411,11 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id - recovery_group { - type = "Boot" + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] pre_action { name = "testPreAction" @@ -410,13 +426,6 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } } - recovery_group { - type = "Failover" - } - - recovery_group { - type = "Shutdown" - } } `, r.template(data), data.RandomInteger) } @@ -431,8 +440,11 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id - recovery_group { - type = "Boot" + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] post_action { name = "testPreAction" @@ -443,14 +455,6 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } } - recovery_group { - type = "Failover" - } - - recovery_group { - type = "Shutdown" - } - } `, r.template(data), data.RandomInteger) @@ -466,8 +470,11 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id - recovery_group { - type = "Boot" + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] pre_action { name = "testPreAction1" @@ -502,13 +509,6 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } } - recovery_group { - type = "Failover" - } - - recovery_group { - type = "Shutdown" - } } `, r.template(data), data.RandomInteger) } @@ -523,17 +523,12 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { 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] - } + shutdown_recovery_group {} - recovery_group { - type = "Failover" - } + failover_recovery_group {} - recovery_group { - type = "Shutdown" + boot_recovery_group { + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] } azure_to_azure_settings { @@ -561,17 +556,12 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { 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] - } + shutdown_recovery_group {} - recovery_group { - type = "Failover" - } + failover_recovery_group {} - recovery_group { - type = "Shutdown" + boot_recovery_group { + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] } azure_to_azure_settings { @@ -582,7 +572,39 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { `, r.template(data), data.RandomInteger) } -func (r SiteRecoveryReplicationRecoveryPlan) wrongSettings(data acceptance.TestData) string { +func (r SiteRecoveryReplicationRecoveryPlan) withMultiBootGroup(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 + + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { + replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] + } + + boot_recovery_group { + pre_action { + name = "testPreAction" + type = "ManualActionDetails" + fail_over_directions = ["PrimaryToRecovery"] + fail_over_types = ["TestFailover"] + manual_action_instruction = "test instruction" + } + } + +} +`, r.template(data), data.RandomInteger) +} + +func (r SiteRecoveryReplicationRecoveryPlan) wrongSettingsWithDeprecatedGroup(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -593,7 +615,7 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id recovery_group { - type = "Boot" + type = "Shutdown" replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] } @@ -602,9 +624,10 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } recovery_group { - type = "Shutdown" + type = "Boot" replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] } + } `, r.template(data), data.RandomInteger) } @@ -619,8 +642,11 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { source_recovery_fabric_id = azurerm_site_recovery_fabric.test1.id target_recovery_fabric_id = azurerm_site_recovery_fabric.test2.id - recovery_group { - type = "Boot" + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { replicated_protected_items = [azurerm_site_recovery_replicated_vm.test.id] post_action { @@ -633,13 +659,6 @@ resource "azurerm_site_recovery_replication_recovery_plan" "test" { } } - recovery_group { - type = "Failover" - } - - recovery_group { - type = "Shutdown" - } } `, r.template(data), data.RandomInteger) } 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 e12e9d458fab..a8daacabcdeb 100644 --- a/website/docs/r/site_recovery_replication_recovery_plan.html.markdown +++ b/website/docs/r/site_recovery_replication_recovery_plan.html.markdown @@ -223,16 +223,14 @@ resource "azurerm_site_recovery_replication_recovery_plan" "example" { source_recovery_fabric_id = azurerm_site_recovery_fabric.primary.id target_recovery_fabric_id = azurerm_site_recovery_fabric.secondary.id - recovery_group { - type = "Boot" + shutdown_recovery_group {} + + failover_recovery_group {} + + boot_recovery_group { replicated_protected_items = [azurerm_site_recovery_replicated_vm.vm-replication.id] } - recovery_group { - type = "Failover" - } - recovery_group { - type = "Shutdown" - } + } ``` @@ -250,6 +248,20 @@ The following arguments are supported: * `recovery_group` - (Optional) Three or more `recovery_group` block defined as below. +**Note:** The `recovery_group` block is deprecated in favor of `shutdown_recovery_group`, `failover_recovery_group` and `boot_recovery_group`. It will be removed in v4.0 of the Azure Provider. + +* `shutdown_recovery_group` - (Optional) One `shutdown_recovery_group` block as defined below. + +-> **NOTE:** `shutdown_recovery_group` will be required in the next major version of the AzureRM Provider. + +* `failover_recovery_group` - (Optional) One `failover_recovery_group` block as defined below. + +-> **NOTE:** `failover_recovery_group` will be required in the next major version of the AzureRM Provider. + +* `boot_recovery_group` - (Optional) One or more `boot_recovery_group` blocks as defined below. + +-> **NOTE:** At least one `boot_recovery_group` block will be required in the next major version of the AzureRM Provider. + * `azure_to_azure_settings` - (Optional) An `azure_to_azure_settings` block defined as block. --- @@ -266,6 +278,32 @@ A `recovery_group` block supports the following: --- +A `shutdown_recovery_group` block supports the following: + +* `pre_action` - (Optional) one or more `action` block as defined below. which will be executed before the group recovery. + +* `post_action` - (Optional) one or more `action` block as defined below. which will be executed after the group recovery. + +--- + +A `failover_recovery_group` block supports the following: + +* `pre_action` - (Optional) one or more `action` block as defined below. which will be executed before the group recovery. + +* `post_action` - (Optional) one or more `action` block as defined below. which will be executed after the group recovery. + +--- + +A `boot_recovery_group` block supports the following: + +* `replicated_protected_items` - (Optional) One or more protected VM IDs. It must not be specified when `type` is `Shutdown`. + +* `pre_action` - (Optional) one or more `action` block as defined below. which will be executed before the group recovery. + +* `post_action` - (Optional) one or more `action` block as defined below. which will be executed after the group recovery. + +--- + An `action` block supports the following: * `name` - (Required) Name of the Action.