diff --git a/client.go b/client.go index 94d5f2d..30a8eff 100644 --- a/client.go +++ b/client.go @@ -190,7 +190,9 @@ type Client interface { GetVolumeGroupSnapshotByName(ctx context.Context, snapName string) (VolumeGroup, error) GetMaxVolumeSize(ctx context.Context) (int64, error) ConfigureMetroVolume(ctx context.Context, id string, config *MetroConfig) (resp MetroSessionResponse, err error) + ConfigureMetroVolumeGroup(ctx context.Context, id string, config *MetroConfig) (resp MetroSessionResponse, err error) EndMetroVolume(ctx context.Context, id string, options *EndMetroVolumeOptions) (resp EmptyResponse, err error) + EndMetroVolumeGroup(ctx context.Context, id string, options *EndMetroVolumeGroupOptions) (resp EmptyResponse, err error) } // ClientIMPL provides basic API client implementation diff --git a/go.mod b/go.mod index 831f190..39c5d56 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - go.mongodb.org/mongo-driver v1.16.1 // indirect + go.mongodb.org/mongo-driver v1.17.0 // indirect golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a163187..02ac9ec 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r40k= +go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/inttests/common.go b/inttests/common.go index 096ca7e..375acd8 100644 --- a/inttests/common.go +++ b/inttests/common.go @@ -85,3 +85,26 @@ func Includes(list *[]string, target string) bool { } return false } + +// GetRemoteSystemForMetro queries the source PowerStore array for configured remote systems +// to find a remote system capable of metro replication and returns the system if one is found +// and an empty RemoteSystem struct otherwise. +func GetRemoteSystemForMetro(client gopowerstore.Client, t *testing.T) gopowerstore.RemoteSystem { + systems, err := client.GetAllRemoteSystems(context.Background()) + if err != nil { + t.Skip("Could not get remote systems. Skipping test...") + } + + // try to find a valid remote system with Metro from the list of all available remote systems + for _, sys := range systems { + // check remote capabilities for metro and create MetroConfig if found + if Includes(&sys.Capabilities, string(gopowerstore.BlockMetro)) { + // make sure the connection is in a good state + if sys.DataConnectionState == string(gopowerstore.ConnStateOK) { + return sys + } + } + } + + return gopowerstore.RemoteSystem{} +} diff --git a/inttests/metrics_test.go b/inttests/metrics_test.go index ed9c967..437f6ce 100644 --- a/inttests/metrics_test.go +++ b/inttests/metrics_test.go @@ -163,22 +163,25 @@ func Test_PerformanceMetricsSmb2BuiltinclientByNode(t *testing.T) { func Test_PerformanceMetricsNfsByNode(t *testing.T) { resp, err := C.PerformanceMetricsNfsByNode(context.Background(), "N1", gopowerstore.FiveMins) checkAPIErr(t, err) - assert.NotEmpty(t, resp) - assert.Equal(t, "performance_metrics_nfs_by_node", resp[0].Entity) + if assert.NotEmpty(t, resp) { + assert.Equal(t, "performance_metrics_nfs_by_node", resp[0].Entity) + } } func Test_PerformanceMetricsNfsv3ByNode(t *testing.T) { resp, err := C.PerformanceMetricsNfsv3ByNode(context.Background(), "N1", gopowerstore.FiveMins) checkAPIErr(t, err) - assert.NotEmpty(t, resp) - assert.Equal(t, "performance_metrics_nfsv3_by_node", resp[0].Entity) + if assert.NotEmpty(t, resp) { + assert.Equal(t, "performance_metrics_nfsv3_by_node", resp[0].Entity) + } } func Test_PerformanceMetricsNfsv4ByNode(t *testing.T) { resp, err := C.PerformanceMetricsNfsv4ByNode(context.Background(), "N1", gopowerstore.FiveMins) checkAPIErr(t, err) - assert.NotEmpty(t, resp) - assert.Equal(t, "performance_metrics_nfsv4_by_node", resp[0].Entity) + if assert.NotEmpty(t, resp) { + assert.Equal(t, "performance_metrics_nfsv4_by_node", resp[0].Entity) + } } func Test_WearMetricsByDrive(t *testing.T) { diff --git a/inttests/volume_group_test.go b/inttests/volume_group_test.go new file mode 100644 index 0000000..0ded1ba --- /dev/null +++ b/inttests/volume_group_test.go @@ -0,0 +1,388 @@ +/* + * + * 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" + "net/http" + "strings" + "testing" + + g "github.com/dell/gopowerstore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +const ( + VGPrefix string = "test_vg_" +) + +type VolumeGroupTestSuite struct { + suite.Suite + + client g.Client + request g.VolumeGroup + + // Assurance that a volume group exists for the tests. + assurance g.CreateResponse +} + +func TestVolumeGroupSuite(t *testing.T) { + suite.Run(t, new(VolumeGroupTestSuite)) +} + +func (s *VolumeGroupTestSuite) SetupSuite() { + s.client = GetNewClient() + + var err error + // Make sure a volume group exists on which we can run tests against. + s.assurance, err = s.client.CreateVolumeGroup(context.Background(), &g.VolumeGroupCreate{ + Name: VGPrefix + randString(8), + }) + assert.NoError(s.T(), err) +} + +func (s *VolumeGroupTestSuite) TearDownSuite() { + // Delete the volume group we created for the test. + _, err := s.client.DeleteVolumeGroup(context.Background(), s.assurance.ID) + assert.NoError(s.T(), err) +} + +func (s *VolumeGroupTestSuite) SetupTest() { +} + +func (s *VolumeGroupTestSuite) TearDownTest() { +} + +// Returns true if one of the volume groups in vgs has an ID matching +// the ID provided by id and false if none of the volume groups have +// a matching ID. +func containsVolumeGroupID(vgs []g.VolumeGroup, id string) bool { + for _, vg := range vgs { + if vg.ID == id { + return true + } + } + return false +} + +// Happy path test. +func (s *VolumeGroupTestSuite) TestGetVolumeGroups() { + resp, err := s.client.GetVolumeGroups(context.Background()) + + if assert.NoError(s.T(), err) { + assert.True(s.T(), containsVolumeGroupID(resp, s.assurance.ID)) + } +} + +/* +/ //////////////////////////// +/ / METRO VOLUME GROUP TESTS / +/ //////////////////////////// +*/ + +// Test struct for metro volume configure and end metro volume suites. +type MetroVolumeGroupTest struct { + client g.Client + + vg struct { + this g.VolumeGroup + volumeIDs []string + } + + metro struct { + config g.MetroConfig + endOpts g.EndMetroVolumeGroupOptions + } +} + +type MetroVolumeGroupTestSuite struct { + suite.Suite + + MetroVolumeGroupTest +} + +func TestMetroVolumeGroupSuite(t *testing.T) { + suite.Run(t, new(MetroVolumeGroupTestSuite)) +} + +func (s *MetroVolumeGroupTestSuite) SetupSuite() { + s.client = GetNewClient() + + // Get a remote system configured for metro replication. + remoteSystem := GetRemoteSystemForMetro(s.client, s.T()) + if remoteSystem.ID == "" { + s.T().Skip("Could not get a remote system configured for metro. Skipping test suite...") + } + + s.metro.config = g.MetroConfig{RemoteSystemID: remoteSystem.ID} + + // Set end-metro configuration to delete remote VG. + s.metro.endOpts = g.EndMetroVolumeGroupOptions{ + DeleteRemoteVolumeGroup: true, + } +} + +// Create a volume group with one or more volumes in the group for testing purposes. +// Save the volume IDs and the volume group ID for use in subsequent tests. +func (s *MetroVolumeGroupTestSuite) SetupTest() { + // Create a volume to add to the vg to make it a valid vg we can test with. + volID, _ := CreateVol(s.T()) + s.vg.volumeIDs = append(s.vg.volumeIDs, volID) + + // Create a unique vg name for each test run. + s.vg.this.Name = VGPrefix + randString(8) + + // Create a volume group to run tests against. + resp, err := s.client.CreateVolumeGroup(context.Background(), &g.VolumeGroupCreate{ + Name: s.vg.this.Name, + VolumeIDs: s.vg.volumeIDs, + IsWriteOrderConsistent: true, + }) + assert.NoError(s.T(), err) + + s.vg.this.ID = resp.ID +} + +// End the metro session on the volume group, remove the volumes from the +// volume group and delete them, delete the volume group, and sanitize +// test variables for the next test run. +func (s *MetroVolumeGroupTestSuite) TearDownTest() { + // End metro vg replication session created during testing. + s.client.EndMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.endOpts) + + // Delete all the volumes in the volume group. + err := deleteAllVolumesInVG(s.client, s.vg.this.ID, s.vg.volumeIDs) + if err != nil { + s.T().Logf("%s Please delete from PowerStore when tests complete.", err.Error()) + } + + // Delete the volume group from the previous test. + _, err = s.client.DeleteVolumeGroup(context.Background(), s.vg.this.ID) + if err != nil { + // 404 status means it was already deleted. + // Warn about other errors encountered while deleting. + if err.(g.APIError).StatusCode != http.StatusNotFound { + s.T().Logf("Unable to delete test volume group %s. Please delete from PowerStore when tests complete. err: %s", s.vg.this.Name, err.Error()) + } + } + + // Sanitize for next test. + s.vg.this.Name = "" + s.vg.this.ID = "" + s.vg.volumeIDs = []string{} +} + +func deleteAllVolumesInVG(c g.Client, vgID string, volumeIDs []string) error { + // Must remove volumes from the volume group before deleting. + _, err := c.RemoveMembersFromVolumeGroup(context.Background(), &g.VolumeGroupMembers{VolumeIDs: volumeIDs}, vgID) + if err != nil { + if !strings.Contains(err.Error(), "One or more volumes to be removed are not part of the volume group") && + err.(g.APIError).StatusCode != http.StatusNotFound { + return err + } + } + + for _, volID := range volumeIDs { + _, err = c.DeleteVolume(context.Background(), nil, volID) + if err != nil { + // 404 status means it was already deleted. + // Warn about other errors encountered while deleting. + if err.(g.APIError).StatusCode != http.StatusNotFound { + return err + } + } + } + return nil +} + +// Should configure a metro volume group without errors. +func (s *MetroVolumeGroupTestSuite) TestConfigureMetroVolumeGroup() { + resp, err := s.client.ConfigureMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.config) + + assert.NoError(s.T(), err) + assert.NotEmpty(s.T(), resp) +} + +// Try to configure metro on a volume group without any volumes in it. +func (s *MetroVolumeGroupTestSuite) TestConfigMetroVGOnEmptyVG() { + // Delete all the volumes from the volume group. + err := deleteAllVolumesInVG(s.client, s.vg.this.ID, s.vg.volumeIDs) + assert.NoError(s.T(), err) + + // Attempt to configure metro on an empty volume group. + _, err = s.client.ConfigureMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.config) + + if assert.Error(s.T(), err) { + assert.Equal(s.T(), http.StatusUnprocessableEntity, err.(g.APIError).StatusCode) + assert.Contains(s.T(), err.(g.APIError).Message, "Replication session creation failed as Volume Group") + } +} + +// Try to configure metro on a non-existent volume group. +func (s *MetroVolumeGroupTestSuite) TestMetroVGNonExistantVG() { + // Delete all the volumes from the volume group. + err := deleteAllVolumesInVG(s.client, s.vg.this.ID, s.vg.volumeIDs) + assert.NoError(s.T(), err) + + // Delete that volume group, retaining the volume group ID. + _, err = s.client.DeleteVolumeGroup(context.Background(), s.vg.this.ID) + assert.NoError(s.T(), err) + + // Try to configure metro volume group using the deleted vg ID. + _, err = s.client.ConfigureMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.config) + + if assert.Error(s.T(), err) { + assert.Equal(s.T(), http.StatusNotFound, err.(g.APIError).StatusCode) + assert.Contains(s.T(), err.(g.APIError).Message, "Unable to find volume group") + } +} + +// Execute ConfigureMetroVolume with a bad request body. +func (s *MetroVolumeGroupTestSuite) TestMetroVGBadRequest() { + // Pass an emtpy configuration body with the request. + _, err := s.client.ConfigureMetroVolumeGroup(context.Background(), s.vg.this.ID, nil) + + if assert.Error(s.T(), err) { + assert.Equal(s.T(), http.StatusBadRequest, err.(g.APIError).StatusCode) + } +} + +/* +/ //////////////////////////////// +/ / END METRO VOLUME GROUP TESTS / +/ //////////////////////////////// +*/ +type EndMetroVolumeGroupTestSuite struct { + suite.Suite + + MetroVolumeGroupTest +} + +func TestEndMetroVolumeGroupSuite(t *testing.T) { + suite.Run(t, new(EndMetroVolumeGroupTestSuite)) +} + +func (s *EndMetroVolumeGroupTestSuite) SetupSuite() { + // Get a new client. + s.client = GetNewClient() + + // Get the remote PowerStore array. + remoteSystem := GetRemoteSystemForMetro(s.client, s.T()) + if remoteSystem.ID == "" { + s.T().Skip("Could not get a remote system configured for metro. Skipping test suite...") + } + + s.metro.config = g.MetroConfig{RemoteSystemID: remoteSystem.ID} + + // Make sure remote VGs are always deleted. + s.metro.endOpts = g.EndMetroVolumeGroupOptions{ + DeleteRemoteVolumeGroup: true, + } +} + +func (s *EndMetroVolumeGroupTestSuite) SetupTest() { + // Create a volume to add to the vg to make it a valid vg we can test with. + volID, _ := CreateVol(s.T()) + s.vg.volumeIDs = append(s.vg.volumeIDs, volID) + + // Create a unique vg name for each test run. + s.vg.this.Name = VGPrefix + randString(8) + + // Create a volume group to run tests against. + resp, err := s.client.CreateVolumeGroup(context.Background(), &g.VolumeGroupCreate{ + Name: s.vg.this.Name, + VolumeIDs: s.vg.volumeIDs, + IsWriteOrderConsistent: true, + }) + assert.NoError(s.T(), err) + + s.vg.this.ID = resp.ID + + // Create a metro vg session. + _, err = s.client.ConfigureMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.config) + if err != nil { + s.T().Skipf("Could not create metro volume group session. Skipping test... Err: %s", err) + } +} + +func (s *EndMetroVolumeGroupTestSuite) TearDownTest() { + // End the metro session if one still exists. + s.client.EndMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.endOpts) + + // Delete all the volumes in the volume group. + err := deleteAllVolumesInVG(s.client, s.vg.this.ID, s.vg.volumeIDs) + if err != nil { + s.T().Logf("%s Please delete from PowerStore when tests complete.", err.Error()) + } + + // Delete the volume group from the previous test. + _, err = s.client.DeleteVolumeGroup(context.Background(), s.vg.this.ID) + if err != nil { + // 404 status means it was already deleted. + // Warn about other errors encountered while deleting. + if err.(g.APIError).StatusCode != http.StatusNotFound { + s.T().Logf("Unable to delete test volume group %s. Please delete from PowerStore when tests complete. err: %s", s.vg.this.Name, err.Error()) + } + } + + // Sanitize for next test. + s.vg.this.Name = "" + s.vg.this.ID = "" + s.vg.volumeIDs = []string{} +} + +// End a valid metro volume group session. Should end without error. +func (s *EndMetroVolumeGroupTestSuite) TestEndMetroVolumeGroup() { + // End the metro session for the volume group. + _, err := s.client.EndMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.endOpts) + + assert.NoError(s.T(), err) +} + +// Try to end a metro volume group session with an invalid session ID. +func (s *EndMetroVolumeGroupTestSuite) TestEndMetroVGInvalidSessionID() { + // Invalid session ID. + invalidSessionID := "invalid-id" + + // End metro vg session with invalid vg ID. + _, err := s.client.EndMetroVolumeGroup(context.Background(), invalidSessionID, &s.metro.endOpts) + + if assert.Error(s.T(), err) { + assert.Equal(s.T(), http.StatusNotFound, err.(g.APIError).StatusCode) + assert.Contains(s.T(), err.(g.APIError).Message, "Unable to find volume group") + } +} + +// Try to end a metro VG session for a VG that is not currently part of a metro session. +func (s *EndMetroVolumeGroupTestSuite) TestEndMetroOnUnreplicatedVG() { + // Setup test scenario by ending the metro session. + _, err := s.client.EndMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.endOpts) + if err != nil { + s.T().Skipf("Could not end metro volume group session. Skipping test... Err: %s", err) + } + + // Try to end the metro vg session again. + _, err = s.client.EndMetroVolumeGroup(context.Background(), s.vg.this.ID, &s.metro.endOpts) + + if assert.Error(s.T(), err) { + assert.Equal(s.T(), http.StatusBadRequest, err.(g.APIError).StatusCode) + assert.Contains(s.T(), err.(g.APIError).Message, "the volume group is not in a metro replication session") + } +} diff --git a/inttests/volume_test.go b/inttests/volume_test.go index 31a71f5..c120e50 100644 --- a/inttests/volume_test.go +++ b/inttests/volume_test.go @@ -277,34 +277,13 @@ func TestMetroVolumeSuite(t *testing.T) { // Find a remote system with Metro support and make sure all metro volume sessions // are terminated with the remote volume being deleted. func (s *MetroVolumeTestSuite) SetupSuite() { - // Begin query to find a remote system for testing Metro - resp, err := C.GetAllRemoteSystems(context.Background()) - skipTestOnError(s.T(), err) - - // try to find a valid remote system with Metro from the list of all available remote systems - for i := range resp { - // get remote system details - rs, err := C.GetRemoteSystem(context.Background(), resp[i].ID) - assert.NoError(s.T(), err) - assert.Equal(s.T(), rs.ID, resp[i].ID) - - // check remote capabilities for metro and create MetroConfig if found - if Includes(&rs.Capabilities, string(gopowerstore.BlockMetro)) { - // make sure the connection is in a good state - if rs.DataConnectionState == string(gopowerstore.ConnStateOK) { - s.metroConfig = gopowerstore.MetroConfig{RemoteSystemID: rs.ID} - break - } - } - - // if none of the remote systems support metro, skip the test - if i == len(resp)-1 { - s.T().Skip("Skipping test as there are no working remote systems with Metro configured on array.") - return - } - + metroSystem := GetRemoteSystemForMetro(C, s.T()) + if metroSystem.ID == "" { + s.T().Skip("Could not get a remote system with metro configured. Skipping test suite...") } + s.metroConfig = gopowerstore.MetroConfig{RemoteSystemID: metroSystem.ID} + // always delete the remote metro volume s.endMetroOpts = gopowerstore.EndMetroVolumeOptions{ DeleteRemoteVolume: true, diff --git a/mocks/Client.go b/mocks/Client.go index 49ffd71..29f745c 100644 --- a/mocks/Client.go +++ b/mocks/Client.go @@ -235,6 +235,34 @@ func (_m *Client) ConfigureMetroVolume(ctx context.Context, id string, config *g return r0, r1 } +// ConfigureMetroVolumeGroup provides a mock function with given fields: ctx, id, config +func (_m *Client) ConfigureMetroVolumeGroup(ctx context.Context, id string, config *gopowerstore.MetroConfig) (gopowerstore.MetroSessionResponse, error) { + ret := _m.Called(ctx, id, config) + + if len(ret) == 0 { + panic("no return value specified for ConfigureMetroVolumeGroup") + } + + var r0 gopowerstore.MetroSessionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.MetroConfig) (gopowerstore.MetroSessionResponse, error)); ok { + return rf(ctx, id, config) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.MetroConfig) gopowerstore.MetroSessionResponse); ok { + r0 = rf(ctx, id, config) + } else { + r0 = ret.Get(0).(gopowerstore.MetroSessionResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *gopowerstore.MetroConfig) error); ok { + r1 = rf(ctx, id, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CopyMetricsByAppliance provides a mock function with given fields: ctx, entityID, interval func (_m *Client) CopyMetricsByAppliance(ctx context.Context, entityID string, interval gopowerstore.MetricsIntervalEnum) ([]gopowerstore.CopyMetricsByApplianceResponse, error) { ret := _m.Called(ctx, entityID, interval) @@ -1309,6 +1337,34 @@ func (_m *Client) EndMetroVolume(ctx context.Context, id string, options *gopowe return r0, r1 } +// EndMetroVolumeGroup provides a mock function with given fields: ctx, id, options +func (_m *Client) EndMetroVolumeGroup(ctx context.Context, id string, options *gopowerstore.EndMetroVolumeGroupOptions) (gopowerstore.EmptyResponse, error) { + ret := _m.Called(ctx, id, options) + + if len(ret) == 0 { + panic("no return value specified for EndMetroVolumeGroup") + } + + var r0 gopowerstore.EmptyResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.EndMetroVolumeGroupOptions) (gopowerstore.EmptyResponse, error)); ok { + return rf(ctx, id, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.EndMetroVolumeGroupOptions) gopowerstore.EmptyResponse); ok { + r0 = rf(ctx, id, options) + } else { + r0 = ret.Get(0).(gopowerstore.EmptyResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *gopowerstore.EndMetroVolumeGroupOptions) error); ok { + r1 = rf(ctx, id, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ExecuteActionOnReplicationSession provides a mock function with given fields: ctx, id, actionType, params func (_m *Client) ExecuteActionOnReplicationSession(ctx context.Context, id string, actionType gopowerstore.ActionType, params *gopowerstore.FailoverParams) (gopowerstore.EmptyResponse, error) { ret := _m.Called(ctx, id, actionType, params) diff --git a/volume_group.go b/volume_group.go index 97051e2..508aae6 100644 --- a/volume_group.go +++ b/volume_group.go @@ -25,8 +25,10 @@ import ( ) const ( - volumeGroupURL = "volume_group" - snapshotURL = "/snapshot" + volumeGroupURL = "volume_group" + snapshotURL = "/snapshot" + actionConfigMetro = "configure_metro" + actionEndMetro = "end_metro" ) func getVolumeGroupDefaultQueryParams(c Client) api.QueryParamsEncoder { @@ -297,3 +299,37 @@ func (c *ClientIMPL) GetVolumeGroupSnapshotByName(ctx context.Context, name stri } return volGroupList[0], err } + +// ConfigureMetroVolumeGroup configures the volume group provided by id for metro replication using the +// configuration supplied by config and returns a MetroSessionResponse containing a replication session ID. +func (c *ClientIMPL) ConfigureMetroVolumeGroup(ctx context.Context, id string, config *MetroConfig) (session MetroSessionResponse, err error) { + _, err = c.APIClient().Query( + ctx, + RequestConfig{ + Method: "POST", + Endpoint: volumeGroupURL, + ID: id, + Action: actionConfigMetro, + Body: config, + }, + &session) + + return session, WrapErr(err) +} + +// EndMetroVolumeGroup ends a metro configuration from a volume group and keeps both volume groups by default. The local copy +// will retain its SCSI Identities while the remote volume group members will get new SCSI Identities if kept. +func (c *ClientIMPL) EndMetroVolumeGroup(ctx context.Context, id string, options *EndMetroVolumeGroupOptions) (resp EmptyResponse, err error) { + _, err = c.APIClient().Query( + ctx, + RequestConfig{ + Method: "POST", + Endpoint: volumeGroupURL, + ID: id, + Action: actionEndMetro, + Body: options, + }, + &resp) + + return resp, WrapErr(err) +} diff --git a/volume_group_test.go b/volume_group_test.go index 02e4855..f6848ee 100644 --- a/volume_group_test.go +++ b/volume_group_test.go @@ -19,6 +19,7 @@ package gopowerstore import ( "context" "fmt" + "net/http" "testing" "github.com/jarcoal/httpmock" @@ -26,13 +27,15 @@ import ( ) const ( + volumeGroupID = "c4e4f58e-cdc2-4b75-a81a-685543b1420f" volumeGroupMockURL = APIMockURL + volumeGroupURL - volumeGroupSnapshotMockURL = APIMockURL + volumeGroupURL + "/test-id" + snapshotURL -) + volumeGroupSnapshotMockURL = APIMockURL + volumeGroupURL + "/" + volumeGroupID + snapshotURL -var ( volGroupSnapID = "1966782b-60c9-40e2-a1ee-9b2b8f6b98e7" volGroupSnapID2 = "34380c29-2203-4490-aeb7-2853b9a85075" + + metroSessionID = "7f354feb-2014-412b-9406-dc325d096f96" + remoteArrayID = "43557404-8446-48db-87b4-5316877f7c26" ) func TestClientIMPL_CreateVolumeGroup(t *testing.T) { @@ -66,7 +69,7 @@ func TestClientIMPL_CreateVolumeGroupSnapshot(t *testing.T) { Description: "vgs-test", } - resp, err := C.CreateVolumeGroupSnapshot(context.Background(), "test-id", &createReq) + resp, err := C.CreateVolumeGroupSnapshot(context.Background(), volumeGroupID, &createReq) assert.Nil(t, err) assert.Equal(t, volID, resp.ID) } @@ -247,3 +250,34 @@ func TestClientIMPL_UpdateVolumeGroupProtectionPolicy(t *testing.T) { assert.Nil(t, err) assert.Equal(t, EmptyResponse(""), resp) } + +func TestClientIMPL_ConfigureMetroVolumeGroup(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + expectedSessionID := metroSessionID + + // Response body contains a valid metro session ID. + bodyResponse := fmt.Sprintf(`{"metro_replication_session_id": "%s"}`, expectedSessionID) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/configure_metro", volumeGroupMockURL, volumeGroupID), + httpmock.NewStringResponder(http.StatusOK, bodyResponse)) + + metroSession, err := C.ConfigureMetroVolumeGroup(context.Background(), volumeGroupID, &MetroConfig{RemoteSystemID: remoteArrayID}) + + assert.NoError(t, err) + assert.Equal(t, expectedSessionID, metroSession.ID) +} + +func TestClientIMPL_EndMetroVolumeGroup(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + expectedStatusCode := http.StatusNoContent + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/end_metro", volumeGroupMockURL, volumeGroupID), + httpmock.NewStringResponder(expectedStatusCode, "")) + + _, err := C.EndMetroVolumeGroup(context.Background(), volumeGroupID, &EndMetroVolumeGroupOptions{DeleteRemoteVolumeGroup: true}) + + assert.NoError(t, err) +} diff --git a/volume_group_types.go b/volume_group_types.go index 36a2934..d278019 100644 --- a/volume_group_types.go +++ b/volume_group_types.go @@ -131,3 +131,15 @@ type VolumeGroupSnapshotCreate struct { // ExpirationTimestamp provides volume group creation time ExpirationTimestamp string `json:"expiration_timestamp,omitempty"` } + +// EndMetroVolumeGroupOptions provides options for deleting the remote volume group and forcing the deletion. +type EndMetroVolumeGroupOptions struct { + // DeleteRemoteVolumeGroup specifies whether or not to delete the remote volume group when ending the metro session. + DeleteRemoteVolumeGroup bool `json:"delete_remote_volume_group,omitempty"` + // ForceDelete specifies if the Metro volume group should be forcefully deleted. + // If the force option is specified, any errors returned while attempting to tear down the remote side of the + // metro session will be ignored and the remote side may be left in an indeterminate state. + // If any errors occur on the local side the operation can still fail. + // It is not recommended to use this option unless the remote side is known to be down. + ForceDelete bool `json:"force,omitempty"` +} diff --git a/volume_types.go b/volume_types.go index fe9bfac..52d76a3 100644 --- a/volume_types.go +++ b/volume_types.go @@ -404,7 +404,7 @@ type MetroSessionResponse struct { ID string `json:"metro_replication_session_id,omitempty"` } -// EndMetroVolumeOptions defines the options associated with deleting a metro volume. +// EndMetroVolumeOptions provides options for deleting the remote volume and forcing the deletion. type EndMetroVolumeOptions struct { // DeleteRemoteVolume specifies whether or not to delete the remote volume when ending the metro session. DeleteRemoteVolume bool `json:"delete_remote_volume,omitempty"`