Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: azurerm_site_recovery_hyperv_replicated_vm #20838

Closed
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package azuresdkhacks

import "github.com/Azure/go-autorest/autorest"

// TODO 4.0: check if this is could be removed
// workaround for https://github.com/Azure/azure-rest-api-specs/issues/22947
Comment on lines +5 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue on hashicorp/go-azure-sdk to track this? We can likely workaround this there


type ReplicationProtectedItemsClient struct {
Client autorest.Client
BaseUri string
}

func NewReplicationProtectedItemsClientWithBaseURI(endpoint string) ReplicationProtectedItemsClient {
return ReplicationProtectedItemsClient{
Client: autorest.NewClientWithUserAgent(userAgent()),
BaseUri: endpoint,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package azuresdkhacks

import (
"context"
"fmt"
"net/http"

"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/go-azure-helpers/polling"
"github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotecteditems"
)

type DeleteOperationResponse struct {
Poller polling.LongRunningPoller
HttpResponse *http.Response
}

// Delete ...
func (c ReplicationProtectedItemsClient) Delete(ctx context.Context, id replicationprotecteditems.ReplicationProtectedItemId, input DisableProtectionInput) (result DeleteOperationResponse, err error) {
req, err := c.preparerForDelete(ctx, id, input)
if err != nil {
err = autorest.NewErrorWithError(err, "replicationprotecteditems.ReplicationProtectedItemsClient", "Delete", nil, "Failure preparing request")
return
}

result, err = c.senderForDelete(ctx, req)
if err != nil {
err = autorest.NewErrorWithError(err, "replicationprotecteditems.ReplicationProtectedItemsClient", "Delete", result.HttpResponse, "Failure sending request")
return
}

return
}

// DeleteThenPoll performs Delete then polls until it's completed
func (c ReplicationProtectedItemsClient) DeleteThenPoll(ctx context.Context, id replicationprotecteditems.ReplicationProtectedItemId, input DisableProtectionInput) error {
result, err := c.Delete(ctx, id, input)
if err != nil {
return fmt.Errorf("performing Delete: %+v", err)
}

if err := result.Poller.PollUntilDone(); err != nil {
return fmt.Errorf("polling after Delete: %+v", err)
}

return nil
}

// preparerForDelete prepares the Delete request.
func (c ReplicationProtectedItemsClient) preparerForDelete(ctx context.Context, id replicationprotecteditems.ReplicationProtectedItemId, input DisableProtectionInput) (*http.Request, error) {
queryParameters := map[string]interface{}{
"api-version": defaultApiVersion,
}

preparer := autorest.CreatePreparer(
autorest.AsContentType("application/json; charset=utf-8"),
autorest.AsPost(),
autorest.WithBaseURL(c.BaseUri),
autorest.WithPath(fmt.Sprintf("%s/remove", id.ID())),
autorest.WithJSON(input),
autorest.WithQueryParameters(queryParameters))
return preparer.Prepare((&http.Request{}).WithContext(ctx))
}

// senderForDelete sends the Delete request. The method will close the
// http.Response Body if it receives an error.
func (c ReplicationProtectedItemsClient) senderForDelete(ctx context.Context, req *http.Request) (future DeleteOperationResponse, err error) {
var resp *http.Response
resp, err = c.Client.Send(req, azure.DoRetryWithRegistration(c.Client))
if err != nil {
return
}

future.Poller, err = polling.NewPollerFromResponse(ctx, resp, c.Client, req.Method)
return
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package azuresdkhacks

type DisableProtectionInput struct {
Properties EmptyInput `json:"properties"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the model here, since ReplicationProviderInput is a Discriminated Type that's Optional, as such this:

package main

import (
	"encoding/json"
	"log"

	"github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotecteditems"
)

func main() {
	foo := replicationprotecteditems.DisableProtectionInput{}
	out, err := json.Marshal(foo)
	if err != nil {
		log.Fatalf("error: %+v", err)
	}
	log.Printf("JSON: %s", out)
}

Should output:

JSON: {"properties":{}}

But actually outputs:

JSON: {"properties":{"replicationProviderInput":null}}

As such this looks to be something we can workaround in hashicorp/go-azure-sdk - can you open an issue about this, with the full HTTP Requests/Responses so that we can diagnose this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to confirm if we need to wait for the go-azure-sdk workaround to merge this PR?

}

type EmptyInput struct {
}
11 changes: 11 additions & 0 deletions internal/services/recoveryservices/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"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/replicationnetworkmappings"
"github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationpolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotectableitems"
"github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationprotecteditems"
"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"
Expand Down Expand Up @@ -38,7 +39,9 @@ type Client struct {
ReplicationPoliciesClient *replicationpolicies.ReplicationPoliciesClient
ContainerMappingClient *replicationprotectioncontainermappings.ReplicationProtectionContainerMappingsClient
NetworkMappingClient *replicationnetworkmappings.ReplicationNetworkMappingsClient
ReplicationProtectableItemsClient *replicationprotectableitems.ReplicationProtectableItemsClient
ReplicationProtectedItemsClient *replicationprotecteditems.ReplicationProtectedItemsClient
HackedReplicationProtectedItemsClient *azuresdkhacks.ReplicationProtectedItemsClient
ReplicationRecoveryPlansClient *replicationrecoveryplans.ReplicationRecoveryPlansClient
}

Expand Down Expand Up @@ -97,12 +100,18 @@ func NewClient(o *common.ClientOptions) *Client {
networkMappingClient := replicationnetworkmappings.NewReplicationNetworkMappingsClientWithBaseURI(o.ResourceManagerEndpoint)
o.ConfigureClient(&networkMappingClient.Client, o.ResourceManagerAuthorizer)

replicationProtectableItemsCLient := replicationprotectableitems.NewReplicationProtectableItemsClientWithBaseURI(o.ResourceManagerEndpoint)
o.ConfigureClient(&replicationProtectableItemsCLient.Client, o.ResourceManagerAuthorizer)

replicationMigrationItemsClient := replicationprotecteditems.NewReplicationProtectedItemsClientWithBaseURI(o.ResourceManagerEndpoint)
o.ConfigureClient(&replicationMigrationItemsClient.Client, o.ResourceManagerAuthorizer)

replicationRecoveryPlanClient := replicationrecoveryplans.NewReplicationRecoveryPlansClientWithBaseURI(o.ResourceManagerEndpoint)
o.ConfigureClient(&replicationRecoveryPlanClient.Client, o.ResourceManagerAuthorizer)

hackedReplicationProtectedItemsClient := azuresdkhacks.NewReplicationProtectedItemsClientWithBaseURI(o.ResourceManagerEndpoint)
o.ConfigureClient(&hackedReplicationProtectedItemsClient.Client, o.ResourceManagerAuthorizer)

return &Client{
ProtectableItemsClient: &protectableItemsClient,
ProtectedItemsClient: &protectedItemsClient,
Expand All @@ -122,7 +131,9 @@ func NewClient(o *common.ClientOptions) *Client {
ReplicationPoliciesClient: &replicationPoliciesClient,
ContainerMappingClient: &containerMappingClient,
NetworkMappingClient: &networkMappingClient,
ReplicationProtectableItemsClient: &replicationProtectableItemsCLient,
ReplicationProtectedItemsClient: &replicationMigrationItemsClient,
ReplicationRecoveryPlansClient: &replicationRecoveryPlanClient,
HackedReplicationProtectedItemsClient: &hackedReplicationProtectedItemsClient,
}
}
26 changes: 26 additions & 0 deletions internal/services/recoveryservices/helpers.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package recoveryservices

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/go-azure-helpers/lang/response"
"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/replicationprotectioncontainers"
)

// This code is a workaround for this bug https://github.com/Azure/azure-sdk-for-go/issues/2824
Expand Down Expand Up @@ -36,3 +40,25 @@ func wasBadRequestWithNotExist(resp *http.Response, err error) bool {

return response.WasBadRequest(resp) && sc == "SubscriptionIdNotRegisteredWithSrs"
}

func fetchHyperVContainerIdByFabricId(ctx context.Context, containerClient *replicationprotectioncontainers.ReplicationProtectionContainersClient, fabricId replicationfabrics.ReplicationFabricId) (string, error) {
id, err := replicationprotectioncontainers.ParseReplicationFabricID(fabricId.ID())
if err != nil {
return "", fmt.Errorf("parsing %s: %+v", fabricId.ID(), err)
}

resp, err := containerClient.ListByReplicationFabricsComplete(ctx, *id)
if err != nil {
return "", fmt.Errorf("listing containers: %+v", err)
}

if len(resp.Items) == 0 || len(resp.Items) > 1 {
return "", fmt.Errorf("expected one container but got %d", len(resp.Items))
}

if resp.Items[0].Id == nil {
return "", fmt.Errorf("container id is nil")
}

return handleAzureSdkForGoBug2824(*resp.Items[0].Id), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ import (
"github.com/hashicorp/terraform-provider-azurerm/utils"
)

const LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const LowerLetterBytes = "abcdefghijklmnopqrstuvwxyz"
const UpperLetterBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const NumberBytes = "1234567890"
const SpecialBytes = "!@#$%^()"

func GenerateRandomPassword(n int) string {
b := make([]byte, n)
for i := range b {
r := rand.Int()
switch r % 3 {
switch r % 4 {
case 0:
b[i] = LetterBytes[rand.Intn(len(LetterBytes))]
b[i] = LowerLetterBytes[rand.Intn(len(LowerLetterBytes))]
case 1:
b[i] = SpecialBytes[rand.Intn(len(SpecialBytes))]
case 2:
b[i] = NumberBytes[rand.Intn(len(NumberBytes))]
case 3:
b[i] = UpperLetterBytes[rand.Intn(len(UpperLetterBytes))]
}
}
return string(b)
Expand Down Expand Up @@ -100,7 +103,7 @@ func (HyperVHostTestResource) rebootVirtualMachine(ctx context.Context, clients
func (r HyperVHostTestResource) PrepareHostTestSteps(data acceptance.TestData, adminPwd string) (steps []acceptance.TestStep) {
return []acceptance.TestStep{
{
Config: r.recovery(data),
Config: r.recovery(data, true),
Check: acceptance.ComposeTestCheckFunc(
// set the registration key value to environment variable.
data.CheckWithClientForResource(r.generateHyperVHostRegistrationCert(func(xmlContent string) error {
Expand All @@ -109,14 +112,14 @@ func (r HyperVHostTestResource) PrepareHostTestSteps(data acceptance.TestData, a
),
},
{
Config: r.hyperVTemplate(data, adminPwd), // split complete template into two parts to reboot the server.
Config: r.hyperVTemplate(data, adminPwd, true), // split complete template into two parts to reboot the server.
Check: acceptance.ComposeTestCheckFunc(
data.CheckWithClientForResource(r.virtualMachineExists, "azurerm_windows_virtual_machine.host"),
data.CheckWithClientForResource(r.rebootVirtualMachine, "azurerm_windows_virtual_machine.host"),
),
},
{
Config: r.template(data, adminPwd),
Config: r.template(data, adminPwd, true),
},
}
}
Expand Down Expand Up @@ -304,7 +307,19 @@ resource "azurerm_network_interface_security_group_association" "hybrid" {
`
}

func (r HyperVHostTestResource) recovery(data acceptance.TestData) string {
// in creating, it should create `hyperv_site` before connect host to it
// but it needs to remove the `hyperv_site` before deleting the host.
func (r HyperVHostTestResource) recovery(data acceptance.TestData, includeSite bool) string {
siteBlock := ""
if includeSite {
siteBlock = `
resource "azurerm_site_recovery_services_vault_hyperv_site" "test" {
name = local.recovery_site_name
recovery_vault_id = azurerm_recovery_services_vault.test.id
}
`
}

return fmt.Sprintf(`
%s

Expand All @@ -317,14 +332,11 @@ resource "azurerm_recovery_services_vault" "test" {
soft_delete_enabled = false
}

resource "azurerm_site_recovery_services_vault_hyperv_site" "test" {
name = local.recovery_site_name
recovery_vault_id = azurerm_recovery_services_vault.test.id
}
`, r.base(data))
%s
`, r.base(data), siteBlock)
}

func (r HyperVHostTestResource) hyperVTemplate(data acceptance.TestData, adminPwd string) string {
func (r HyperVHostTestResource) hyperVTemplate(data acceptance.TestData, adminPwd string, includeSite bool) string {
return fmt.Sprintf(`
%[1]s

Expand Down Expand Up @@ -455,10 +467,11 @@ resource "azurerm_windows_virtual_machine" "host" {
%[3]s

%[4]s
`, r.recovery(data), adminPwd, r.keyVault(), r.securityGroup())
`, r.recovery(data, includeSite), adminPwd, r.keyVault(), r.securityGroup())
}

func (r HyperVHostTestResource) template(data acceptance.TestData, adminPwd string) string {
// for resources need a connected host, remove site and these resources and then remove the host, or it may fail.
func (r HyperVHostTestResource) template(data acceptance.TestData, adminPwd string, includeSite bool) string {
return fmt.Sprintf(`
%s
# register the server could only be done by CustomScriptExtension because it requires local admin to run.
Expand Down Expand Up @@ -530,5 +543,5 @@ resource "azurerm_virtual_machine_extension" "script" {
]
}

`, r.hyperVTemplate(data, adminPwd), HostName)
`, r.hyperVTemplate(data, adminPwd, includeSite), HostName)
}
1 change: 1 addition & 0 deletions internal/services/recoveryservices/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (r Registration) Resources() []sdk.Resource {
ReplicationPolicyHyperVResource{},
HyperVSiteResource{},
HyperVReplicationPolicyAssociationResource{},
SiteRecoveryHyperVReplicatedVMResource{},
}
}

Expand Down
Loading