From 7c03b96d7efa3b518ecc602fedb6aa3e32016171 Mon Sep 17 00:00:00 2001 From: Priyanshu Krishnan <118425422+Krishnan-Priyanshu@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:35:16 +0530 Subject: [PATCH] Replication job (#242) * added replication-job * added wait time * added AT Replication Job * revert replication-job * added dell/common-github-actions/go-code-formatter-vetter@main to workflow * fmt test * added Golang Validation * fix uses * added go static analysis * dir fix * Revert Static-Analysis Job * added port to provisioner connection * added isDelete Flag * delete all synciq jobs in destroy * rename main dir to terraformAT * idempotency test * no read refresh * added delete custom logic * state remain same on notfound job * fix test * added const * added base doc * update action doc * enable synciq service on testing --- .github/workflows/terraform-ci.yml | 5 +- docs/resources/synciq_replication_job.md | 46 +++ powerscale/helper/synciq_replication_job.go | 50 ++++ powerscale/models/synciq_replication_job.go | 8 + powerscale/provider/provider.go | 1 + .../synciq_replication_job_resource.go | 264 ++++++++++++++++++ .../provider/synciq_replication_job_test.go | 154 ++++++++++ 7 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 docs/resources/synciq_replication_job.md create mode 100644 powerscale/helper/synciq_replication_job.go create mode 100644 powerscale/provider/synciq_replication_job_resource.go create mode 100644 powerscale/provider/synciq_replication_job_test.go diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml index 14115451..e131091b 100644 --- a/.github/workflows/terraform-ci.yml +++ b/.github/workflows/terraform-ci.yml @@ -40,10 +40,11 @@ jobs: uses: actions/checkout@v3 - name: Extract go client run: make clean extract-client - - name: Run the formatter, linter, and vetter - uses: dell/common-github-actions/go-code-formatter-linter-vetter@main + - name: Run the formatter and vetter + uses: dell/common-github-actions/go-code-formatter-vetter@main with: directories: ./powerscale/... + sanitize: name: Check for forbidden words runs-on: ubuntu-latest diff --git a/docs/resources/synciq_replication_job.md b/docs/resources/synciq_replication_job.md new file mode 100644 index 00000000..d9c4384f --- /dev/null +++ b/docs/resources/synciq_replication_job.md @@ -0,0 +1,46 @@ +--- +# Copyright (c) 2024 Dell Inc., or its subsidiaries. All Rights Reserved. +# +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://mozilla.org/MPL/2.0/ +# +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +title: "powerscale_synciq_replication_job resource" +linkTitle: "powerscale_synciq_replication_job" +page_title: "powerscale_synciq_replication_job Resource - terraform-provider-powerscale" +subcategory: "" +description: |- + Resource for managing SyncIQReplicationJobResource on OpenManage Enterprise. +--- + +# powerscale_synciq_replication_job (Resource) + +Resource for managing SyncIQReplicationJobResource on OpenManage Enterprise. + + + + + +## Schema + +### Required + +- `action` (String) Action for the job - run, test, resync_prep, allow_write, allow_write_revert +- `id` (String) ID/Name of the policy + +### Optional + +- `is_paused` (Boolean) change job state to running or paused. +- `wait_time` (Number) Wait Time for the job + +Unless specified otherwise, all fields of this resource can be updated. + diff --git a/powerscale/helper/synciq_replication_job.go b/powerscale/helper/synciq_replication_job.go new file mode 100644 index 00000000..eb69c4c7 --- /dev/null +++ b/powerscale/helper/synciq_replication_job.go @@ -0,0 +1,50 @@ +package helper + +import ( + "context" + powerscale "dell/powerscale-go-client" + "net/http" + "terraform-provider-powerscale/client" +) + +// GetSyncIQReplicationJob get syncIQ replication job. +func GetSyncIQReplicationJob(ctx context.Context, client *client.Client, jobID string) (*powerscale.V1SyncJobsExtended, *http.Response, error) { + resp, httpResp, err := client.PscaleOpenAPIClient.SyncApi.GetSyncv1SyncJob(ctx, jobID).Execute() + if err != nil { + return nil, httpResp, err + } + return resp, httpResp, err +} + +// CreateSyncIQReplicationJob create syncIQ replication job. +func CreateSyncIQReplicationJob(ctx context.Context, client *client.Client, job powerscale.V1SyncJob) (string, error) { + if job.Action != nil && *job.Action == "run" { + job.Action = nil + } + resp, _, err := client.PscaleOpenAPIClient.SyncApi.CreateSyncv1SyncJob(ctx).V1SyncJob(job).Execute() + if err != nil { + return "", err + } + return resp.Id, nil +} + +// UpdateSyncIQReplicationJob update syncIQ replication job. +func UpdateSyncIQReplicationJob(ctx context.Context, client *client.Client, jobID string, job powerscale.V1SyncJobExtendedExtended) (*http.Response, error) { + resp, err := client.PscaleOpenAPIClient.SyncApi.UpdateSyncv1SyncJob(ctx, jobID).V1SyncJob(job).Execute() + if err != nil { + return resp, err + } + return resp, nil +} + +// DeleteSyncIQReplicationJob delete syncIQ replication job. +func DeleteSyncIQReplicationJob(ctx context.Context, client *client.Client, jobID string) error { + deleteJob := powerscale.V1SyncJobExtendedExtended{ + State: "canceled", + } + resp, err := UpdateSyncIQReplicationJob(ctx, client, jobID, deleteJob) + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil // already deleted + } + return err +} diff --git a/powerscale/models/synciq_replication_job.go b/powerscale/models/synciq_replication_job.go index c25f6b73..7a4a6e82 100644 --- a/powerscale/models/synciq_replication_job.go +++ b/powerscale/models/synciq_replication_job.go @@ -21,6 +21,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +// SyncIQReplicationJobResourceModel describes the SyncIQ Replication Job resource data model. +type SyncIQReplicationJobResourceModel struct { + Id types.String `tfsdk:"id"` + Action types.String `tfsdk:"action"` + IsPaused types.Bool `tfsdk:"is_paused"` + WaitTime types.Int64 `tfsdk:"wait_time"` +} + // SyncIQReplicationJobDataSourceModel describes the SyncIQ Replication Job datasource data model. type SyncIQReplicationJobDataSourceModel struct { SyncIQReplicationJobs []SyncIQReplicationJobModel `tfsdk:"synciq_jobs"` diff --git a/powerscale/provider/provider.go b/powerscale/provider/provider.go index 3093b4df..034ab512 100644 --- a/powerscale/provider/provider.go +++ b/powerscale/provider/provider.go @@ -208,6 +208,7 @@ func (p *PscaleProvider) Resources(ctx context.Context) []func() resource.Resour NewWriteableSnapshotResource, NewSnapshotRestoreResource, NewNfsAliasResource, + NewSyncIQReplicationJobResource, } } diff --git a/powerscale/provider/synciq_replication_job_resource.go b/powerscale/provider/synciq_replication_job_resource.go new file mode 100644 index 00000000..c6cba4ec --- /dev/null +++ b/powerscale/provider/synciq_replication_job_resource.go @@ -0,0 +1,264 @@ +package provider + +import ( + "context" + powerscale "dell/powerscale-go-client" + "fmt" + "net/http" + "terraform-provider-powerscale/client" + "terraform-provider-powerscale/powerscale/helper" + "terraform-provider-powerscale/powerscale/models" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var ( + _ resource.Resource = &synciqPolicyResource{} + _ resource.ResourceWithConfigure = &synciqPolicyResource{} + _ resource.ResourceWithImportState = &synciqPolicyResource{} +) + +const ( + paused = "paused" + running = "running" +) + +// NewSyncIQReplicationJobResource is a helper function to simplify the provider implementation. +func NewSyncIQReplicationJobResource() resource.Resource { + return &SyncIQReplicationJobResource{} +} + +// SyncIQReplicationJobResource is the resource implementation. +type SyncIQReplicationJobResource struct { + client *client.Client +} + +// Configure implements resource.ResourceWithConfigure. +func (s *SyncIQReplicationJobResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + pscaleClient, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + s.client = pscaleClient +} + +// Metadata returns the resource type name. +func (r *SyncIQReplicationJobResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_synciq_replication_job" +} + +// Schema defines the schema for the resource. +func (r *SyncIQReplicationJobResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Resource for managing SyncIQReplicationJobResource on OpenManage Enterprise.", + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + Description: "ID/Name of the policy", + MarkdownDescription: "ID/Name of the policy", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "action": schema.StringAttribute{ + Required: true, + Description: "Action for the job - run, test, resync_prep, allow_write, allow_write_revert", + MarkdownDescription: "Action for the job - run, test, resync_prep, allow_write, allow_write_revert", + Validators: []validator.String{ + stringvalidator.OneOf("run", "test", "resync_prep", "allow_write", "allow_write_revert"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "is_paused": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "change job state to running or paused.", + MarkdownDescription: "change job state to running or paused.", + Default: booldefault.StaticBool(false), + }, + "wait_time": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Wait Time for the job", + MarkdownDescription: "Wait Time for the job", + Default: int64default.StaticInt64(5), + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *SyncIQReplicationJobResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource create : Started") + //Get Plan Data + var plan models.SyncIQReplicationJobResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if plan.IsPaused.ValueBool() { + resp.Diagnostics.AddError("Config Error", "SyncIQ Replication Job cannot be paused befor job creation.") + } + + var createJob powerscale.V1SyncJob + // Get param from tf input + err := helper.ReadFromState(ctx, &plan, &createJob) + if err != nil { + resp.Diagnostics.AddError( + "Error reading create plan", + err.Error(), + ) + return + } + _, err = helper.CreateSyncIQReplicationJob(ctx, r.client, createJob) + if err != nil { + errStr := "Could not create syncIQ Replication Job with error: " + message := helper.GetErrorString(err, errStr) + resp.Diagnostics.AddError( + "Error creating syncIQ Replication Job", + message, + ) + return + } + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource create: updating state finished, saving ...") + // Save into State + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource create: finish") +} + +// Read refreshes the Terraform state with the latest data. +func (r *SyncIQReplicationJobResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource read: started") + var state models.SyncIQReplicationJobResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + time.Sleep(time.Duration(state.WaitTime.ValueInt64()) * time.Second) + tflog.Debug(ctx, "calling get syncIQ Replication Job on powerscale client") + readState, httpResp, err := helper.GetSyncIQReplicationJob(ctx, r.client, state.Id.ValueString()) + if err != nil { + if httpResp != nil && httpResp.StatusCode == http.StatusNotFound { + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource read: finished") + return + } + errStr := "Could not read syncIQ Replication Job with error: " + message := helper.GetErrorString(err, errStr) + resp.Diagnostics.AddError( + "Error reading syncIQ Replication Job", + message, + ) + return + } + if len(readState.Jobs) > 0 { + job := readState.Jobs[0] + state.Id = types.StringValue(job.PolicyName) + if job.State == "running" { + state.IsPaused = types.BoolValue(false) + } else if job.State == "paused" { + state.IsPaused = types.BoolValue(true) + } + } + + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource read: finished reading state") + //Save into State + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource read: finished") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *SyncIQReplicationJobResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource update: started") + var state, plan models.SyncIQReplicationJobResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + diags = req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !plan.IsPaused.Equal(state.IsPaused) { + isPause := running + if plan.IsPaused.ValueBool() { + isPause = paused + } + updateJob := powerscale.V1SyncJobExtendedExtended{ + State: isPause, + } + _, err := helper.UpdateSyncIQReplicationJob(ctx, r.client, state.Id.ValueString(), updateJob) + if err != nil { + errStr := "Could not update syncIQ Replication Job with error: " + message := helper.GetErrorString(err, errStr) + resp.Diagnostics.AddError( + "Error updating syncIQ Replication Job", + message, + ) + return + } + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + } + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource update: finished") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *SyncIQReplicationJobResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource delete: started") + var state models.SyncIQReplicationJobResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + err := helper.DeleteSyncIQReplicationJob(ctx, r.client, state.Id.ValueString()) + if err != nil { + errStr := "Could not delete syncIQ Replication Job with error: " + message := helper.GetErrorString(err, errStr) + resp.Diagnostics.AddError( + "Error deleting syncIQ Replication Job", + message, + ) + } + time.Sleep(time.Duration(state.WaitTime.ValueInt64()) * time.Second) + resp.State.RemoveResource(ctx) + tflog.Trace(ctx, "resource_SyncIQReplicationJobResource delete: finished") +} diff --git a/powerscale/provider/synciq_replication_job_test.go b/powerscale/provider/synciq_replication_job_test.go new file mode 100644 index 00000000..578b4bb4 --- /dev/null +++ b/powerscale/provider/synciq_replication_job_test.go @@ -0,0 +1,154 @@ +/* +Copyright (c) 2024 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "fmt" + "regexp" + "terraform-provider-powerscale/powerscale/helper" + "testing" + + "github.com/bytedance/mockey" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccSyncIQReplicationJobResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ProviderConfig + errorReplicationJob, + ExpectError: regexp.MustCompile(`.*SyncIQ Replication Job cannot be paused befor job creation.*`), + }, + { + PreConfig: func() { + FunctionMocker = mockey.Mock(helper.ReadFromState).Return(fmt.Errorf("Error reading create plan")).Build() + }, + Config: ProviderConfig + SetupReplication() + createReplicationJob, + ExpectError: regexp.MustCompile("Error reading create plan"), + }, + { + PreConfig: func() { + FunctionMocker.Release() + FunctionMocker = mockey.Mock(helper.CreateSyncIQReplicationJob).Return(nil, fmt.Errorf("Error creating syncIQ Replication Job")).Build() + }, + Config: ProviderConfig + SetupReplication() + createReplicationJob, + ExpectError: regexp.MustCompile("Error creating syncIQ Replication Job"), + }, + { + // create synciq replication job positive test + PreConfig: func() { + FunctionMocker.Release() + }, + Config: ProviderConfig + SetupReplication() + createReplicationJob, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("powerscale_synciq_replication_job.job1", "id", "TerraformPolicy"), + ), + }, + { + PreConfig: func() { + FunctionMocker = mockey.Mock(helper.GetSyncIQReplicationJob).Return(nil, nil, fmt.Errorf("Error reading syncIQ Replication Job")).Build() + }, + Config: ProviderConfig + SetupReplication() + updateReplicationJob, + ExpectError: regexp.MustCompile("Error reading syncIQ Replication Job"), + }, + { + PreConfig: func() { + FunctionMocker.Release() + FunctionMocker = mockey.Mock(helper.UpdateSyncIQReplicationJob).Return(nil, fmt.Errorf("Error updating syncIQ Replication Job")).Build() + }, + Config: ProviderConfig + SetupReplication() + updateReplicationJob, + ExpectError: regexp.MustCompile("Error updating syncIQ Replication Job"), + }, + { + PreConfig: func() { + FunctionMocker.Release() + }, + Config: ProviderConfig + SetupReplication() + updateReplicationJob, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("powerscale_synciq_replication_job.job1", "id", "TerraformPolicy"), + ), + }, + }, + }) +} + +func SetupReplication() string { + connection := fmt.Sprintf(` + connection { + host = "%s" + port = %s + user = "%s" + password = "%s" + type = "ssh" + } + `, powerScaleSSHIP, powerscaleSSHPort, powerscaleUsername, powerscalePassword) + + createLargeFile := fmt.Sprintf(` + resource "terraform_data" "large_file" { + provisioner "remote-exec" { + inline = [ + "mkdir -p /ifs/terraformAT/source", + "head -c 10000000 /dev/urandom > /ifs/terraformAT/source/large_file.dat", + "mkdir -p /ifs/terraformAT/target", + " isi sync settings modify --service=on", + "isi sync rules create bandwidth 00:00-23:59 X-S 129", + "echo 'confirm create policy' | isi sync policies create --name=TerraformPolicy --source-root-path=/ifs/terraformAT/source/ --target-host=%s --target-path=/ifs/terraformAT/target/ --action=sync", + "sleep 10", + ] + `+connection+` + } + + provisioner "remote-exec" { + when = destroy + inline = [ + "rm -rf /ifs/terraform", + "echo 'yes' | isi sync rules delete bw-0", + "echo 'yes' | isi sync job cancel --all", + "echo 'yes' | isi sync pol rm --all", + ] + `+connection+` + } + }`, powerScaleSSHIP) + return createLargeFile +} + +var createReplicationJob = ` +resource "powerscale_synciq_replication_job" "job1" { + action = "run" + id = "TerraformPolicy" + is_paused = false + depends_on = [terraform_data.large_file] +} +` + +var updateReplicationJob = ` +resource "powerscale_synciq_replication_job" "job1" { + action = "run" + id = "TerraformPolicy" + is_paused = true + depends_on = [terraform_data.large_file] +} +` + +var errorReplicationJob = ` +resource "powerscale_synciq_replication_job" "errorJob" { + action = "resync_prep" + id = "TerraformPolicy" + is_paused = true +}`