diff --git a/internal/services/recoveryservices/registration.go b/internal/services/recoveryservices/registration.go index 01dc2ac2654d..9aae064aa8e0 100644 --- a/internal/services/recoveryservices/registration.go +++ b/internal/services/recoveryservices/registration.go @@ -30,6 +30,7 @@ func (r Registration) Resources() []sdk.Resource { HyperVSiteResource{}, HyperVReplicationPolicyAssociationResource{}, VMWareReplicationPolicyResource{}, + VMWareReplicationPolicyAssociationResource{}, } } diff --git a/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource.go b/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource.go new file mode 100644 index 000000000000..e355a3aef4cd --- /dev/null +++ b/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource.go @@ -0,0 +1,245 @@ +package recoveryservices + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservices/2022-10-01/vaults" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotectioncontainermappings" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotectioncontainers" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationvaultsetting" + "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/recoveryservices/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/suppress" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +const SiteRecoveryReplicationPolicyVMWareAssociationTargetContainerId string = "Microsoft Azure" + +type SiteRecoveryReplicationPolicyVmwareAssociationModel struct { + Name string `tfschema:"name"` + RecoveryVaultId string `tfschema:"recovery_vault_id"` + RecoveryReplicationPolicyId string `tfschema:"policy_id"` +} + +type VMWareReplicationPolicyAssociationResource struct{} + +var _ sdk.Resource = VMWareReplicationPolicyAssociationResource{} + +func (s VMWareReplicationPolicyAssociationResource) ModelObject() interface{} { + return &SiteRecoveryReplicationPolicyVmwareAssociationModel{} +} + +func (s VMWareReplicationPolicyAssociationResource) ResourceType() string { + return "azurerm_site_recovery_vmware_replication_policy_association" +} + +func (s VMWareReplicationPolicyAssociationResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "recovery_vault_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: vaults.ValidateVaultID, + }, + + "policy_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + DiffSuppressFunc: suppress.CaseDifference, + }, + } +} + +func (s VMWareReplicationPolicyAssociationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (s VMWareReplicationPolicyAssociationResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model SiteRecoveryReplicationPolicyVmwareAssociationModel + err := metadata.Decode(&model) + if err != nil { + return fmt.Errorf("decoding %+v", err) + } + + client := metadata.Client.RecoveryServices.ContainerMappingClient + containerClient := metadata.Client.RecoveryServices.ProtectionContainerClient + settingsClient := metadata.Client.RecoveryServices.VaultsSettingsClient + + err = validateReplicationPolicyAssociationVaultConfig(ctx, settingsClient, model.RecoveryVaultId) + if err != nil { + return fmt.Errorf("validating %s: %+v", model.RecoveryVaultId, err) + } + + // There should be only 1 fabric and only 1 container in Vmware Vault. + containerId, err := fetchReplicationPolicyAssociationContainerId(ctx, containerClient, model.RecoveryVaultId) + if err != nil { + return fmt.Errorf("fetch replication container from %q: %+v", model.RecoveryVaultId, err) + } + + parsedContainerId, err := replicationprotectioncontainers.ParseReplicationProtectionContainerID(containerId) + if err != nil { + return fmt.Errorf("parse %q: %+v", containerId, err) + } + + id := replicationprotectioncontainermappings.NewReplicationProtectionContainerMappingID(parsedContainerId.SubscriptionId, parsedContainerId.ResourceGroupName, parsedContainerId.VaultName, parsedContainerId.ReplicationFabricName, parsedContainerId.ReplicationProtectionContainerName, model.Name) + + existing, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing site recovery protection container mapping (%s): %+v", parsedContainerId, err) + } + } + + if existing.Model != nil && existing.Model.Id != nil && *existing.Model.Id != "" { + return tf.ImportAsExistsError("azurerm_site_recovery_replication_policy_vmware_association", *existing.Model.Id) + } + + type RawProviderSpecificInput struct { + Type string `json:"-"` + Values map[string]interface{} `json:"-"` + } + + parameters := replicationprotectioncontainermappings.CreateProtectionContainerMappingInput{ + Properties: &replicationprotectioncontainermappings.CreateProtectionContainerMappingInputProperties{ + TargetProtectionContainerId: utils.String(SiteRecoveryReplicationPolicyVMWareAssociationTargetContainerId), + PolicyId: &model.RecoveryReplicationPolicyId, + ProviderSpecificInput: &RawProviderSpecificInput{}, + }, + } + + err = client.CreateThenPoll(ctx, id, parameters) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + + return nil + }, + } +} + +func (s VMWareReplicationPolicyAssociationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + id, err := replicationprotectioncontainermappings.ParseReplicationProtectionContainerMappingID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing %s: %+v", metadata.ResourceData.Id(), err) + } + + client := metadata.Client.RecoveryServices.ContainerMappingClient + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("reading %s : %+v", id.String(), err) + } + + vaultId := vaults.NewVaultID(id.SubscriptionId, id.ResourceGroupName, id.VaultName) + state := SiteRecoveryReplicationPolicyVmwareAssociationModel{ + Name: id.ReplicationProtectionContainerMappingName, + RecoveryVaultId: vaultId.ID(), + } + + if model := resp.Model; model != nil { + if prop := model.Properties; prop != nil { + if prop.PolicyId != nil { + state.RecoveryReplicationPolicyId = *prop.PolicyId + } + } + } + + return metadata.Encode(&state) + }, + } +} + +func (s VMWareReplicationPolicyAssociationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + id, err := replicationprotectioncontainermappings.ParseReplicationProtectionContainerMappingID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + client := metadata.Client.RecoveryServices.ContainerMappingClient + + input := replicationprotectioncontainermappings.RemoveProtectionContainerMappingInput{ + Properties: &replicationprotectioncontainermappings.RemoveProtectionContainerMappingInputProperties{ + ProviderSpecificInput: &replicationprotectioncontainermappings.ReplicationProviderContainerUnmappingInput{}, + }, + } + + err = client.DeleteThenPoll(ctx, *id, input) + if err != nil { + return fmt.Errorf("deleting %s : %+v", id.String(), err) + } + + return nil + }, + } +} + +func (s VMWareReplicationPolicyAssociationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.ReplicationProtectionContainerMappingsID +} + +func validateReplicationPolicyAssociationVaultConfig(ctx context.Context, settingsClient *replicationvaultsetting.ReplicationVaultSettingClient, vaultId string) error { + vId, err := replicationvaultsetting.ParseVaultID(vaultId) + if err != nil { + return fmt.Errorf("parse %s: %+v", vaultId, err) + } + + settingsId := replicationvaultsetting.NewReplicationVaultSettingID(vId.SubscriptionId, vId.ResourceGroupName, vId.VaultName, "default") + resp, err := settingsClient.Get(ctx, settingsId) + if err != nil { + return fmt.Errorf("retire %s: %+v", settingsId, err) + } + + if resp.Model != nil && resp.Model.Properties != nil && resp.Model.Properties.VMwareToAzureProviderType != nil { + return fmt.Errorf("can not associate a modern policy to classic VMWare vault") + } + + return nil +} + +func fetchReplicationPolicyAssociationContainerId(ctx context.Context, containerClient *replicationprotectioncontainers.ReplicationProtectionContainersClient, vaultId string) (containerId string, err error) { + vId, err := replicationprotectioncontainers.ParseVaultID(vaultId) + if err != nil { + return "", fmt.Errorf("parse %s: %+v", vaultId, err) + } + + resp, err := containerClient.ListComplete(ctx, *vId) + if err != nil { + return "", err + } + + if len(resp.Items) != 1 { + return "", fmt.Errorf("there should be only one protection container in Classic Recovery Vault, get: %v", len(resp.Items)) + } + + return handleAzureSdkForGoBug2824(*resp.Items[0].Id), nil +} diff --git a/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource_test.go b/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource_test.go new file mode 100644 index 000000000000..231f04a03746 --- /dev/null +++ b/internal/services/recoveryservices/site_recovery_vmware_replication_policy_association_resource_test.go @@ -0,0 +1,74 @@ +package recoveryservices_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotectioncontainermappings" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type SiteRecoveryVMWareReplicationPolicyAssociationResource struct{} + +func TestAccSiteRecoveryVMWareReplicationPolicyAssociation_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_site_recovery_vmware_replication_policy_association", "test") + r := SiteRecoveryVMWareReplicationPolicyAssociationResource{} + + vaultId := os.Getenv("ARM_TEST_VMWARE_VAULT_ID") + if vaultId == "" { + t.Skip("Skipping as ARM_TEST_VMWARE_VAULT_ID is not specified") + return + } + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, vaultId), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +// association policy requires there to be a VMWare Server connected to the vault. +func (SiteRecoveryVMWareReplicationPolicyAssociationResource) basic(data acceptance.TestData, vaultId string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_site_recovery_vmware_replication_policy" "test" { + recovery_vault_id = "%s" + name = "acctest-policy-%d" + recovery_point_retention_in_minutes = %d + application_consistent_snapshot_frequency_in_minutes = %d +} + +resource "azurerm_site_recovery_vmware_replication_policy_association" "test" { + name = "acctest-%d" + recovery_vault_id = "%s" + policy_id = azurerm_site_recovery_vmware_replication_policy.test.id +} +`, vaultId, data.RandomInteger, 24*60, 4*60, data.RandomInteger, vaultId) +} + +func (t SiteRecoveryVMWareReplicationPolicyAssociationResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := replicationprotectioncontainermappings.ParseReplicationProtectionContainerMappingID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.RecoveryServices.ContainerMappingClient.Get(ctx, *id) + if err != nil { + return nil, fmt.Errorf("reading %s: %+v", id.String(), err) + } + + return utils.Bool(resp.Model != nil), nil +} diff --git a/website/docs/r/site_recovery_vmware_replication_policy_association.html.markdown b/website/docs/r/site_recovery_vmware_replication_policy_association.html.markdown new file mode 100644 index 000000000000..3aeadf48899b --- /dev/null +++ b/website/docs/r/site_recovery_vmware_replication_policy_association.html.markdown @@ -0,0 +1,77 @@ +--- +subcategory: "Recovery Services" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_site_recovery_vmware_replication_policy_association" +description: |- + Manages an Azure Site Recovery replication policy association for VMWare on Azure. +--- + +# azurerm_site_recovery_vmware_replication_policy_association + +Manages an Azure Site Recovery replication policy for VMWare within a Recovery Vault. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "East US" +} + +resource "azurerm_recovery_services_vault" "example" { + name = "example-recovery-vault" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Standard" +} + +resource "azurerm_site_recovery_vmware_replication_policy" "example" { + name = "example-policy" + + recovery_vault_id = azurerm_recovery_services_vault.example.id + recovery_point_retention_in_minutes = 1440 + application_consistent_snapshot_frequency_in_minutes = 240 +} + +resource "azurerm_site_recovery_vmware_replication_policy_association" "example" { + name = "example-association" + recovery_vault_id = azurerm_recovery_services_vault.example.id + policy_id = azurerm_site_recovery_vmware_replication_policy.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the replication policy association. Changing this forces a new association to be + created. + +* `recovery_vault_id` - (Required) The ID of the Recovery Service Vault to which the policy should be associated. + Changing this forces a new association to be created. + +* `policy_id` - (Required) The ID of the VMWare replication policy which to be associated. Changing this forces a new + association to be created. + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `id` - The ID of the Site Recovery Replication Policy. + +## Timeouts + +The `timeouts` block allows you to +specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Site Recovery VMWare Replication Policy Association. +* `read` - (Defaults to 5 minutes) Used when retrieving the Site Recovery VMWare Replication Policy Association. +* `delete` - (Defaults to 30 minutes) Used when deleting the Site Recovery VMWare Replication Policy Association. + +## Import + +Site Recovery Replication Policies can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_site_recovery_vmware_replication_policy_association.mypolicy /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resource-group-name/providers/Microsoft.RecoveryServices/vaults/recovery-vault-name/replicationFabrics/site-name/replicationProtectionContainers/container-name/replicationProtectionContainerMappings/mapping-name +```