diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 92f8ae0..56edcbe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,26 +7,12 @@ # be requested for review when someone opens a pull request. # order is alphabetical for easier maintenance. # +# Abhilash Muralidhara (abhi16394) # Adarsh Kumar Yadav (adarsh-dell) # Akshay Saini (AkshaySainiDell) -# Boya Murthy (boyamurthy) -# Christian Coffield (ChristianAtDell, christian_coffield@dell.com) -# Utkarsh Dubey (delldubey) -# Francis Nijay (francis-nijay) -# Tarandeep Singh Gill (gilltaran) -# Harish P (harishp8889) -# Harshita Pandey (harshitap26) -# Vinutha Kini (kenivi1) -# Niranjan N (niranjan-n1) -# Prasanna Muthukumaraswamy (prablr79) -# Santhosh Lakshmanan (santhoshatdell, santhosh.lakshmanan@dell.com) -# Satya Narayana (satyakonduri) -# Santhosh Shekarappa (santhoshhs10) -# Yamunadevi Shanmugam (shanmydell) -# Surya Prakash Gupta (suryagupta4) -# Sushma T S (tssushma) +# Don Khan (donatwork) +# Luke Lau (lukeatdell) +# Santhosh Lakshmanan (santhoshatdell) - - -# for all files: -* @adarsh-dell @AkshaySainiDell @boyamurthy @ChristianAtDell @delldubey @francis-nijay @gilltaran @harishp8889 @harshitap26 @kenivi1 @niranjan-n1 @prablr79 @santhoshatdell @satyakonduri @santhoshhs10 @shanmydell @suryagupta4 @tssushma +# for all files +* @abhi16394 @adarsh-dell @AkshaySainiDell @donatwork @lukeatdell @santhoshatdell diff --git a/Makefile b/Makefile index 2a447cf..fed211b 100644 --- a/Makefile +++ b/Makefile @@ -27,8 +27,9 @@ unit-test: go test -v -coverprofile=c.out $(unit_test_paths) int-test: - go test -timeout 600s -v -coverprofile=c.out -coverpkg github.com/dell/gopowerstore \ - $(integration_tests_path) + source $(integration_tests_path)/GOPOWERSTORE_TEST.env \ + && \ + go test -timeout 600s -shuffle=on -v -coverprofile=c.out -coverpkg github.com/dell/gopowerstore $(integration_tests_path) gocover: go tool cover -html=c.out diff --git a/api/api.go b/api/api.go index 942912c..0e7ad96 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2023 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -122,7 +122,7 @@ type ClientIMPL struct { username string password string httpClient *http.Client - defaultTimeout uint64 + defaultTimeout int64 requestIDKey ContextKey customHTTPHeaders http.Header logger Logger @@ -133,7 +133,7 @@ type ClientIMPL struct { // New creates and initialize API client func New(apiURL string, username string, - password string, insecure bool, defaultTimeout, rateLimit uint64, requestIDKey ContextKey, + password string, insecure bool, defaultTimeout int64, rateLimit int, requestIDKey ContextKey, ) (*ClientIMPL, error) { debug, _ = strconv.ParseBool(os.Getenv("GOPOWERSTORE_DEBUG")) if apiURL == "" || username == "" || password == "" { @@ -164,7 +164,7 @@ func New(apiURL string, username string, log.Print("Session management is enabled.") } - throttle := NewTimeoutSemaphore(int(defaultTimeout), int(rateLimit), &defaultLogger{}) + throttle := NewTimeoutSemaphore(defaultTimeout, rateLimit, &defaultLogger{}) clientImpl := &ClientIMPL{ apiURL: apiURL, diff --git a/api/api_test.go b/api/api_test.go index 9820c87..4e1325c 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -71,8 +71,8 @@ func TestNew(t *testing.T) { url := "test_url" user := "admin" password := "password" - timeout := uint64(120) - limit := uint64(1000) + timeout := int64(120) + limit := int(1000) var err error var c *ClientIMPL c, err = New(url, user, password, false, timeout, limit, key) @@ -86,7 +86,7 @@ func TestNew(t *testing.T) { } func testClient(t *testing.T, apiURL string) *ClientIMPL { - c, err := New(apiURL, "admin", "password", false, uint64(10), uint64(1000), "key") + c, err := New(apiURL, "admin", "password", false, int64(10), int(1000), "key") if err != nil { t.FailNow() } diff --git a/api/throttling.go b/api/throttling.go index fca7747..7b8493b 100644 --- a/api/throttling.go +++ b/api/throttling.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2021-2022 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. @@ -42,7 +42,7 @@ type TimeoutSemaphore struct { Logger Logger } -func NewTimeoutSemaphore(timeout, rateLimit int, logger Logger) *TimeoutSemaphore { +func NewTimeoutSemaphore(timeout int64, rateLimit int, logger Logger) *TimeoutSemaphore { log := logger if log == nil { diff --git a/client.go b/client.go index a87e96e..94d5f2d 100644 --- a/client.go +++ b/client.go @@ -189,6 +189,8 @@ type Client interface { GetVolumeGroupSnapshots(ctx context.Context) ([]VolumeGroup, error) 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) + EndMetroVolume(ctx context.Context, id string, options *EndMetroVolumeOptions) (resp EmptyResponse, err error) } // ClientIMPL provides basic API client implementation @@ -261,7 +263,7 @@ func NewClient() (Client, error) { if err == nil { options.SetInsecure(insecure) } - httpTimeout, err := strconv.ParseUint(os.Getenv(HTTPTimeoutEnv), 10, 64) + httpTimeout, err := strconv.ParseInt(os.Getenv(HTTPTimeoutEnv), 10, 64) if err == nil { options.SetDefaultTimeout(httpTimeout) diff --git a/client_options.go b/client_options.go index 17fac53..bc8f8ff 100644 --- a/client_options.go +++ b/client_options.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -38,8 +38,8 @@ func NewClientOptions() *ClientOptions { // ClientOptions struct provide additional options for api client configuration type ClientOptions struct { insecure *bool // skip https cert check - defaultTimeout *uint64 - rateLimit *uint64 + defaultTimeout *int64 + rateLimit *int // define field name in context which will be used for tracing requestIDKey *api.ContextKey } @@ -53,7 +53,7 @@ func (co *ClientOptions) Insecure() bool { } // DefaultTimeout returns http client default timeout -func (co *ClientOptions) DefaultTimeout() uint64 { +func (co *ClientOptions) DefaultTimeout() int64 { if co.defaultTimeout == nil { return clientOptionsDefaultTimeout } @@ -61,7 +61,7 @@ func (co *ClientOptions) DefaultTimeout() uint64 { } // RateLimit returns http client rate limit -func (co *ClientOptions) RateLimit() uint64 { +func (co *ClientOptions) RateLimit() int { if co.rateLimit == nil { return clientOptionsDefaultRateLimit } @@ -83,13 +83,13 @@ func (co *ClientOptions) SetInsecure(value bool) *ClientOptions { } // SetDefaultTimeout sets default http client timeout value -func (co *ClientOptions) SetDefaultTimeout(value uint64) *ClientOptions { +func (co *ClientOptions) SetDefaultTimeout(value int64) *ClientOptions { co.defaultTimeout = &value return co } // SetRateLimit returns http client rate limit -func (co *ClientOptions) SetRateLimit(value uint64) *ClientOptions { +func (co *ClientOptions) SetRateLimit(value int) *ClientOptions { co.rateLimit = &value return co } diff --git a/client_options_test.go b/client_options_test.go index 1459da9..5604cad 100644 --- a/client_options_test.go +++ b/client_options_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -32,7 +32,7 @@ func TestClientOptions_Insecure(t *testing.T) { func TestClientOptions_DefaultTimeout(t *testing.T) { co := NewClientOptions() - value := uint64(120) + value := int64(120) co.SetDefaultTimeout(value) assert.Equal(t, value, co.DefaultTimeout()) } diff --git a/cluster_types.go b/cluster_types.go index 1771286..7bc5ff1 100644 --- a/cluster_types.go +++ b/cluster_types.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2021-2022 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. @@ -29,13 +29,46 @@ type RemoteSystem struct { SerialNumber string `json:"serial_number,omitempty"` // Management IP address of the remote system instance ManagementAddress string `json:"management_address,omitempty"` + // Possible data connection states of a remote system + DataConnectionState string `json:"data_connection_state,omitempty"` + // List of supported remote protection capabilities + Capabilities []string `json:"capabilities,omitempty"` } // Fields returns fields which must be requested to fill struct func (r *RemoteSystem) Fields() []string { - return []string{"id", "name", "description", "serial_number", "management_address"} + return []string{"id", "name", "description", "serial_number", "management_address", "data_connection_state", "capabilities"} } +type DataConnectStateEnum string + +// List of possible data connection states for a RemoteSystem +const ( + ConnStateOK DataConnectStateEnum = "OK" + ConnStatePartialDataConnLoss DataConnectStateEnum = "Partial_Data_Connection_Loss" + ConnStateCompleteDataConnLoss DataConnectStateEnum = "Complete_Data_Connection_Loss" + ConnStateNotAvailable DataConnectStateEnum = "Status_Not_Available" + ConnStateNoTargetsDiscovered DataConnectStateEnum = "No_Targets_Discovered" + ConnStateInitializing DataConnectStateEnum = "Initializing" + ConnStateUnstable DataConnectStateEnum = "Data_Connection_Unstable" +) + +type RemoteCapabilitiesEnum string + +// List of possible remote protection capabilities for a remote system +const ( + AsyncBlock RemoteCapabilitiesEnum = "Asynchronous_Block_Replication" + SyncBlock RemoteCapabilitiesEnum = "Synchronous_Block_Replication" + AsyncFile RemoteCapabilitiesEnum = "Asynchronous_File_Replication" + AsyncVvol RemoteCapabilitiesEnum = "Asynchronous_Vvol_Replication" + BlockNonDisruptiveImport RemoteCapabilitiesEnum = "Block_Nondisruptive_Import" + BlockAgentlessImport RemoteCapabilitiesEnum = "Block_Agentless_Import" + FileImport RemoteCapabilitiesEnum = "File_Import" + BlockMetro RemoteCapabilitiesEnum = "Block_Metro_Active_Active" + RemoteBackup RemoteCapabilitiesEnum = "Remote_Backup" + SyncFile RemoteCapabilitiesEnum = "Synchronous_File_Replication" +) + // Cluster details about the cluster type Cluster struct { // Unique identifier of the cluster. diff --git a/go.mod b/go.mod index 2d16fad..831f190 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,25 @@ module github.com/dell/gopowerstore -go 1.22 +go 1.23 require ( - github.com/go-openapi/strfmt v0.21.2 + github.com/go-openapi/strfmt v0.23.0 github.com/jarcoal/httpmock v1.2.0 - github.com/joho/godotenv v1.4.0 + github.com/joho/godotenv v1.5.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.9.0 ) require ( - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/errors v0.20.2 // indirect - github.com/go-stack/stack v1.8.1 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.4.0 // indirect - go.mongodb.org/mongo-driver v1.9.1 // indirect - golang.org/x/sys v0.1.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + go.mongodb.org/mongo-driver v1.16.1 // 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 cd3aeec..a163187 100644 --- a/go.sum +++ b/go.sum @@ -1,88 +1,48 @@ -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os= -github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= -github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= github.com/maxatome/go-testdeep v1.11.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= -go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c= -go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +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= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inttests/README.md b/inttests/README.md index 8895d11..fb27279 100644 --- a/inttests/README.md +++ b/inttests/README.md @@ -5,10 +5,11 @@ You should export all ENV variables required for API client initialization befor It's also possible to create GOPOWERSTORE_TEST.env which will hold all required vars _GOPOWERSTORE_TEST.env example:_ -```GOPOWERSTORE_INSECURE=true - GOPOWERSTORE_HTTP_TIMEOUT=60 - GOPOWERSTORE_APIURL=https://127.0.0.1/api/rest - GOPOWERSTORE_USERNAME=admin - GOPOWERSTORE_PASSWORD=Password - GOPOWERSTORE_DEBUG=true +```shell +GOPOWERSTORE_INSECURE=true +GOPOWERSTORE_HTTP_TIMEOUT=60 +GOPOWERSTORE_APIURL=https://127.0.0.1/api/rest +GOPOWERSTORE_USERNAME=admin +GOPOWERSTORE_PASSWORD=Password +GOPOWERSTORE_DEBUG=true ``` \ No newline at end of file diff --git a/inttests/api_test.go b/inttests/api_test.go index a823f26..89d1aa1 100644 --- a/inttests/api_test.go +++ b/inttests/api_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -24,29 +24,44 @@ import ( "testing" "time" + "github.com/dell/gopowerstore" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestTimeout(t *testing.T) { +type customLogger struct{} + +type APITestSuite struct { + suite.Suite + C gopowerstore.Client +} + +func TestApiSuite(t *testing.T) { + suite.Run(t, new(APITestSuite)) +} + +func (s *APITestSuite) SetupTest() { + s.C = GetNewClient() +} + +func (s *APITestSuite) TestTimeout() { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond*10) defer cancelFunc() - _, err := C.GetVolumes(ctx) - assert.NotNil(t, err) + _, err := s.C.GetVolumes(ctx) + assert.NotNil(s.T(), err) } -func TestTraceID(t *testing.T) { - _, err := C.GetVolumes(C.SetTraceID(context.Background(), "reqid-1")) - assert.Nil(t, err) +func (s *APITestSuite) TestTraceID() { + _, err := s.C.GetVolumes(s.C.SetTraceID(context.Background(), "reqid-1")) + assert.Nil(s.T(), err) } -func TestCustomLogger(t *testing.T) { - C.SetLogger(&customLogger{}) - _, err := C.GetVolumes(C.SetTraceID(context.Background(), "reqid-1")) - assert.Nil(t, err) +func (s *APITestSuite) TestCustomLogger() { + s.C.SetLogger(&customLogger{}) + _, err := s.C.GetVolumes(s.C.SetTraceID(context.Background(), "reqid-1")) + assert.Nil(s.T(), err) } -type customLogger struct{} - func (cl *customLogger) Info(_ context.Context, format string, args ...interface{}) { log.Printf("INFO:"+format, args...) } diff --git a/inttests/common.go b/inttests/common.go index 3eaf864..096ca7e 100644 --- a/inttests/common.go +++ b/inttests/common.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -19,9 +19,19 @@ package inttests import ( + "context" "crypto/rand" "math/big" "testing" + + "github.com/dell/gopowerstore" +) + +const ( + TestVolumePrefix = "test_vol_" + DefaultVolSize int64 = 1048576 + DefaultChunkSize int64 = 1048576 + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ) func checkAPIErr(t *testing.T, err error) { @@ -37,8 +47,6 @@ func skipTestOnError(t *testing.T, err error) { } } -const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - func randString(n int) string { b := make([]byte, n) for i := range b { @@ -51,3 +59,29 @@ func randString(n int) string { } return string(b) } + +func CreateVol(t *testing.T) (volID, volName string) { + volName = TestVolumePrefix + randString(8) + createParams := gopowerstore.VolumeCreate{} + createParams.Name = &volName + size := DefaultVolSize + createParams.Size = &size + createResp, err := C.CreateVolume(context.Background(), &createParams) + checkAPIErr(t, err) + return createResp.ID, volName +} + +func DeleteVol(t *testing.T, id string) { + _, err := C.DeleteVolume(context.Background(), nil, id) + checkAPIErr(t, err) +} + +// check for the target string in the list of strings and return true if found. +func Includes(list *[]string, target string) bool { + for _, s := range *list { + if s == target { + return true + } + } + return false +} diff --git a/inttests/host_test.go b/inttests/host_test.go index 7e86cae..41c6e52 100644 --- a/inttests/host_test.go +++ b/inttests/host_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -136,7 +136,7 @@ func TestGetHostVolumeMapping(t *testing.T) { } func TestAttachDetachVolume(t *testing.T) { - volID, _ := createVol(t) + volID, _ := CreateVol(t) hostID, _ := createHost(t) // attach attach := gopowerstore.HostVolumeAttach{} @@ -159,7 +159,7 @@ func TestAttachDetachVolume(t *testing.T) { // try detach second time apiError := err.(gopowerstore.APIError) assert.True(t, apiError.HostIsNotAttachedToVolume()) - deleteVol(t, volID) + DeleteVol(t, volID) deleteHost(t, hostID) // try detach not exist host _, err = C.DetachVolumeFromHost(context.Background(), hostID, &detach) @@ -169,7 +169,7 @@ func TestAttachDetachVolume(t *testing.T) { } func TestDeleteAttachedVolume(t *testing.T) { - volID, _ := createVol(t) + volID, _ := CreateVol(t) hostID, _ := createHost(t) // attach attach := gopowerstore.HostVolumeAttach{} diff --git a/inttests/init_client.go b/inttests/init_client.go index 7cdd31c..46c20cc 100644 --- a/inttests/init_client.go +++ b/inttests/init_client.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -44,3 +44,16 @@ func initClient() { func init() { initClient() } + +func GetNewClient() (client gopowerstore.Client) { + err := godotenv.Load(envVarsFile) + if err != nil { + log.Printf("%s file not found.", envVarsFile) + } + client, err = gopowerstore.NewClient() + if err != nil { + panic(err) + } + + return client +} diff --git a/inttests/limit_test.go b/inttests/limit_test.go index b04acc5..79aed9e 100644 --- a/inttests/limit_test.go +++ b/inttests/limit_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2023 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2023-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. @@ -23,30 +23,45 @@ import ( "net/http" "testing" + "github.com/dell/gopowerstore" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestGetMaxVolumeSize(t *testing.T) { - customHeaders := C.GetCustomHTTPHeaders() +type GetMaxVolumeSizeTestSuite struct { + suite.Suite + C gopowerstore.Client +} + +func TestGetMaxVolumeSizeSuite(t *testing.T) { + suite.Run(t, new(GetMaxVolumeSizeTestSuite)) +} + +func (s *GetMaxVolumeSizeTestSuite) SetupTest() { + s.C = GetNewClient() +} + +func (s *GetMaxVolumeSizeTestSuite) TestGetMaxVolumeSize() { + customHeaders := s.C.GetCustomHTTPHeaders() if customHeaders == nil { customHeaders = make(http.Header) } customHeaders.Add("DELL-VISIBILITY", "internal") - C.SetCustomHTTPHeaders(customHeaders) + s.C.SetCustomHTTPHeaders(customHeaders) - limit, err := C.GetMaxVolumeSize(context.Background()) + limit, err := s.C.GetMaxVolumeSize(context.Background()) // reset custom header customHeaders.Del("DELL-VISIBILITY") - C.SetCustomHTTPHeaders(customHeaders) + s.C.SetCustomHTTPHeaders(customHeaders) - checkAPIErr(t, err) - assert.Positive(t, limit) + checkAPIErr(s.T(), err) + assert.Positive(s.T(), limit) } -func TestGetMaxVolumeSizeEndpointNotFound(t *testing.T) { - limit, err := C.GetMaxVolumeSize(context.Background()) +func (s *GetMaxVolumeSizeTestSuite) TestGetMaxVolumeSizeEndpointNotFound() { + limit, err := s.C.GetMaxVolumeSize(context.Background()) - assert.Equal(t, "The REST endpoint [GET /api/rest/limit?select=id%2Climit] cannot be found.", err.Error()) - assert.Negative(t, limit) + assert.Equal(s.T(), "The REST endpoint [GET /api/rest/limit?select=id%2Climit] cannot be found.", err.Error()) + assert.Negative(s.T(), limit) } diff --git a/inttests/metrics_test.go b/inttests/metrics_test.go index c081dd1..ed9c967 100644 --- a/inttests/metrics_test.go +++ b/inttests/metrics_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -240,15 +240,17 @@ func Test_SpaceMetricsByVolumeGroup(t *testing.T) { func Test_CopyMetricsByAppliance(t *testing.T) { resp, err := C.CopyMetricsByAppliance(context.Background(), "A1", gopowerstore.OneDay) checkAPIErr(t, err) - assert.NotEmpty(t, resp) - assert.Equal(t, "copy_metrics_by_appliance", resp[0].Entity) + if assert.NotEmpty(t, resp) { + assert.Equal(t, "copy_metrics_by_appliance", resp[0].Entity) + } } func Test_CopyMetricsByCluster(t *testing.T) { resp, err := C.CopyMetricsByCluster(context.Background(), "0", gopowerstore.OneDay) checkAPIErr(t, err) - assert.NotEmpty(t, resp) - assert.Equal(t, "copy_metrics_by_cluster", resp[0].Entity) + if assert.NotEmpty(t, resp) { + assert.Equal(t, "copy_metrics_by_cluster", resp[0].Entity) + } } func Test_CopyMetricsByVolumeGroup(t *testing.T) { diff --git a/inttests/replication_test.go b/inttests/replication_test.go index 01812bb..2fc75e5 100644 --- a/inttests/replication_test.go +++ b/inttests/replication_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2021-2022 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. @@ -41,6 +41,10 @@ type ReplicationTestSuite struct { vol gopowerstore.CreateResponse } +const ( + OneMB int64 = 1048576 +) + func (suite *ReplicationTestSuite) 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 @@ -123,29 +127,36 @@ func getRemoteSystem(t *testing.T, suite *ReplicationTestSuite) (string, string) func (suite *ReplicationTestSuite) TestReplication() { 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 suite.vg, err = C.CreateVolumeGroup(context.Background(), &gopowerstore.VolumeGroupCreate{ Name: "intcsi" + suite.randomString + "-vgtst", ProtectionPolicyID: suite.pp.ID, }) assert.NoError(t, err) + // create a volume within the volume group volName := "intcsi" + suite.randomString + "-voltst" - size := int64(1048576) + 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) + // get the replication session using the volume group ID. May take some time. for tout := 0; tout < 30; tout++ { _, err = C.GetReplicationSessionByLocalResourceID(context.Background(), suite.vg.ID) if err == nil { diff --git a/inttests/volume_test.go b/inttests/volume_test.go index b0d8f0a..31a71f5 100644 --- a/inttests/volume_test.go +++ b/inttests/volume_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -25,30 +25,9 @@ import ( "github.com/dell/gopowerstore" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -const ( - TestVolumePrefix = "test_vol_" - DefaultVolSize int64 = 1048576 - DefaultChunkSize int64 = 1048576 -) - -func createVol(t *testing.T) (string, string) { - volName := TestVolumePrefix + randString(8) - createParams := gopowerstore.VolumeCreate{} - createParams.Name = &volName - size := DefaultVolSize - createParams.Size = &size - createResp, err := C.CreateVolume(context.Background(), &createParams) - checkAPIErr(t, err) - return createResp.ID, volName -} - -func deleteVol(t *testing.T, id string) { - _, err := C.DeleteVolume(context.Background(), nil, id) - checkAPIErr(t, err) -} - func createSnap(volID string, t *testing.T, volName string) gopowerstore.CreateResponse { return createSnapWithSuffix(volID, t, volName, "snapshot") } @@ -69,8 +48,8 @@ func createSnapWithSuffix(volID string, t *testing.T, volName string, snapshotSu } func TestModifyVolume(t *testing.T) { - volID, _ := createVol(t) - defer deleteVol(t, volID) + volID, _ := CreateVol(t) + defer DeleteVol(t, volID) _, err := C.ModifyVolume(context.Background(), &gopowerstore.VolumeModify{Size: DefaultVolSize * 2, Name: "rename"}, volID) checkAPIErr(t, err) @@ -81,8 +60,8 @@ func TestModifyVolume(t *testing.T) { } func TestGetSnapshotsByVolumeID(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) @@ -95,8 +74,8 @@ func TestGetSnapshotsByVolumeID(t *testing.T) { } func TestGetSnapshot(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) @@ -113,8 +92,8 @@ func TestGetSnapshots(t *testing.T) { } func TestGetNonExistingSnapshot(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) @@ -128,15 +107,15 @@ func TestGetNonExistingSnapshot(t *testing.T) { } func TestCreateSnapshot(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) } func TestDeleteSnapshot(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) _, err := C.DeleteSnapshot(context.Background(), nil, snap.ID) @@ -144,8 +123,8 @@ func TestDeleteSnapshot(t *testing.T) { } func TestCreateVolumeFromSnapshot(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) @@ -155,7 +134,7 @@ func TestCreateVolumeFromSnapshot(t *testing.T) { snapVol, err := C.CreateVolumeFromSnapshot(context.Background(), &createParams, snap.ID) checkAPIErr(t, err) assert.NotEmpty(t, snapVol.ID) - deleteVol(t, snapVol.ID) + DeleteVol(t, snapVol.ID) } func TestGetVolumes(t *testing.T) { @@ -164,26 +143,26 @@ func TestGetVolumes(t *testing.T) { } func TestGetVolume(t *testing.T) { - volID, volName := createVol(t) + volID, volName := CreateVol(t) volume, err := C.GetVolume(context.Background(), volID) checkAPIErr(t, err) assert.NotEmpty(t, volume.Name) assert.Equal(t, volName, volume.Name) - deleteVol(t, volID) + DeleteVol(t, volID) } func TestGetVolumeByName(t *testing.T) { - volID, volName := createVol(t) + volID, volName := CreateVol(t) volume, err := C.GetVolumeByName(context.Background(), volName) checkAPIErr(t, err) assert.NotEmpty(t, volume.Name) assert.Equal(t, volName, volume.Name) - deleteVol(t, volID) + DeleteVol(t, volID) } func TestCreateDeleteVolume(t *testing.T) { - volID, _ := createVol(t) - deleteVol(t, volID) + volID, _ := CreateVol(t) + DeleteVol(t, volID) } func TestDeleteUnknownVol(t *testing.T) { @@ -207,8 +186,8 @@ func TestGetVolumesWithTrace(t *testing.T) { } func TestVolumeAlreadyExist(t *testing.T) { - volID, name := createVol(t) - defer deleteVol(t, volID) + volID, name := CreateVol(t) + defer DeleteVol(t, volID) createReq := gopowerstore.VolumeCreate{} createReq.Name = &name size := DefaultVolSize @@ -220,8 +199,8 @@ func TestVolumeAlreadyExist(t *testing.T) { } func TestSnapshotAlreadyExist(t *testing.T) { - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) snap := createSnap(volID, t, volName) assert.NotEmpty(t, snap.ID) @@ -244,9 +223,10 @@ func TestGetInvalidVolume(t *testing.T) { } func TestComputeDifferences(t *testing.T) { + pstoreClient := GetNewClient() // Create volume - volID, volName := createVol(t) - defer deleteVol(t, volID) + volID, volName := CreateVol(t) + defer DeleteVol(t, volID) // Create snap of volume snap1 := createSnapWithSuffix(volID, t, volName, "snapshot1") @@ -266,18 +246,146 @@ func TestComputeDifferences(t *testing.T) { Length: &length, Offset: &offset, } - defaultHeaders := C.GetCustomHTTPHeaders() + defaultHeaders := pstoreClient.GetCustomHTTPHeaders() if defaultHeaders == nil { defaultHeaders = make(http.Header) } customHeaders := defaultHeaders // for accessing internal REST-APIs customHeaders.Add("DELL-VISIBILITY", "internal") - C.SetCustomHTTPHeaders(customHeaders) + pstoreClient.SetCustomHTTPHeaders(customHeaders) - resp, err := C.ComputeDifferences(context.Background(), &snapdiffParams, snap1.ID) + resp, err := pstoreClient.ComputeDifferences(context.Background(), &snapdiffParams, snap1.ID) checkAPIErr(t, err) // AA== is equivalent to an empty bitmap assert.Equal(t, "AA==", *resp.ChunkBitmap) assert.Equal(t, int64(-1), *resp.NextOffset) } + +type MetroVolumeTestSuite struct { + suite.Suite + volID string + metroSession gopowerstore.MetroSessionResponse + metroConfig gopowerstore.MetroConfig + endMetroOpts gopowerstore.EndMetroVolumeOptions +} + +func TestMetroVolumeSuite(t *testing.T) { + suite.Run(t, new(MetroVolumeTestSuite)) +} + +// 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 + } + + } + + // always delete the remote metro volume + s.endMetroOpts = gopowerstore.EndMetroVolumeOptions{ + DeleteRemoteVolume: true, + ForceDelete: false, + } +} + +func (s *MetroVolumeTestSuite) SetupTest() { + // Get a new volume for each test. + s.volID, _ = CreateVol(s.T()) + // sanitize for next test run. + s.metroSession.ID = "" +} + +func (s *MetroVolumeTestSuite) TearDownTest() { + // end the metro volume session if one was created + if s.metroSession.ID != "" { + _, err := C.EndMetroVolume(context.Background(), s.volID, &s.endMetroOpts) + assert.NoError(s.T(), err) + } + + // clean up any volumes from the array + DeleteVol(s.T(), s.volID) +} + +func (s *MetroVolumeTestSuite) TestConfigureMetroVolume() { + var err error + s.metroSession, err = C.ConfigureMetroVolume(context.Background(), s.volID, &s.metroConfig) + assert.NoError(s.T(), err) + assert.NotEmpty(s.T(), s.metroSession.ID) +} + +func (s *MetroVolumeTestSuite) TestConfigureMetroVolumeWithNonExistantVolume() { + var err error + // try to configure metro on a nonexistent volume + volID := "invalid" + s.metroSession, err = C.ConfigureMetroVolume(context.Background(), volID, &s.metroConfig) + assert.Equal(s.T(), http.StatusNotFound, err.(gopowerstore.APIError).StatusCode) + assert.Empty(s.T(), s.metroSession.ID) +} + +func (s *MetroVolumeTestSuite) TestConfigureMetroVolumeWithBadRemoteSystemId() { + var err error + s.metroSession, err = C.ConfigureMetroVolume(context.Background(), s.volID, &gopowerstore.MetroConfig{ + RemoteSystemID: "invalid-id", + }) + assert.Equal(s.T(), http.StatusNotFound, err.(gopowerstore.APIError).StatusCode) + assert.Empty(s.T(), s.metroSession.ID) +} + +func (s *MetroVolumeTestSuite) TestConfigureMetroVolumeOnExistingMetroVolume() { + var err error + // configure the volume for metro + s.metroSession, err = C.ConfigureMetroVolume(context.Background(), s.volID, &s.metroConfig) + assert.NoError(s.T(), err) + + // try to create the same metro config again + _, err = C.ConfigureMetroVolume(context.Background(), s.volID, &s.metroConfig) + assert.Equal(s.T(), http.StatusBadRequest, err.(gopowerstore.APIError).StatusCode) +} + +func (s *MetroVolumeTestSuite) TestEndMetroVolume() { + // configure the volume for metro + _, err := C.ConfigureMetroVolume(context.Background(), s.volID, &s.metroConfig) + assert.NoError(s.T(), err) + + // end the metro volume session with config to delete the remote volume + _, err = C.EndMetroVolume(context.Background(), s.volID, &s.endMetroOpts) + assert.NoError(s.T(), err) +} + +func (s *MetroVolumeTestSuite) TestEndMetroVolumeWithNonExistantVolume() { + badVolID := "invalid" + + // attempt to end metro volume session using a volume that should not exist. + _, err := C.EndMetroVolume(context.Background(), badVolID, &s.endMetroOpts) + assert.Equal(s.T(), http.StatusNotFound, err.(gopowerstore.APIError).StatusCode) +} + +func (s *MetroVolumeTestSuite) TestEndMetroVolumeWithUnreplicatedVolume() { + // try to end metro volume session on a volume that exists but is not part of a metro session + _, err := C.EndMetroVolume(context.Background(), s.volID, &s.endMetroOpts) + assert.Equal(s.T(), http.StatusBadRequest, err.(gopowerstore.APIError).StatusCode) +} diff --git a/mocks/Client.go b/mocks/Client.go index 3df2048..49ffd71 100644 --- a/mocks/Client.go +++ b/mocks/Client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.45.0. DO NOT EDIT. package mocks @@ -207,6 +207,34 @@ func (_m *Client) ComputeDifferences(ctx context.Context, snapdiffParams *gopowe return r0, r1 } +// ConfigureMetroVolume provides a mock function with given fields: ctx, id, config +func (_m *Client) ConfigureMetroVolume(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 ConfigureMetroVolume") + } + + 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) @@ -1253,6 +1281,34 @@ func (_m *Client) DetachVolumeFromHostGroup(ctx context.Context, hostGroupID str return r0, r1 } +// EndMetroVolume provides a mock function with given fields: ctx, id, options +func (_m *Client) EndMetroVolume(ctx context.Context, id string, options *gopowerstore.EndMetroVolumeOptions) (gopowerstore.EmptyResponse, error) { + ret := _m.Called(ctx, id, options) + + if len(ret) == 0 { + panic("no return value specified for EndMetroVolume") + } + + var r0 gopowerstore.EmptyResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.EndMetroVolumeOptions) (gopowerstore.EmptyResponse, error)); ok { + return rf(ctx, id, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *gopowerstore.EndMetroVolumeOptions) 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.EndMetroVolumeOptions) 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/replication_types.go b/replication_types.go index 9ff8d85..1c4ccd6 100644 --- a/replication_types.go +++ b/replication_types.go @@ -163,10 +163,22 @@ type ReplicationSession struct { LocalResourceID string `json:"local_resource_id,omitempty"` RemoteResourceID string `json:"remote_resource_id,omitempty"` RemoteSystemID string `json:"remote_system_id,omitempty"` // todo: maybe name? + // Type of replication session. One of Synchronous, Asynchronous, or Metro_Active_Active. + Type string `json:"type,omitempty"` StorageElementPairs []StorageElementPair `json:"storage_element_pairs,omitempty"` } func (r *ReplicationSession) Fields() []string { - return []string{"id", "state", "role", "resource_type", "local_resource_id", "remote_resource_id", "remote_system_id", "storage_element_pairs"} + return []string{"id", "state", "role", "resource_type", "local_resource_id", "remote_resource_id", "remote_system_id", "type", "storage_element_pairs"} } + +// ReplicationRoleEnum - List of replication role types associated with a replication session +type ReplicationRoleEnum string + +const ( + ReplicationRoleSource ReplicationRoleEnum = "Source" + ReplicationRoleDestination ReplicationRoleEnum = "Destination" + ReplicationRoleMetroPreferred ReplicationRoleEnum = "Metro_Preferred" + ReplicationRoleMetroNonPreferred ReplicationRoleEnum = "Metro_Non_Preferred" +) diff --git a/volume.go b/volume.go index d8c0025..de350bd 100644 --- a/volume.go +++ b/volume.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -237,7 +237,7 @@ func (c *ClientIMPL) ComputeDifferences(ctx context.Context, Method: "POST", Endpoint: volumeURL, ID: volID, - Action: "compute_differences", + Action: VolumeActionComputeDifferences, Body: computeDiffParams, }, &resp) @@ -254,7 +254,7 @@ func (c *ClientIMPL) CreateVolumeFromSnapshot(ctx context.Context, Method: "POST", Endpoint: volumeURL, ID: snapID, - Action: "clone", + Action: VolumeActionClone, Body: createParams, }, &resp) @@ -271,7 +271,7 @@ func (c *ClientIMPL) CreateSnapshot(ctx context.Context, Method: "POST", Endpoint: volumeURL, ID: id, - Action: "snapshot", + Action: VolumeActionSnapshot, Body: createSnapParams, }, &resp) @@ -311,7 +311,7 @@ func (c *ClientIMPL) CloneVolume(ctx context.Context, Method: "POST", Endpoint: volumeURL, ID: volID, - Action: "clone", + Action: VolumeActionClone, Body: createParams, }, &resp) @@ -354,3 +354,39 @@ func (c *ClientIMPL) GetApplianceByName(ctx context.Context, name string) (resp } return appList[0], err } + +// ConfigureMetroVolume configures the given volume, id, for metro replication with +// the remote PowerStore system and optional remote PowerStore appliance provided in config. +// Returns the metro replication session ID and any errors. +func (c *ClientIMPL) ConfigureMetroVolume(ctx context.Context, id string, config *MetroConfig) (resp MetroSessionResponse, err error) { + _, err = c.APIClient().Query( + ctx, + RequestConfig{ + Method: "POST", + Endpoint: volumeURL, + Action: VolumeActionConfigureMetro, + ID: id, + Body: config, + }, + &resp) + + return resp, WrapErr(err) +} + +// EndMetroVolume ends the metro session for a volume, id, between two PowerStore systems. +// deleteOpts provides options to delete the replicated volume on the remote system and +// whether or not to force the session removal. +func (c *ClientIMPL) EndMetroVolume(ctx context.Context, id string, deleteOpts *EndMetroVolumeOptions) (resp EmptyResponse, err error) { + _, err = c.APIClient().Query( + ctx, + RequestConfig{ + Method: "POST", + Endpoint: volumeURL, + Action: VolumeActionEndMetro, + ID: id, + Body: deleteOpts, + }, + &resp) + + return resp, WrapErr(err) +} diff --git a/volume_group_test.go b/volume_group_test.go index 1857fe3..02e4855 100644 --- a/volume_group_test.go +++ b/volume_group_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. @@ -116,7 +116,7 @@ func TestClientIMPL_GetVolumeGroupByName(t *testing.T) { func TestClientIMPL_GetVolumeGroupsByVolumeID(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - respData := fmt.Sprintf(`{ "volume_group": [{"id": "%s", "name": "%s"}] }`, volID2, "volume-group") + respData := fmt.Sprintf(`{ "volume_groups": [{"id": "%s", "name": "%s"}] }`, volID2, "volume-group") httpmock.RegisterResponder("GET", fmt.Sprintf("%s/%s", volumeMockURL, volID), httpmock.NewStringResponder(200, respData)) @@ -201,7 +201,7 @@ func TestClientIMPL_ModifyVolumeGroup(t *testing.T) { func TestClientIMPL_ModifyVolumeGroupSnapshot(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - respData := fmt.Sprintf(``) + respData := fmt.Sprint(``) httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/%s", volumeGroupMockURL, volID), httpmock.NewStringResponder(201, respData)) diff --git a/volume_test.go b/volume_test.go index 8c7f96a..41ff703 100644 --- a/volume_test.go +++ b/volume_test.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2023 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -21,10 +21,12 @@ package gopowerstore import ( "context" "fmt" + "net/http" "testing" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) const ( @@ -33,39 +35,53 @@ const ( ) var ( - volID = "6b930711-46bc-4a4b-9d6a-22c77a7838c4" - volID2 = "3765da74-28a7-49db-a693-10cec1de91f8" - appID = "A1" - volSnapID = "1966782b-60c9-40e2-a1ee-9b2b8f6b98e7" - volSnapID2 = "34380c29-2203-4490-aeb7-2853b9a85075" + volID = "6b930711-46bc-4a4b-9d6a-22c77a7838c4" + volID2 = "3765da74-28a7-49db-a693-10cec1de91f8" + appID = "A1" + volSnapID = "1966782b-60c9-40e2-a1ee-9b2b8f6b98e7" + volSnapID2 = "34380c29-2203-4490-aeb7-2853b9a85075" + metroConfig = MetroConfig{ + RemoteSystemID: "47921973-b0eb-485d-8492-c5d7f6ca216c", + RemoteApplianceID: appID, + } ) -func TestClientIMPL_GetVolumes(t *testing.T) { +type VolumeTestSuite struct { + suite.Suite +} + +func TestVolumeSuite(t *testing.T) { + suite.Run(t, new(VolumeTestSuite)) +} + +func (s *VolumeTestSuite) SetupTest() { httpmock.Activate() - defer httpmock.DeactivateAndReset() +} + +func (s *VolumeTestSuite) TearDownTest() { + httpmock.DeactivateAndReset() +} + +func (s *VolumeTestSuite) TestClientIMPL_GetVolumes() { respData := fmt.Sprintf(`[{"id": "%s"}, {"id": "%s"}]`, volID, volID2) httpmock.RegisterResponder("GET", volumeMockURL, httpmock.NewStringResponder(200, respData)) vols, err := C.GetVolumes(context.Background()) - assert.Nil(t, err) - assert.Len(t, vols, 2) - assert.Equal(t, volID, vols[0].ID) + assert.Nil(s.T(), err) + assert.Len(s.T(), vols, 2) + assert.Equal(s.T(), volID, vols[0].ID) } -func TestClientIMPL_GetVolume(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetVolume() { respData := fmt.Sprintf(`{"id": "%s"}`, volID) httpmock.RegisterResponder("GET", fmt.Sprintf("%s/%s", volumeMockURL, volID), httpmock.NewStringResponder(200, respData)) vol, err := C.GetVolume(context.Background(), volID) - assert.Nil(t, err) - assert.Equal(t, volID, vol.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volID, vol.ID) } -func TestClientIMPL_GetVolumeByName(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetVolumeByName() { setResponder := func(respData string) { httpmock.RegisterResponder("GET", volumeMockURL, httpmock.NewStringResponder(200, respData)) @@ -73,30 +89,26 @@ func TestClientIMPL_GetVolumeByName(t *testing.T) { respData := fmt.Sprintf(`[{"id": "%s"}]`, volID) setResponder(respData) vol, err := C.GetVolumeByName(context.Background(), "test") - assert.Nil(t, err) - assert.Equal(t, volID, vol.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volID, vol.ID) httpmock.Reset() setResponder("") _, err = C.GetVolumeByName(context.Background(), "test") - assert.NotNil(t, err) + assert.NotNil(s.T(), err) apiError := err.(APIError) - assert.True(t, apiError.NotFound()) + assert.True(s.T(), apiError.NotFound()) } -func TestClientIMPL_GetAppliance(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetAppliance() { respData := fmt.Sprintf(`{"id": "%s"}`, appID) httpmock.RegisterResponder("GET", fmt.Sprintf("%s/%s", applianceMockURL, appID), httpmock.NewStringResponder(200, respData)) app, err := C.GetAppliance(context.Background(), appID) - assert.Nil(t, err) - assert.Equal(t, appID, app.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), appID, app.ID) } -func TestClientIMPL_GetApplianceByName(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetApplianceByName() { setResponder := func(respData string) { httpmock.RegisterResponder("GET", applianceMockURL, httpmock.NewStringResponder(200, respData)) @@ -104,19 +116,17 @@ func TestClientIMPL_GetApplianceByName(t *testing.T) { respData := fmt.Sprintf(`[{"id": "%s"}]`, appID) setResponder(respData) ap, err := C.GetApplianceByName(context.Background(), "test") - assert.Nil(t, err) - assert.Equal(t, appID, ap.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), appID, ap.ID) httpmock.Reset() setResponder("") _, err = C.GetApplianceByName(context.Background(), "test") - assert.NotNil(t, err) + assert.NotNil(s.T(), err) apiError := err.(APIError) - assert.True(t, apiError.NotFound()) + assert.True(s.T(), apiError.NotFound()) } -func TestClientIMPL_GetSnapshotsByVolumeID(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetSnapshotsByVolumeID() { respData := fmt.Sprintf(`[{ "description":"", "id":"%s", @@ -143,37 +153,31 @@ func TestClientIMPL_GetSnapshotsByVolumeID(t *testing.T) { httpmock.NewStringResponder(200, respData)) resp, err := C.GetSnapshotsByVolumeID(context.Background(), volID2) - assert.Nil(t, err) - assert.Equal(t, 1, len(resp)) - assert.Equal(t, volID2, resp[0].ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(resp)) + assert.Equal(s.T(), volID2, resp[0].ID) } -func TestClientIMPL_GetSnapshot(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetSnapshot() { respData := fmt.Sprintf(`{"id": "%s"}`, volSnapID) httpmock.RegisterResponder("GET", fmt.Sprintf("%s/%s", volumeMockURL, volSnapID), httpmock.NewStringResponder(200, respData)) snapshot, err := C.GetSnapshot(context.Background(), volSnapID) - assert.Nil(t, err) - assert.Equal(t, volSnapID, snapshot.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volSnapID, snapshot.ID) } -func TestClientIMPL_GetSnapshots(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetSnapshots() { respData := fmt.Sprintf(`[{"id": "%s"}, {"id": "%s"}]`, volSnapID, volSnapID2) httpmock.RegisterResponder("GET", volumeMockURL, httpmock.NewStringResponder(200, respData)) snapshots, err := C.GetSnapshots(context.Background()) - assert.Nil(t, err) - assert.Len(t, snapshots, 2) - assert.Equal(t, volSnapID, snapshots[0].ID) + assert.Nil(s.T(), err) + assert.Len(s.T(), snapshots, 2) + assert.Equal(s.T(), volSnapID, snapshots[0].ID) } -func TestClientIMPL_GetSnapshotByName(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_GetSnapshotByName() { setResponder := func(respData string) { httpmock.RegisterResponder("GET", volumeMockURL, httpmock.NewStringResponder(200, respData)) @@ -181,19 +185,17 @@ func TestClientIMPL_GetSnapshotByName(t *testing.T) { respData := fmt.Sprintf(`[{"id": "%s"}]`, volSnapID) setResponder(respData) snap, err := C.GetSnapshotByName(context.Background(), "test") - assert.Nil(t, err) - assert.Equal(t, volSnapID, snap.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volSnapID, snap.ID) httpmock.Reset() setResponder("") _, err = C.GetSnapshotByName(context.Background(), "test") - assert.NotNil(t, err) + assert.NotNil(s.T(), err) apiError := err.(APIError) - assert.True(t, apiError.NotFound()) + assert.True(s.T(), apiError.NotFound()) } -func TestClientIMPL_CreateVolume(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_CreateVolume() { respData := fmt.Sprintf(`{"id": "%s"}`, volID) httpmock.RegisterResponder("POST", volumeMockURL, httpmock.NewStringResponder(201, respData)) @@ -204,13 +206,11 @@ func TestClientIMPL_CreateVolume(t *testing.T) { createReq.Size = &size resp, err := C.CreateVolume(context.Background(), &createReq) - assert.Nil(t, err) - assert.Equal(t, volID, resp.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volID, resp.ID) } -func TestClientIMPL_CreateSnapshot(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_CreateSnapshot() { respData := fmt.Sprintf(`{"id": "%s"}`, volID2) httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/snapshot", volumeMockURL, volID), httpmock.NewStringResponder(201, respData)) @@ -221,13 +221,11 @@ func TestClientIMPL_CreateSnapshot(t *testing.T) { createReq.Description = &desc resp, err := C.CreateSnapshot(context.Background(), &createReq, volID) - assert.Nil(t, err) - assert.Equal(t, volID2, resp.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volID2, resp.ID) } -func TestClientIMPL_CreateVolumeFromSnapshot(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_CreateVolumeFromSnapshot() { respData := fmt.Sprintf(`{"id": "%s"}`, volID2) httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/clone", volumeMockURL, volID), httpmock.NewStringResponder(201, respData)) @@ -236,13 +234,11 @@ func TestClientIMPL_CreateVolumeFromSnapshot(t *testing.T) { createParams := VolumeClone{} createParams.Name = &name resp, err := C.CreateVolumeFromSnapshot(context.Background(), &createParams, volID) - assert.Nil(t, err) - assert.Equal(t, volID2, resp.ID) + assert.Nil(s.T(), err) + assert.Equal(s.T(), volID2, resp.ID) } -func TestClientIMPL_ComputeDifferences(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_ComputeDifferences() { respData := `{"chunk_bitmap":"Dw==","next_offset":-1}` httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/compute_differences", volumeMockURL, volID), httpmock.NewStringResponder(201, respData)) @@ -258,14 +254,12 @@ func TestClientIMPL_ComputeDifferences(t *testing.T) { Offset: &offset, } resp, err := C.ComputeDifferences(context.Background(), &computeDiffParams, volID) - assert.Nil(t, err) - assert.Equal(t, "Dw==", *resp.ChunkBitmap) - assert.Equal(t, int64(-1), *resp.NextOffset) + assert.Nil(s.T(), err) + assert.Equal(s.T(), "Dw==", *resp.ChunkBitmap) + assert.Equal(s.T(), int64(-1), *resp.NextOffset) } -func TestClientIMPL_ModifyVolume(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_ModifyVolume() { respData := "" httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/%s", volumeMockURL, volID), httpmock.NewStringResponder(201, respData)) @@ -276,32 +270,51 @@ func TestClientIMPL_ModifyVolume(t *testing.T) { } resp, err := C.ModifyVolume(context.Background(), &modifyParams, volID) - assert.Nil(t, err) - assert.Equal(t, EmptyResponse(""), resp) + assert.Nil(s.T(), err) + assert.Equal(s.T(), EmptyResponse(""), resp) } -func TestClientIMPL_DeleteSnapshot(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_DeleteSnapshot() { httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/%s", volumeMockURL, volID), httpmock.NewStringResponder(204, "")) force := true deleteReq := VolumeDelete{} deleteReq.ForceInternal = &force resp, err := C.DeleteSnapshot(context.Background(), &deleteReq, volID) - assert.Nil(t, err) - assert.Len(t, string(resp), 0) + assert.Nil(s.T(), err) + assert.Len(s.T(), string(resp), 0) } -func TestClientIMPL_DeleteVolume(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func (s *VolumeTestSuite) TestClientIMPL_DeleteVolume() { httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/%s", volumeMockURL, volID), httpmock.NewStringResponder(204, "")) force := true deleteReq := VolumeDelete{} deleteReq.ForceInternal = &force resp, err := C.DeleteVolume(context.Background(), &deleteReq, volID) - assert.Nil(t, err) - assert.Len(t, string(resp), 0) + assert.Nil(s.T(), err) + assert.Len(s.T(), string(resp), 0) +} + +func (s *VolumeTestSuite) TestClientIMPL_ConfigureMetroVolume() { + sessionID := "test-id" + sessionIDJSON := fmt.Sprintf(`{"metro_replication_session_id": "%s"}`, sessionID) + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/%s", volumeMockURL, volID, VolumeActionConfigureMetro), + httpmock.NewStringResponder(http.StatusOK, sessionIDJSON)) + + resp, err := C.ConfigureMetroVolume(context.Background(), volID, &metroConfig) + assert.Nil(s.T(), err) + assert.Equal(s.T(), sessionID, resp.ID) +} + +func (s *VolumeTestSuite) TestClientIMPL_EndMetroVolume() { + opts := EndMetroVolumeOptions{DeleteRemoteVolume: true} + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/%s/%s", volumeMockURL, volID, VolumeActionEndMetro), + httpmock.NewStringResponder(http.StatusNoContent, "")) + + resp, err := C.EndMetroVolume(context.Background(), volID, &opts) + assert.Nil(s.T(), err) + assert.Empty(s.T(), resp) } diff --git a/volume_types.go b/volume_types.go index 01f2856..fe9bfac 100644 --- a/volume_types.go +++ b/volume_types.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2023 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-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. @@ -113,6 +113,16 @@ const ( AppTypeEnumRelationOther AppTypeEnum = "Other" ) +// Actions are used to build a PowerStore API query. Each action represents an +// endpoint under the /volume/ prefix. +const ( + VolumeActionClone string = "clone" + VolumeActionComputeDifferences string = "compute_differences" + VolumeActionConfigureMetro string = "configure_metro" + VolumeActionEndMetro string = "end_metro" + VolumeActionSnapshot string = "snapshot" +) + // VolumeCreate create volume request type VolumeCreate struct { // Unique name for the volume to be created. @@ -375,3 +385,33 @@ func (v *Volume) Fields() []string { func (n *ApplianceInstance) Fields() []string { return []string{"id", "name", "service_tag"} } + +// MetroConfig defines the properties required to configure a metro volume replication session. +type MetroConfig struct { + // RemoteSystemID is a required parameter specifying the remote PowerStore array/cluster on which + // the metro volume should be replicated. + RemoteSystemID string `json:"remote_system_id"` + // RemoteApplianceID is an optional parameter specifying a specific remote PowerStore appliance + // on which the metro volume or volume group should be replicated. + RemoteApplianceID string `json:"remote_appliance_id,omitempty"` +} + +// MetroSessionResponse id the Unique identifier of the replication session assigned +// to the volume if it has been configured as a metro volume between two PowerStore clusters. +type MetroSessionResponse struct { + // ID is a unique identifier of the metro replication session and + // is included in response to configuring a metro . + ID string `json:"metro_replication_session_id,omitempty"` +} + +// EndMetroVolumeOptions defines the options associated with deleting a metro volume. +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"` + // ForceDelete specifies if the Metro volume 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"` +}