diff --git a/docs/resources/gaussdb_opengauss_restore.md b/docs/resources/gaussdb_opengauss_restore.md new file mode 100644 index 0000000000..228d18a561 --- /dev/null +++ b/docs/resources/gaussdb_opengauss_restore.md @@ -0,0 +1,89 @@ +--- +subcategory: "GaussDB" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_gaussdb_opengauss_restore" +description: |- + Use this resource to restore a GaussDB OpenGauss instance with a backup within HuaweiCloud. +--- + +# huaweicloud_gaussdb_opengauss_restore + +Use this resource to restore a GaussDB OpenGauss instance with a backup within HuaweiCloud. + +-> **NOTE:** Deleting restoration record is not supported. If you destroy a resource of restoration record, +the restoration record is only removed from the state, but it remains in the cloud. And the instance doesn't return to +the state before restoration. + +## Example Usage + +### restore by backup_id + +```hcl +variable "target_instance_id" {} +variable "source_instance_id" {} +variable "backup_id" {} + +resource "huaweicloud_gaussdb_opengauss_restore" "test" { + target_instance_id = var.target_instance_id + source_instance_id = var.source_instance_id + type = "backup" + backup_id = var.backup_id +} +``` + +### restore by timestamp + +```hcl +variable "target_instance_id" {} +variable "source_instance_id" {} + +resource "huaweicloud_gaussdb_opengauss_restore" "test" { + target_instance_id = var.target_instance_id + source_instance_id = var.source_instance_id + type = "timestamp" + restore_time = 1673852043000 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) The region in which to create the GaussDB OpenGauss restore resource. If omitted, + the provider-level region will be used. Changing this creates a new resource. + +* `target_instance_id` - (Required, String, ForceNew) Specifies the target instance ID. + + Changing this creates a new resource. + +* `source_instance_id` - (Required, String, ForceNew) Specifies the source instance ID. + + Changing this creates a new resource. + +* `type` - (Required, String, ForceNew) Specifies the restoration type. Value options: + + **backup**: indicates restoration from backup files. In this mode, `backup_id` is mandatory. + + **timestamp**: indicates point-in-time restoration. In this mode, `restore_time` is mandatory. + + Changing this creates a new resource. + +* `backup_id` - (Optional, String, ForceNew) Specifies the ID of the backup to be restored. It indicates the ID of the + full backup corresponding to schema_type. This parameter must be specified when the backup file is used for restoration. + + Changing this creates a new resource. + +* `restore_time` - (Optional, String, ForceNew) Specifies the time point of data restoration in the UNIX timestamp format. + The unit is millisecond and the time zone is UTC. + + Changing this creates a new resource. + +## Attribute Reference + +In addition to all arguments above, the following attribute is exported: + +* `id` - The resource ID. The value is the restore job ID. + +## Timeouts + +This resource provides the following timeouts configuration options: + +* `create` - Default is 60 minutes. diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index e5ab6e3c56..6f8404926b 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1773,6 +1773,7 @@ func Provider() *schema.Provider { "huaweicloud_gaussdb_opengauss_database": gaussdb.ResourceOpenGaussDatabase(), "huaweicloud_gaussdb_opengauss_backup": gaussdb.ResourceGaussDBOpenGaussBackup(), "huaweicloud_gaussdb_opengauss_backup_stop": gaussdb.ResourceOpenGaussBackupStop(), + "huaweicloud_gaussdb_opengauss_restore": gaussdb.ResourceOpenGaussRestore(), "huaweicloud_gaussdb_opengauss_eip_associate": gaussdb.ResourceOpenGaussEipAssociate(), "huaweicloud_gaussdb_opengauss_primary_standby_switch": gaussdb.ResourceOpenGaussPrimaryStandbySwitch(), "huaweicloud_gaussdb_opengauss_parameter_template": gaussdb.ResourceOpenGaussParameterTemplate(), diff --git a/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore_test.go b/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore_test.go new file mode 100644 index 0000000000..67bb664486 --- /dev/null +++ b/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore_test.go @@ -0,0 +1,214 @@ +package gaussdb + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance/common" +) + +func TestAccOpenGaussRestore_basic(t *testing.T) { + name := acceptance.RandomAccResourceName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckEpsID(t) + acceptance.TestAccPreCheckHighCostAllow(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccOpenGaussRestoreonfig_basic(name), + }, + }, + }) +} + +func TestAccOpenGaussRestore_withTimestamp(t *testing.T) { + name := acceptance.RandomAccResourceName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckEpsID(t) + acceptance.TestAccPreCheckHighCostAllow(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccOpenGaussRestoreonfig_withTimestamp(name), + }, + }, + }) +} + +func testAccOpenGaussRestoreonfig_base(name string) string { + return fmt.Sprintf(` +%[1]s + +data "huaweicloud_availability_zones" "test" {} + +data "huaweicloud_gaussdb_opengauss_flavors" "test" { + version = "8.201" + ha_mode = "centralization_standard" +} + +resource "huaweicloud_networking_secgroup_rule" "in_v4_tcp_opengauss" { + security_group_id = huaweicloud_networking_secgroup.test.id + ethertype = "IPv4" + direction = "ingress" + protocol = "tcp" + remote_ip_prefix = "0.0.0.0/0" +} + +resource "huaweicloud_networking_secgroup_rule" "in_v4_tcp_opengauss_egress" { + security_group_id = huaweicloud_networking_secgroup.test.id + ethertype = "IPv4" + direction = "egress" + protocol = "tcp" + remote_ip_prefix = "0.0.0.0/0" +} + +resource "huaweicloud_gaussdb_opengauss_instance" "source" { + depends_on = [ + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss_egress + ] + + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[0].spec_code + name = "%[2]s_source" + password = "Huangwei!120521" + enterprise_project_id = "%[3]s" + + availability_zone = join(",", [data.huaweicloud_availability_zones.test.names[0], + data.huaweicloud_availability_zones.test.names[1], + data.huaweicloud_availability_zones.test.names[2]]) + + ha { + mode = "centralization_standard" + replication_mode = "sync" + consistency = "eventual" + instance_mode = "basic" + } + + volume { + type = "ULTRAHIGH" + size = 40 + } +} +`, common.TestBaseNetwork(name), name, acceptance.HW_ENTERPRISE_PROJECT_ID_TEST) +} + +func testAccOpenGaussRestoreonfig_basic(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_gaussdb_opengauss_instance" "target" { + depends_on = [ + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss_egress + ] + + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[0].spec_code + name = "%[2]s_target" + password = "Huangwei!120521" + enterprise_project_id = "%[3]s" + + availability_zone = join(",", [data.huaweicloud_availability_zones.test.names[0], + data.huaweicloud_availability_zones.test.names[1], + data.huaweicloud_availability_zones.test.names[2]]) + + ha { + mode = "centralization_standard" + replication_mode = "sync" + consistency = "eventual" + instance_mode = "basic" + } + + volume { + type = "ULTRAHIGH" + size = 40 + } +} + +resource "huaweicloud_gaussdb_opengauss_backup" "backup" { + instance_id = huaweicloud_gaussdb_opengauss_instance.source.id + name = "%[2]s_backup" +} + +resource "huaweicloud_gaussdb_opengauss_restore" "test" { + target_instance_id = huaweicloud_gaussdb_opengauss_instance.target.id + source_instance_id = huaweicloud_gaussdb_opengauss_instance.source.id + type = "backup" + backup_id = huaweicloud_gaussdb_opengauss_backup.backup.id +} +`, testAccOpenGaussRestoreonfig_base(name), name, acceptance.HW_ENTERPRISE_PROJECT_ID_TEST) +} + +func testAccOpenGaussRestoreonfig_withTimestamp(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_gaussdb_opengauss_instance" "target" { + depends_on = [ + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, + huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss_egress + ] + + vpc_id = huaweicloud_vpc.test.id + subnet_id = huaweicloud_vpc_subnet.test.id + security_group_id = huaweicloud_networking_secgroup.test.id + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[0].spec_code + name = "%[2]s_target" + password = "Huangwei!120521" + enterprise_project_id = "%[3]s" + + availability_zone = join(",", [data.huaweicloud_availability_zones.test.names[0], + data.huaweicloud_availability_zones.test.names[1], + data.huaweicloud_availability_zones.test.names[2]]) + + ha { + mode = "centralization_standard" + replication_mode = "sync" + consistency = "eventual" + instance_mode = "basic" + } + + volume { + type = "ULTRAHIGH" + size = 40 + } +} + +resource "huaweicloud_gaussdb_opengauss_backup" "backup" { + instance_id = huaweicloud_gaussdb_opengauss_instance.source.id + name = "%[2]s_backup" +} + +data "huaweicloud_gaussdb_opengauss_restore_time_ranges" "restore_time" { + depends_on = [huaweicloud_gaussdb_opengauss_backup.backup] + + instance_id = huaweicloud_gaussdb_opengauss_instance.source.id + date = split("T", huaweicloud_gaussdb_opengauss_backup.backup.end_time)[0] +} + +resource "huaweicloud_gaussdb_opengauss_restore" "test" { + target_instance_id = huaweicloud_gaussdb_opengauss_instance.target.id + source_instance_id = huaweicloud_gaussdb_opengauss_instance.source.id + type = "timestamp" + restore_time = data.huaweicloud_gaussdb_opengauss_restore_time_ranges.restore_time.restore_time[0].start_time +} +`, testAccOpenGaussRestoreonfig_base(name), name, acceptance.HW_ENTERPRISE_PROJECT_ID_TEST) +} diff --git a/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore.go b/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore.go new file mode 100644 index 0000000000..8bf188c3c8 --- /dev/null +++ b/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_restore.go @@ -0,0 +1,156 @@ +package gaussdb + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/chnsz/golangsdk" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API GaussDB POST /v3/{project_id}/instances/recovery +// @API GaussDB GET /v3/{project_id}/instances +// @API GaussDB GET /v3/{project_id}/jobs +func ResourceOpenGaussRestore() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceOpenGaussRestoreCreate, + ReadContext: resourceOpenGaussRestoreRead, + DeleteContext: resourceOpenGaussRestoreDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "target_instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "source_instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "backup_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "restore_time": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{"backup_id"}, + }, + }, + } +} + +func resourceOpenGaussRestoreCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + + var ( + httpUrl = "v3/{project_id}/instances/recovery" + product = "opengauss" + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating GaussDB client: %s", err) + } + + targetInstanceId := d.Get("target_instance_id").(string) + createPath := client.Endpoint + httpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + + createOpt := golangsdk.RequestOpts{KeepResponseBody: true} + createOpt.JSONBody = utils.RemoveNil(buildCreateRestoreBodyParams(d)) + + retryFunc := func() (interface{}, bool, error) { + res, err := client.Request("POST", createPath, &createOpt) + retry, err := handleMultiOperationsError(err) + return res, retry, err + } + r, err := common.RetryContextWithWaitForState(&common.RetryContextWithWaitForStateParam{ + Ctx: ctx, + RetryFunc: retryFunc, + WaitFunc: instanceStateRefreshFunc(client, d.Get("target_instance_id").(string)), + WaitTarget: []string{"ACTIVE"}, + Timeout: d.Timeout(schema.TimeoutCreate), + DelayTimeout: 10 * time.Second, + PollInterval: 10 * time.Second, + }) + if err != nil { + return diag.Errorf("error restoring GaussDB OpenGauss instance (%s): %s", targetInstanceId, err) + } + + createRespBody, err := utils.FlattenResponse(r.(*http.Response)) + if err != nil { + return diag.FromErr(err) + } + + jobId := utils.PathSearch("job_id", createRespBody, nil) + if jobId == nil { + return diag.Errorf("error restoring GaussDB OpenGauss instance(%s), job_id is not found in the response", + targetInstanceId) + } + + d.SetId(jobId.(string)) + + err = checkGaussDBOpenGaussJobFinish(ctx, client, jobId.(string), 120, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.Errorf("error waiting for restoring GaussDB OpenGauss instance(%s) to complete: %s", + targetInstanceId, err) + } + + return nil +} + +func buildCreateRestoreBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "source": map[string]interface{}{ + "instance_id": d.Get("source_instance_id"), + "type": d.Get("type"), + "backup_id": utils.ValueIgnoreEmpty(d.Get("backup_id")), + "restore_time": utils.ValueIgnoreEmpty(d.Get("restore_time")), + }, + "target": map[string]interface{}{ + "instance_id": d.Get("target_instance_id"), + }, + } + return bodyParams +} + +func resourceOpenGaussRestoreRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil +} + +func resourceOpenGaussRestoreDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + errorMsg := "Deleting restoration record is not supported. The restoration record is only removed from the state," + + " but it remains in the cloud. And the instance doesn't return to the state before restoration." + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: errorMsg, + }, + } +}