diff --git a/inttests/replication_test.go b/inttests/replication_async_test.go similarity index 100% rename from inttests/replication_test.go rename to inttests/replication_async_test.go diff --git a/inttests/replication_sync_test.go b/inttests/replication_sync_test.go new file mode 100644 index 0000000..150c129 --- /dev/null +++ b/inttests/replication_sync_test.go @@ -0,0 +1,158 @@ +/* + * + * Copyright © 2024 Dell Inc. or its subsidiaries. All Rights Reserved. + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-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 inttests + +import ( + "context" + "os" + "testing" + + "github.com/dell/gopowerstore" + "github.com/joho/godotenv" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ReplicationTestSuiteSync struct { + suite.Suite + remoteSystem string + remoteSystemMIP string + randomString string + remoteClient gopowerstore.Client + pp gopowerstore.CreateResponse + vg gopowerstore.CreateResponse + rr gopowerstore.CreateResponse + vol gopowerstore.CreateResponse +} + +func (suite *ReplicationTestSuiteSync) SetupSuite() { + // instead of completely hardcoded/constant string, let's make it dynamic + // in case if TearDownSuite doesn't run at the end, we will not be blocked for next round of testing + suite.randomString = randString(8) + suite.remoteSystem, suite.remoteSystemMIP = getRemoteSystemSync(suite.T(), suite) + err := godotenv.Load("GOPOWERSTORE_TEST.env") + if err != nil { + return + } + user := os.Getenv("GOPOWERSTORE_USERNAME") + pass := os.Getenv("GOPOWERSTORE_PASSWORD") + + clientOptions := &gopowerstore.ClientOptions{} + clientOptions.SetInsecure(true) + client, err := gopowerstore.NewClientWithArgs("https://"+suite.remoteSystemMIP+"/api/rest", user, pass, clientOptions) + if err != nil { + return + } + suite.remoteClient = client +} + +func (suite *ReplicationTestSuiteSync) TearDownSuite() { + vg, err := C.GetVolumeGroup(context.Background(), suite.vg.ID) + assert.NoError(suite.T(), err) + pp, err := C.GetProtectionPolicyByName(context.Background(), "intcsi"+suite.randomString+"-pptst") + assert.NoError(suite.T(), err) + rr, err := C.GetReplicationRuleByName(context.Background(), "intcsi"+suite.randomString+"-ruletst") + assert.NoError(suite.T(), err) + if len(rr.ProtectionPolicies) != 1 || len(pp.ReplicationRules) != 1 || len(vg.Volumes) != 1 || len(pp.VolumeGroups) != 1 { + suite.T().Fail() + } + C.ModifyVolumeGroup(context.Background(), &gopowerstore.VolumeGroupModify{ProtectionPolicyID: ""}, suite.vg.ID) + C.RemoveMembersFromVolumeGroup(context.Background(), &gopowerstore.VolumeGroupMembers{VolumeIDs: []string{suite.vol.ID}}, suite.vg.ID) + C.ModifyVolume(context.Background(), &gopowerstore.VolumeModify{ProtectionPolicyID: ""}, suite.vol.ID) + C.DeleteProtectionPolicy(context.Background(), suite.pp.ID) + C.DeleteReplicationRule(context.Background(), suite.rr.ID) + C.DeleteVolumeGroup(context.Background(), suite.vg.ID) + vgid, err := suite.remoteClient.GetVolumeGroupByName(context.Background(), "intcsi"+suite.randomString+"-vgtst") + if err != nil { + logrus.Info(err) + } + suite.remoteClient.DeleteVolumeGroup(context.Background(), vgid.ID) + C.DeleteVolume(context.Background(), nil, suite.vol.ID) +} + +func getRemoteSystemSync(t *testing.T, suite *ReplicationTestSuiteSync) (string, string) { + resp, err := C.GetAllRemoteSystems(context.Background()) + skipTestOnError(t, err) + if len(resp) == 0 { + t.Skip("Skipping test as there are no remote systems configured on array.") + } + // try to find the working remote system from the list of all available/configured remoteSystems + for i := range resp { + rs, err := C.GetRemoteSystem(context.Background(), resp[i].ID) + assert.NoError(t, err) + assert.Equal(t, rs.ID, resp[i].ID) + // create replicationRule and Protection policy beforeHand to check if remote system is working fine or not + suite.rr, err = C.CreateReplicationRule(context.Background(), &gopowerstore.ReplicationRuleCreate{ + Name: "intcsi" + suite.randomString + "-ruletst", + Rpo: gopowerstore.RpoZero, + RemoteSystemID: rs.ID, + }) + assert.NoError(t, err) + + suite.pp, err = C.CreateProtectionPolicy(context.Background(), &gopowerstore.ProtectionPolicyCreate{ + Name: "intcsi" + suite.randomString + "-pptst", + ReplicationRuleIDs: []string{suite.rr.ID}, + }) + + if err == nil { + return resp[i].ID, resp[i].ManagementAddress + } + // need to delete replication rule created earlier with the remoteIP not able to create Protection policy + C.DeleteReplicationRule(context.Background(), suite.rr.ID) + suite.rr.ID = "" + } + t.Skip("Skipping test as there are no working remote systems configured on array.") + return "", "" +} + +func (suite *ReplicationTestSuiteSync) TestReplicationSync() { + t := suite.T() + + // get the remote powerstore system + remoteSystem := suite.remoteSystem + rs, err := C.GetRemoteSystem(context.Background(), remoteSystem) + assert.NoError(t, err) + assert.Equal(t, rs.ID, remoteSystem) + + // create a volume group with a protection policy + // A protection policy with a synchronous replication rule can only be applied to a write-order consistent volume group + suite.vg, err = C.CreateVolumeGroup(context.Background(), &gopowerstore.VolumeGroupCreate{ + Name: "intcsi" + suite.randomString + "-vgtst", + ProtectionPolicyID: suite.pp.ID, + IsWriteOrderConsistent: true, + }) + assert.NoError(t, err) + + // create a volume within the volume group + volName := "intcsi" + suite.randomString + "-voltst" + size := int64(OneMB) + suite.vol, err = C.CreateVolume(context.Background(), &gopowerstore.VolumeCreate{ + Name: &volName, + Size: &size, + VolumeGroupID: suite.vg.ID, + }) + assert.NoError(t, err) + + // get the volume group from the volume ID + volID := suite.vol.ID + _, err = C.GetVolumeGroupsByVolumeID(context.Background(), volID) + assert.NoError(t, err) +} + +func TestReplicationSuiteSync(t *testing.T) { + suite.Run(t, new(ReplicationTestSuiteSync)) +} diff --git a/replication_test.go b/replication_test.go index 84ff206..5db8579 100644 --- a/replication_test.go +++ b/replication_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2021-2023 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2021-2024 Dell Inc. or its subsidiaries. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,24 @@ func TestClientIMPL_CreateReplicationRule(t *testing.T) { assert.Equal(t, volID, resp.ID) } +func TestClientIMPL_CreateReplicationRuleSync(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + respData := fmt.Sprintf(`{"id": "%s"}`, volID) + httpmock.RegisterResponder("POST", replicationRuleMockURL, + httpmock.NewStringResponder(201, respData)) + + createReq := ReplicationRuleCreate{ + Name: "rr-test", + Rpo: RpoZero, + RemoteSystemID: "XX-0000X", + } + + resp, err := C.CreateReplicationRule(context.Background(), &createReq) + assert.Nil(t, err) + assert.Equal(t, volID, resp.ID) +} + func TestClientIMPL_DeleteProtectionPolicy(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/replication_types.go b/replication_types.go index 1c4ccd6..c615465 100644 --- a/replication_types.go +++ b/replication_types.go @@ -31,6 +31,7 @@ const ( RpoSixHours RPOEnum = "Six_Hours" RpoTwelveHours RPOEnum = "Twelve_Hours" RpoOneDay RPOEnum = "One_Day" + RpoZero RPOEnum = "Zero" RsStateInitializing RSStateEnum = "Initializing" RsStateOk RSStateEnum = "OK" RsStateSynchronizing RSStateEnum = "Synchronizing" @@ -49,7 +50,7 @@ const ( func (rpo RPOEnum) IsValid() error { switch rpo { - case RpoFiveMinutes, RpoFifteenMinutes, RpoThirtyMinutes, RpoOneHour, RpoSixHours, RpoTwelveHours, RpoOneDay: + case RpoFiveMinutes, RpoFifteenMinutes, RpoThirtyMinutes, RpoOneHour, RpoSixHours, RpoTwelveHours, RpoOneDay, RpoZero: return nil } return errors.New("invalid rpo type") @@ -60,6 +61,7 @@ type ReplicationRuleCreate struct { // Name of the replication rule. Name string `json:"name"` // Recovery point objective (RPO), which is the acceptable amount of data, measured in units of time, that may be lost in case of a failure. + // If RPO is Zero, it indicates the replication_type is 'sync'. Rpo RPOEnum `json:"rpo"` // Unique identifier of the remote system to which this rule will replicate the associated resources RemoteSystemID string `json:"remote_system_id"` @@ -71,6 +73,7 @@ type ReplicationRule struct { // Name of replication rule Name string `json:"name"` // Rpo (Recovery point objective), which is the acceptable amount of data, measured in units of time, that may be lost in case of a failure. + // If RPO is Zero, it indicates the replication_type is 'sync'. Rpo RPOEnum `json:"rpo"` // RemoteSystemID - unique identifier of the remote system to which this rule will replicate the associated resources. RemoteSystemID string `json:"remote_system_id"` diff --git a/replication_types_test.go b/replication_types_test.go new file mode 100644 index 0000000..6a94128 --- /dev/null +++ b/replication_types_test.go @@ -0,0 +1,84 @@ +/* + * + * Copyright © 2024 Dell Inc. or its subsidiaries. All Rights Reserved. + * + * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-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 gopowerstore + +import "testing" + +func TestRPOEnum_IsValid(t *testing.T) { + const ( + RpoInvalid RPOEnum = "RPO_Invalid" + ) + + tests := []struct { + name string + rpo RPOEnum + wantErr bool + }{ + { + name: "Rpo FiveMinutes", + rpo: RpoFiveMinutes, + wantErr: false, + }, + { + name: "Rpo FifteenMinutes", + rpo: RpoFifteenMinutes, + wantErr: false, + }, + { + name: "Rpo ThirtyMinutes", + rpo: RpoThirtyMinutes, + wantErr: false, + }, + { + name: "Rpo OneHour", + rpo: RpoOneHour, + wantErr: false, + }, + { + name: "Rpo SixHours", + rpo: RpoSixHours, + wantErr: false, + }, + { + name: "Rpo TwelveHours", + rpo: RpoTwelveHours, + wantErr: false, + }, + { + name: "Rpo OneDay", + rpo: RpoOneDay, + wantErr: false, + }, + { + name: "Rpo Zero", + rpo: RpoZero, + wantErr: false, + }, + { + name: "Rpo Invalid", + rpo: RpoInvalid, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.rpo.IsValid(); (err != nil) != tt.wantErr { + t.Errorf("RPOEnum.IsValid() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}