Skip to content

Commit

Permalink
New Resource: `azurerm_site_recovery_vmware_replication_policy_associ…
Browse files Browse the repository at this point in the history
…ation` (#21389)
  • Loading branch information
ziyeqf authored May 24, 2023
1 parent 705cd17 commit 4b6f5c9
Show file tree
Hide file tree
Showing 4 changed files with 397 additions and 0 deletions.
1 change: 1 addition & 0 deletions internal/services/recoveryservices/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (r Registration) Resources() []sdk.Resource {
HyperVReplicationPolicyAssociationResource{},
HyperVNetworkMappingResource{},
VMWareReplicationPolicyResource{},
VMWareReplicationPolicyAssociationResource{},
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 4b6f5c9

Please sign in to comment.