From 27281047efeadbe4b88422cc856eee1f8ef838e6 Mon Sep 17 00:00:00 2001 From: delldubey Date: Wed, 8 Nov 2023 13:14:35 +0530 Subject: [PATCH] Upgrading unit test coverage (#55) --- mock/mock.go | 417 ++++++++++++++++++++++-------- mock/symmetrix13.json | 49 ++++ replication.go | 2 +- unit_steps_test.go | 134 ++++++++-- unittest/file.feature | 29 ++- unittest/pmax.feature | 4 +- unittest/pmax_replication.feature | 133 ++++++---- unittest/srdf.feature | 87 +++++-- 8 files changed, 658 insertions(+), 197 deletions(-) create mode 100644 mock/symmetrix13.json diff --git a/mock/mock.go b/mock/mock.go index 8f20db8..c7d5529 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -35,34 +35,38 @@ import ( // constants const ( - APIVersion = "{apiversion}" - PREFIX = "/univmax/restapi/" + APIVersion - PREFIXNOVERSION = "/univmax/restapi" - PRIVATEPREFIX = "/univmax/restapi/private/" + APIVersion - INTERNALPREFIX = "/univmax/restapi/internal/100" - defaultUsername = "username" - defaultPassword = "password" - Debug = false - DefaultStorageGroup = "CSI-Test-SG-1" - DefaultStorageGroup1 = "CSI-Test-SG-2" - DefaultProtectedStorageGroup = "CSI-Test-ProtectedSG" - DefaultSymmetrixID = "000197900046" - DefaultRemoteSymID = "000000000013" - DefaultRDFDir = "OR-1C" - DefaultRDFPort = 3 - PostELMSRSymmetrixID = "000197900047" - DefaultStoragePool = "SRP_1" - DefaultServiceLevel = "Optimized" - DefaultFcStoragePortWWN = "5000000000000001" - DefaultRDFGNo = 13 - DefaultRemoteRDFGNo = 13 - DefaultRDFLabel = "csi-mock-test" - RemoteArrayHeaderKey = "RemoteArray" - RemoteArrayHeaderValue = "true" - DefaultNASServerID = "64xxx7a6-03b5-xxx-xxx-0zzzz8200209" - DefaultNASServerName = "nas-1" - DefaultFSID = "64xxx7a6-03b5-xxx-xxx-0zzzz8200208" - DefaultFSName = "fs-ds-1" + APIVersion = "{apiversion}" + PREFIX = "/univmax/restapi/" + APIVersion + PREFIXNOVERSION = "/univmax/restapi" + PRIVATEPREFIX = "/univmax/restapi/private/" + APIVersion + INTERNALPREFIX = "/univmax/restapi/internal/100" + defaultUsername = "username" + defaultPassword = "password" + Debug = false + DefaultStorageGroup = "CSI-Test-SG-1" + DefaultStorageGroup1 = "CSI-Test-SG-2" + DefaultASYNCProtectedSG = "csi-rep-sg-ns-test" + DefaultMETROProtectedSG = "csi-rep-sg-ns-test" + DefaultSymmetrixID = "000197900046" + DefaultRemoteSymID = "000000000013" + DefaultRDFDir = "OR-1C" + DefaultRDFPort = 3 + PostELMSRSymmetrixID = "000197900047" + DefaultStoragePool = "SRP_1" + DefaultServiceLevel = "Optimized" + DefaultFcStoragePortWWN = "5000000000000001" + DefaultAsyncRDFGNo = 13 + DefaultAsyncRemoteRDFGNo = 13 + DefaultAsyncRDFLabel = "csi-mock-async" + DefaultMetroRDFGNo = 14 + DefaultRemoteRDFGNo = 14 + DefaultMetroRDFLabel = "csi-mock-metro" + RemoteArrayHeaderKey = "RemoteArray" + RemoteArrayHeaderValue = "true" + DefaultNASServerID = "64xxx7a6-03b5-xxx-xxx-0zzzz8200209" + DefaultNASServerName = "nas-1" + DefaultFSID = "64xxx7a6-03b5-xxx-xxx-0zzzz8200208" + DefaultFSName = "fs-ds-1" ) const ( @@ -112,13 +116,16 @@ var Data struct { // SRDF StorageGroupIDToRDFStorageGroup map[string]*types.RDFStorageGroup - RDFGroup *types.RDFGroup - SGRDFInfo *types.SGRDFInfo + AsyncRDFGroup *types.RDFGroup + MetroRDFGroup *types.RDFGroup + AsyncSGRDFInfo *types.SGRDFInfo + MetroSGRDFInfo *types.SGRDFInfo // File - FileSysIDToFileSystem map[string]*types.FileSystem - NFSExportIDToNFSExport map[string]*types.NFSExport - NASServerIDToNASServer map[string]*types.NASServer + FileSysIDToFileSystem map[string]*types.FileSystem + NFSExportIDToNFSExport map[string]*types.NFSExport + NASServerIDToNASServer map[string]*types.NASServer + FileIntIDtoFileInterface map[string]*types.FileInterface } // InducedErrors constants @@ -241,6 +248,8 @@ var InducedErrors struct { CreateNFSExportError bool UpdateNFSExportError bool DeleteNFSExportError bool + GetFileInterfaceError bool + ExecuteActionError bool } // hasError checks to see if the specified error (via pointer) @@ -360,6 +369,10 @@ func Reset() { InducedErrors.CreateSnapshotPolicyError = false InducedErrors.ModifySnapshotPolicyError = false InducedErrors.DeleteSnapshotPolicyError = false + InducedErrors.GetStorageGroupSnapshotError = false + InducedErrors.CreateSnapshotPolicyError = false + InducedErrors.GetStorageGroupSnapshotSnapError = false + InducedErrors.GetStorageGroupSnapshotSnapModifyError = false InducedErrors.GetFileSystemListError = false InducedErrors.GetNFSExportListError = false InducedErrors.GetNASServerListError = false @@ -374,6 +387,8 @@ func Reset() { InducedErrors.CreateNFSExportError = false InducedErrors.UpdateNFSExportError = false InducedErrors.DeleteNFSExportError = false + InducedErrors.GetFileInterfaceError = false + InducedErrors.ExecuteActionError = false Data.JSONDir = "mock" Data.VolumeIDToIdentifier = make(map[string]string) Data.VolumeIDToSize = make(map[string]int) @@ -402,10 +417,11 @@ func Reset() { Data.FileSysIDToFileSystem = make(map[string]*types.FileSystem) Data.NFSExportIDToNFSExport = make(map[string]*types.NFSExport) Data.NASServerIDToNASServer = make(map[string]*types.NASServer) - Data.RDFGroup = &types.RDFGroup{ - RdfgNumber: DefaultRDFGNo, - Label: DefaultRDFLabel, - RemoteRdfgNumber: DefaultRDFGNo, + Data.FileIntIDtoFileInterface = make(map[string]*types.FileInterface) + Data.AsyncRDFGroup = &types.RDFGroup{ + RdfgNumber: DefaultAsyncRDFGNo, + Label: DefaultAsyncRDFLabel, + RemoteRdfgNumber: DefaultAsyncRDFGNo, RemoteSymmetrix: DefaultRemoteSymID, NumDevices: 0, TotalDeviceCapacity: 0.0, @@ -413,13 +429,31 @@ func Reset() { Type: "Dynamic", Async: true, } - Data.SGRDFInfo = &types.SGRDFInfo{ - RdfGroupNumber: DefaultRDFGNo, + Data.MetroRDFGroup = &types.RDFGroup{ + RdfgNumber: DefaultMetroRDFGNo, + Label: DefaultMetroRDFLabel, + RemoteRdfgNumber: DefaultMetroRDFGNo, + RemoteSymmetrix: DefaultRemoteSymID, + NumDevices: 0, + TotalDeviceCapacity: 0.0, + Modes: []string{"Active"}, + Type: "Metro", + Metro: true, + } + Data.AsyncSGRDFInfo = &types.SGRDFInfo{ + RdfGroupNumber: DefaultAsyncRDFGNo, VolumeRdfTypes: []string{"R1"}, States: []string{"Consistent"}, Modes: []string{"Asynchronous"}, LargerRdfSides: []string{"Equal"}, } + Data.MetroSGRDFInfo = &types.SGRDFInfo{ + RdfGroupNumber: DefaultMetroRDFGNo, + VolumeRdfTypes: []string{"R1"}, + States: []string{"Consistent"}, + Modes: []string{"Active"}, + LargerRdfSides: []string{"Equal"}, + } initMockCache() } @@ -433,8 +467,11 @@ func initMockCache() { AddStorageGroup("CSI-Test-SG-6", "None", "None") // #nosec G20 AddStorageGroup("CSI-Test-Fake-Remote-SG", "None", "None") // #nosec G20 // Initialize protected SG - AddStorageGroup(DefaultProtectedStorageGroup, "None", "None") // #nosec G20 - AddRDFStorageGroup(DefaultProtectedStorageGroup, DefaultRemoteSymID) // #nosec G20 + AddStorageGroup(DefaultASYNCProtectedSG, "None", "None") // #nosec G20 + AddStorageGroup(DefaultMETROProtectedSG, "None", "None") // #nosec G20 + AddRDFStorageGroup(DefaultASYNCProtectedSG, DefaultRemoteSymID) // #nosec G20 + AddRDFStorageGroup(DefaultMETROProtectedSG, DefaultRemoteSymID) // #nosec G20 + // ISCSI directors iscsiDir1 := "SE-1E" iscsidir1PortKey1 := iscsiDir1 + ":" + "4" @@ -487,13 +524,15 @@ func initMockCache() { // AddFileObjects adds file objects for mock objects func AddFileObjects() { // Add a File System - AddNewFileSystem("id1", "fs-1", 4000) + AddNewFileSystem("id1", DefaultFSName, 4000) // Add a NFS Export AddNewNFSExport("id1", "nfs-0") AddNewNFSExport("id2", "nfs-del") // Add a NAS Server AddNewNASServer("id1", "nas-1") AddNewNASServer("id2", "nas-del") + // Add a FileInterface + AddNewFileInterface("id1", "interface-1") } var mockRouter http.Handler @@ -603,6 +642,8 @@ func getRouter() http.Handler { router.HandleFunc(PREFIX+"/file/symmetrix/{symid}/nfs_export", handleNFSExport) router.HandleFunc(PREFIX+"/file/symmetrix/{symid}/nas_server/{nasID}", handleNASServer) router.HandleFunc(PREFIX+"/file/symmetrix/{symid}/nas_server", handleNASServer) + router.HandleFunc(PREFIX+"/file/symmetrix/{symid}/file_interface", handleFileInterface) + router.HandleFunc(PREFIX+"/file/symmetrix/{symid}/file_interface/{interfaceID}", handleFileInterface) mockRouter = router return router @@ -893,8 +934,8 @@ func handleFreeRDF(w http.ResponseWriter, r *http.Request) { return } nxtFreeRDFG := &types.NextFreeRDFGroup{ - LocalRdfGroup: []int{DefaultRDFGNo}, - RemoteRdfGroup: []int{DefaultRDFGNo}, + LocalRdfGroup: []int{DefaultAsyncRDFGNo}, + RemoteRdfGroup: []int{DefaultAsyncRDFGNo}, } writeJSON(w, nxtFreeRDFG) } @@ -936,8 +977,8 @@ func handleRDFDevicePairCreation(w http.ResponseWriter, r *http.Request) { LocalVolumeName: routeParams["volume_id"], RemoteSymmID: routeParams["symid"], LocalSymmID: routeParams["symid"], - LocalRdfGroupNumber: DefaultRDFGNo, - RemoteRdfGroupNumber: DefaultRemoteRDFGNo, + LocalRdfGroupNumber: DefaultAsyncRDFGNo, + RemoteRdfGroupNumber: DefaultAsyncRemoteRDFGNo, }, } writeJSON(w, rdfPairs) @@ -955,20 +996,20 @@ func handleRDFDevicePairInfo(w http.ResponseWriter, r *http.Request) { } routeParams := mux.Vars(r) var volumeConfig string - if routeParams["symid"] == Data.RDFGroup.RemoteSymmetrix { + if routeParams["symid"] == Data.AsyncRDFGroup.RemoteSymmetrix { volumeConfig = "RDF2+TDEV" } else { volumeConfig = "RDF1+TDEV" } rdfDevicePairInfo := &types.RDFDevicePair{ - LocalRdfGroupNumber: Data.RDFGroup.RdfgNumber, - RemoteRdfGroupNumber: Data.RDFGroup.RdfgNumber, + LocalRdfGroupNumber: Data.AsyncRDFGroup.RdfgNumber, + RemoteRdfGroupNumber: Data.AsyncRDFGroup.RdfgNumber, LocalSymmID: routeParams["symid"], - RemoteSymmID: Data.RDFGroup.RemoteSymmetrix, + RemoteSymmID: Data.AsyncRDFGroup.RemoteSymmetrix, LocalVolumeName: routeParams["volume_id"], RemoteVolumeName: routeParams["volume_id"], VolumeConfig: volumeConfig, - RdfMode: Data.RDFGroup.Modes[0], + RdfMode: Data.AsyncRDFGroup.Modes[0], RdfpairState: "Consistent", LargerRdfSide: "Equal", } @@ -992,7 +1033,7 @@ func handleRDFGroup(w http.ResponseWriter, r *http.Request) { rdfGroupNumber := routeParams["rdf_no"] ReturnRDFGroup(w, rdfGroupNumber) case http.MethodPost: - writeJSON(w, Data.RDFGroup) + writeJSON(w, Data.AsyncRDFGroup) default: writeError(w, "Method["+r.Method+"] not allowed", http.StatusMethodNotAllowed) } @@ -1007,13 +1048,13 @@ func ReturnRDFGroup(w http.ResponseWriter, rdfg string) { func returnRDFGroup(w http.ResponseWriter, rdfg string) { if rdfg != "" { - if rdfg != fmt.Sprintf("%d", Data.RDFGroup.RdfgNumber) { + if rdfg != fmt.Sprintf("%d", Data.AsyncRDFGroup.RdfgNumber) && rdfg != fmt.Sprintf("%d", Data.MetroRDFGroup.RdfgNumber) { writeError(w, "The specified RA group is not valid", http.StatusNotFound) } else { if InducedErrors.RDFGroupHasPairError { - Data.RDFGroup.NumDevices = 1 + Data.AsyncRDFGroup.NumDevices = 1 } - writeJSON(w, Data.RDFGroup) + writeJSON(w, Data.AsyncRDFGroup) } } else { rdflist := &types.RDFGroupList{ @@ -1088,6 +1129,7 @@ func handleSGRDFCreation(w http.ResponseWriter, r *http.Request) { return } routeParams := mux.Vars(r) + mode := sgsrdf.ReplicationMode storageGroupName := routeParams["id"] symmetrixID := routeParams["symid"] if _, err := AddRDFStorageGroup(storageGroupName, symmetrixID); err != nil { @@ -1100,12 +1142,24 @@ func handleSGRDFCreation(w http.ResponseWriter, r *http.Request) { } volume := Data.VolumeIDToVolume[volumeID] volume.Type = "RDF1+TDEV" - volume.RDFGroupIDList = []types.RDFGroupID{ - {RDFGroupNumber: Data.RDFGroup.RdfgNumber}, + if strings.Compare(mode, "Active") == 0 { + volume.RDFGroupIDList = []types.RDFGroupID{ + {RDFGroupNumber: Data.MetroRDFGroup.RdfgNumber}, + } + } else { + volume.RDFGroupIDList = []types.RDFGroupID{ + {RDFGroupNumber: Data.AsyncRDFGroup.RdfgNumber}, + } } } sgrdfInfo := new(types.SGRDFInfo) - err := copier.Copy(sgrdfInfo, Data.SGRDFInfo) + dataToCopy := new(types.SGRDFInfo) + if mode == "Active" { + dataToCopy = Data.MetroSGRDFInfo + } else { + dataToCopy = Data.AsyncSGRDFInfo + } + err := copier.Copy(sgrdfInfo, dataToCopy) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return @@ -1129,15 +1183,21 @@ func handleSGRDF(w http.ResponseWriter, r *http.Request) { func handleSGRDFInfo(w http.ResponseWriter, r *http.Request) { if InducedErrors.GetSRDFInfoError { - writeError(w, "Error retrieving SRDF Info(%s): induced error", http.StatusRequestTimeout) + writeError(w, "Error retrieving SRDF Info: induced error", http.StatusRequestTimeout) return } routeParams := mux.Vars(r) - if routeParams["rdf_no"] != fmt.Sprintf("%d", Data.RDFGroup.RdfgNumber) { + rdfNo := routeParams["rdf_no"] + if rdfNo != fmt.Sprintf("%d", Data.AsyncRDFGroup.RdfgNumber) && rdfNo != fmt.Sprintf("%d", Data.MetroRDFGroup.RdfgNumber) { writeError(w, "The specified RA group is not valid", http.StatusNotFound) } else { sgrdfInfo := new(types.SGRDFInfo) - err := copier.Copy(sgrdfInfo, Data.SGRDFInfo) + var err error + if rdfNo == fmt.Sprintf("%d", Data.AsyncRDFGroup.RdfgNumber) { + err = copier.Copy(sgrdfInfo, Data.AsyncSGRDFInfo) + } else { + err = copier.Copy(sgrdfInfo, Data.MetroSGRDFInfo) + } if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return @@ -1151,9 +1211,56 @@ func handleSGRDFInfo(w http.ResponseWriter, r *http.Request) { } } -func handleSGRDFAction(w http.ResponseWriter, _ *http.Request) { - // TODO: execute actions by updating the memory cache - w.WriteHeader(200) +func handleSGRDFAction(w http.ResponseWriter, r *http.Request) { + if InducedErrors.ExecuteActionError { + writeError(w, "Failed to execute action on RDFG: induced error", http.StatusBadRequest) + return + } + routeParams := mux.Vars(r) + rdfNo := routeParams["rdf_no"] + decoder := json.NewDecoder(r.Body) + modifySRDFGParam := &types.ModifySGRDFGroup{} + err := decoder.Decode(modifySRDFGParam) + if err != nil { + writeError(w, "problem decoding PUT ACTION payload: "+err.Error(), http.StatusBadRequest) + return + } + action := modifySRDFGParam.Action + PerformActionOnRDFSG(w, rdfNo, action) +} + +func PerformActionOnRDFSG(w http.ResponseWriter, rdfNo, action string) { + mockCacheMutex.Lock() + defer mockCacheMutex.Unlock() + performActionOnRDFSG(w, rdfNo, action) +} + +func performActionOnRDFSG(w http.ResponseWriter, rdfNo, action string) { + if rdfNo != fmt.Sprintf("%d", Data.AsyncRDFGroup.RdfgNumber) && rdfNo != fmt.Sprintf("%d", Data.MetroRDFGroup.RdfgNumber) { + writeError(w, "The specified RA group is not valid", http.StatusNotFound) + } else { + // we only support actions on ASYNC + switch action { + case "Establish": + Data.AsyncSGRDFInfo.States = []string{"Consistent"} + return + case "Suspend": + Data.AsyncSGRDFInfo.States = []string{"Suspended"} + return + case "Resume": + Data.AsyncSGRDFInfo.States = []string{"Consistent"} + return + case "Failback": + Data.AsyncSGRDFInfo.States = []string{"Consistent"} + return + case "Failover": + Data.AsyncSGRDFInfo.States = []string{"Failed Over"} + return + case "Swap": + Data.AsyncSGRDFInfo.States = []string{"Consistent"} + return + } + } } // GET /univmax/restapi/system/version @@ -1193,7 +1300,7 @@ func handleSymmetrix(w http.ResponseWriter, r *http.Request) { if id == "" { returnJSONFile(Data.JSONDir, "symmetrixList.json", w, nil) } - if id != "000197900046" && id != "000197900047" { + if id != "000197900046" && id != "000197900047" && id != DefaultRemoteSymID { writeError(w, "Symmetrix not found", http.StatusNotFound) return } @@ -1201,6 +1308,8 @@ func handleSymmetrix(w http.ResponseWriter, r *http.Request) { returnJSONFile(Data.JSONDir, "symmetrix46.json", w, nil) } else if id == "000197900047" { returnJSONFile(Data.JSONDir, "symmetrix47.json", w, nil) + } else { + returnJSONFile(Data.JSONDir, "symmetrix13.json", w, nil) } } @@ -1295,7 +1404,7 @@ func handleVolume(w http.ResponseWriter, r *http.Request) { return } if volID != "" { - if vars["symid"] == Data.RDFGroup.RemoteSymmetrix { + if vars["symid"] == Data.AsyncRDFGroup.RemoteSymmetrix { returnVolume(w, volID, true) } else { returnVolume(w, volID, false) @@ -1329,7 +1438,7 @@ func handleVolume(w http.ResponseWriter, r *http.Request) { return } if updateVolumePayload.EditVolumeActionParam.ModifyVolumeIdentifierParam != nil { - if vars["symid"] == Data.RDFGroup.RemoteSymmetrix { + if vars["symid"] == Data.AsyncRDFGroup.RemoteSymmetrix { RenameVolume(w, updateVolumePayload.EditVolumeActionParam.ModifyVolumeIdentifierParam, volID, executionOption, true) } else { RenameVolume(w, updateVolumePayload.EditVolumeActionParam.ModifyVolumeIdentifierParam, volID, executionOption, false) @@ -1368,7 +1477,7 @@ func deleteVolume(volID string) error { } Data.VolumeIDToVolume[volID] = nil } else { - return errors.New("Volume not found") + return errors.New("Could not find volume") } return nil } @@ -1392,14 +1501,14 @@ func returnVolume(w http.ResponseWriter, volID string, remote bool) { return } if InducedErrors.GetRemoteVolumeError { - writeError(w, "Volume cannot be found", http.StatusNotFound) + writeError(w, "Could not find volume", http.StatusNotFound) return } if InducedErrors.InvalidRemoteVolumeError { newVol.StorageGroupIDList = nil } if !strings.Contains(vol.Type, "RDF") { - writeError(w, "Volume not found", http.StatusNotFound) + writeError(w, "Could not find volume", http.StatusNotFound) return } newVol.Type = strings.ReplaceAll(newVol.Type, "RDF1", "RDF2") @@ -1408,7 +1517,7 @@ func returnVolume(w http.ResponseWriter, volID string, remote bool) { writeJSON(w, newVol) return } - writeError(w, "Volume cannot be found: "+volID, http.StatusNotFound) + writeError(w, "Could not find volume: "+volID, http.StatusNotFound) } } @@ -1655,7 +1764,7 @@ func handleStorageGroup(w http.ResponseWriter, r *http.Request) { writeError(w, "Error retrieving Storage Group(s): induced error", http.StatusRequestTimeout) return } - if vars["symid"] == Data.RDFGroup.RemoteSymmetrix && strings.Contains(sgID, "rep") { + if vars["symid"] == Data.AsyncRDFGroup.RemoteSymmetrix && strings.Contains(sgID, "rep") { ReturnStorageGroup(w, sgID, true) } else { ReturnStorageGroup(w, sgID, false) @@ -1748,7 +1857,7 @@ func handleStorageGroup(w http.ResponseWriter, r *http.Request) { // Data.StorageGroupIDToNVolumes[sgID] = 0 // fmt.Println("SG Name: ", sgID) AddStorageGroupFromCreateParams(createSGPayload) - if vars["symid"] == Data.RDFGroup.RemoteSymmetrix { + if vars["symid"] == Data.AsyncRDFGroup.RemoteSymmetrix { ReturnStorageGroup(w, sgID, true) } else { ReturnStorageGroup(w, sgID, false) @@ -2193,7 +2302,7 @@ func newVolume(volumeID, volumeIdentifier string, size int, sgList []string) { if _, ok := Data.StorageGroupIDToRDFStorageGroup[sgList[0]]; ok { volume.Type = "RDF1+TDEV" volume.RDFGroupIDList = []types.RDFGroupID{ - {RDFGroupNumber: Data.RDFGroup.RdfgNumber}, + {RDFGroupNumber: Data.AsyncRDFGroup.RdfgNumber}, } } Data.VolumeIDToVolume[volumeID] = volume @@ -2645,8 +2754,14 @@ func addOneVolumeToStorageGroup(volumeID, volumeIdentifier, sgID string, size in // Update volume's replication details in case the storage-group is replicated if _, ok := Data.StorageGroupIDToRDFStorageGroup[sgID]; ok { Data.VolumeIDToVolume[volumeID].Type = "RDF1+TDEV" - Data.VolumeIDToVolume[volumeID].RDFGroupIDList = []types.RDFGroupID{ - {RDFGroupNumber: Data.RDFGroup.RdfgNumber}, + if strings.Contains(sgID, "ASYNC") { + Data.VolumeIDToVolume[volumeID].RDFGroupIDList = []types.RDFGroupID{ + {RDFGroupNumber: Data.AsyncRDFGroup.RdfgNumber}, + } + } else { + Data.VolumeIDToVolume[volumeID].RDFGroupIDList = []types.RDFGroupID{ + {RDFGroupNumber: Data.MetroRDFGroup.RdfgNumber}, + } } } @@ -3763,7 +3878,7 @@ func handleVolSnaps(w http.ResponseWriter, r *http.Request) { return } if Data.VolumeIDToVolume[volID] == nil { - writeError(w, "Volume cannot be found: "+volID, http.StatusNotFound) + writeError(w, "Could not find volume: "+volID, http.StatusNotFound) return } @@ -3864,7 +3979,7 @@ func handleGenerations(w http.ResponseWriter, r *http.Request) { mockCacheMutex.Lock() defer mockCacheMutex.Unlock() if Data.VolumeIDToVolume[volID] == nil { - writeError(w, "Volume cannot be found: "+volID, http.StatusNotFound) + writeError(w, "Could not find volume: "+volID, http.StatusNotFound) return } @@ -4515,9 +4630,61 @@ func handleFileSystem(w http.ResponseWriter, r *http.Request) { return } if InducedErrors.GetFileSystemError { - writeError(w, "Error retrieving file system: induced error", http.StatusNotFound) + writeError(w, "Error retrieving file system, Could not find: induced error", http.StatusNotFound) return } + if fsID == "" { + // send in a list of file Syste + // Here we want a volume iterator. + queryParams := r.URL.Query() + fileIdentifier := queryParams.Get("name") + if fileIdentifier != "" { + fsIDNameList := make([]types.FileSystemIDName, 0) + for _, fs := range Data.FileSysIDToFileSystem { + if fs.Name == fileIdentifier { + fsIDName := types.FileSystemIDName{ + ID: fs.ID, + Name: fs.Name, + } + fsIDNameList = append(fsIDNameList, fsIDName) + } + } + fileSysIter := &types.FileSystemIterator{ + ResultList: types.FileSystemList{ + FileSystemList: fsIDNameList, + From: 1, + To: len(fsIDNameList), + }, + ID: "52248851-fd6b-42c8-b7c7-2a9c0e40441a_0", + Count: len(fsIDNameList), + ExpirationTime: 1688114398468, + MaxPageSize: 1000, + } + writeJSON(w, fileSysIter) + } else { + fileSysIter := &types.FileSystemIterator{ + ResultList: types.FileSystemList{ + FileSystemList: []types.FileSystemIDName{ + { + ID: DefaultFSID, + Name: DefaultFSName, + }, + { + ID: "64xxx7a6-03b5-xxx-xxx-0zzzz8200209", + Name: "fs-ds-2", + }, + }, + From: 1, + To: 2, + }, + ID: "52248851-fd6b-42c8-b7c7-2a9c0e40441a_0", + Count: 2, + ExpirationTime: 1688114398468, + MaxPageSize: 1000, + } + writeJSON(w, fileSysIter) + } + } ReturnFileSystem(w, fsID) case http.MethodPost: if InducedErrors.CreateFileSystemError { @@ -4531,8 +4698,10 @@ func handleFileSystem(w http.ResponseWriter, r *http.Request) { writeError(w, "InvalidJson", http.StatusBadRequest) return } - AddNewFileSystem("id-2", createFileSystemParam.Name, createFileSystemParam.SizeTotal) - ReturnFileSystem(w, "id-2") + id := strconv.Itoa(time.Now().Nanosecond()) + fsID := fmt.Sprintf("%s-%s-%d-%s", "649112ce-742b", "id", len(Data.FileSysIDToFileSystem), id) + AddNewFileSystem(fsID, createFileSystemParam.Name, createFileSystemParam.SizeTotal) + ReturnFileSystem(w, fsID) case http.MethodPut: if InducedErrors.UpdateFileSystemError { writeError(w, "Error updating file system: induced error", http.StatusRequestTimeout) @@ -4580,29 +4749,6 @@ func returnFileSystem(w http.ResponseWriter, fsID string) { return } writeJSON(w, fileSys) - } else { - // send in a list of file System - fileSysIter := &types.FileSystemIterator{ - ResultList: types.FileSystemList{ - FileSystemList: []types.FileSystemIDName{ - { - ID: DefaultFSID, - Name: DefaultFSName, - }, - { - ID: "64xxx7a6-03b5-xxx-xxx-0zzzz8200209", - Name: "fs-ds-2", - }, - }, - From: 1, - To: 2, - }, - ID: "52248851-fd6b-42c8-b7c7-2a9c0e40441a_0", - Count: 2, - ExpirationTime: 1688114398468, - MaxPageSize: 1000, - } - writeJSON(w, fileSysIter) } } @@ -4783,6 +4929,69 @@ func newFileSystem(fsID, fsName string, sizeInMiB int64) *types.FileSystem { } } +// AddNewFileInterface adds a new file interface into mock +func AddNewFileInterface(id, name string) { + mockCacheMutex.Lock() + defer mockCacheMutex.Unlock() + addNewFileInterface(id, name) +} + +func addNewFileInterface(interfaceID, interfaceName string) { + Data.FileIntIDtoFileInterface[interfaceID] = newFileInterface(interfaceID, interfaceName) +} + +func newFileInterface(interfaceID, interfaceName string) *types.FileInterface { + return &types.FileInterface{ + ID: interfaceID, + NasServer: DefaultNASServerID, + NetDevice: "eth-1", + MacAddress: "01:01:ab:01:01:zx", + IPAddress: "100.125.0.109", + Netmask: "255.255.255.0", + Gateway: "172.125.0.1", + VlanID: 0, + Name: interfaceName, + Role: "Production", + IsDisabled: false, + Override: false, + } +} + +// /univmax/restapi/100/file/symmetrix/{symID}//file_interface/ +// /univmax/restapi/100/file/symmetrix/file_interface/{interfaceID} +func handleFileInterface(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + interfaceID := vars["interfaceID"] + switch r.Method { + case http.MethodGet: + if InducedErrors.GetFileInterfaceError { + writeError(w, "Error retrieving FileSystemInterface: induced error", http.StatusNotFound) + return + } + ReturnFileInterface(w, interfaceID) + default: + writeError(w, "Invalid Method", http.StatusBadRequest) + } +} + +// ReturnFileInterface returns File Interface object +func ReturnFileInterface(w http.ResponseWriter, interfaceID string) { + mockCacheMutex.Lock() + defer mockCacheMutex.Unlock() + returnFileInterface(w, interfaceID) +} + +func returnFileInterface(w http.ResponseWriter, interfaceID string) { + if interfaceID != "" { + if fi, ok := Data.FileIntIDtoFileInterface[interfaceID]; ok { + writeJSON(w, fi) + } else { + writeError(w, "Could not find FileInterface", http.StatusNotFound) + return + } + } +} + func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { diff --git a/mock/symmetrix13.json b/mock/symmetrix13.json new file mode 100644 index 0000000..c715765 --- /dev/null +++ b/mock/symmetrix13.json @@ -0,0 +1,49 @@ +{ + "symmetrixId": "000000000013", + "local": true, + "model": "PowerMax_2500", + "microcode": "6079.175.0", + "microcode_date": "10-12-2023", + "microcode_registered_build": 268, + "microcode_package_version": "10.0.1.3 (HotFix 10108, Build 6079_175/0268, 2023-10-12 19:17:49)", + "cache_size_mb": 454656, + "fe_dir_count": 4, + "be_dir_count": 4, + "rdf_dir_count": 4, + "max_hyper_per_disk": 512, + "dev_masking_aclx_config": "Enabled", + "aclx_lun_addr": 0, + "config_change_state": "Enabled", + "disk_group_assignment": "In Use", + "raid_config": "RAID-5 (4+1)", + "pav_model": "DynamicStandardPAV", + "pav_alias_limit": 255, + "sddf_state": "Enabled", + "srdfa_max_throttle": 0, + "srdfa_cache_usage": 75, + "max_sys_slot": 1656000, + "max_dev_slot": 82800, + "last_ipl_time": "2023-02-20 16:34:27", + "last_fast_ipl_time": "2023-10-16 23:46:25", + "symm_alert": "11/39", + "service_level_rt_multiplier": "Default", + "fba_geo_emulation": "Native", + "cache_partition": "Disabled", + "disk_service_state": "Deferred", + "data_encryption": "Enabled", + "rep_cache_usage": 0, + "device_count": 15458, + "disk_count": 33, + "spare_disk_count": 1, + "unconfig_disk_count": 0, + "reliability_state": "Degraded-NoRebuild", + "all_flash": true, + "system_sized_property": [ + { + "srp_name": "SRP_1", + "sized_fba_data_reduction_ratio": "4.0:1", + "sized_fba_capacity_tb": 1196, + "sized_fba_reducible_percent": 90 + } + ] +} \ No newline at end of file diff --git a/replication.go b/replication.go index 2659eb2..20454f5 100644 --- a/replication.go +++ b/replication.go @@ -251,7 +251,7 @@ func (c *Client) DeleteStorageGroupSnapshot(ctx context.Context, symID string, s if err != nil { log.Error("Error in Delete Storage Group Snapshot: " + err.Error()) } else { - log.Info(fmt.Sprintf("Successfully deleted volume: %s", snapID)) + log.Infof("Successfully deleted Storage Group Snapshot: %s", snapID) } return err } diff --git a/unit_steps_test.go b/unit_steps_test.go index 61855ae..111c6dc 100644 --- a/unit_steps_test.go +++ b/unit_steps_test.go @@ -135,6 +135,7 @@ type unitContext struct { fileSystem *types.FileSystem nfsExport *types.NFSExport nasServer *types.NASServer + fileInterface *types.FileInterface } func (c *unitContext) reset() { @@ -177,6 +178,7 @@ func (c *unitContext) reset() { c.fileSystem = nil c.nfsExport = nil c.nasServer = nil + c.fileInterface = nil } func (c *unitContext) iInduceError(errorType string) error { @@ -250,6 +252,18 @@ func (c *unitContext) iInduceError(errorType string) error { mock.InducedErrors.CreateNFSExportError = false mock.InducedErrors.UpdateNFSExportError = false mock.InducedErrors.DeleteNFSExportError = false + mock.InducedErrors.GetFileInterfaceError = false + mock.InducedErrors.ExecuteActionError = false + mock.InducedErrors.CreateSnapshotPolicyError = false + mock.InducedErrors.GetStorageGroupSnapshotError = false + mock.InducedErrors.GetStorageGroupSnapshotSnapError = false + mock.InducedErrors.GetStorageGroupSnapshotSnapDetailError = false + mock.InducedErrors.GetStorageGroupSnapshotSnapModifyError = false + mock.InducedErrors.GetSnapshotPolicyError = false + mock.InducedErrors.GetSnapshotPolicyListError = false + mock.InducedErrors.CreateSnapshotPolicyError = false + mock.InducedErrors.ModifySnapshotPolicyError = false + mock.InducedErrors.DeleteSnapshotPolicyError = false switch errorType { case "InvalidJSON": mock.InducedErrors.InvalidJSON = true @@ -399,6 +413,8 @@ func (c *unitContext) iInduceError(errorType string) error { mock.InducedErrors.GetRDFGroupError = true case "GetSnapshotPolicyError": mock.InducedErrors.GetSnapshotPolicyError = true + case "CreateSnapshotPolicyError": + mock.InducedErrors.CreateSnapshotPolicyError = true case "GetSnapshotPolicyListError": mock.InducedErrors.GetSnapshotPolicyListError = true case "ModifySnapshotPolicyError": @@ -433,6 +449,10 @@ func (c *unitContext) iInduceError(errorType string) error { mock.InducedErrors.UpdateNFSExportError = true case "DeleteNFSExportError": mock.InducedErrors.DeleteNFSExportError = true + case "GetFileInterfaceError": + mock.InducedErrors.GetFileInterfaceError = true + case "ExecuteActionError": + mock.InducedErrors.ExecuteActionError = true case "none": default: return fmt.Errorf("unknown errorType: %s", errorType) @@ -1644,6 +1664,22 @@ func (c *unitContext) iCallGetStorageGroupSnapshotsWith(storageGroupID string) e return nil } +func (c *unitContext) iCallGetStorageGroupSnapshotsWithAndParam(storageGroupID, params string) error { + var exludeManualSnaps bool + var exludeSlSnaps bool + param := strings.Split(params, ",") + for _, p := range param { + if p == "exludeManualSnaps" { + exludeManualSnaps = true + } + if p == "exludeSlSnaps" { + exludeSlSnaps = true + } + } + c.sgSnapshot, c.err = c.client.GetStorageGroupSnapshots(context.TODO(), symID, storageGroupID, exludeManualSnaps, exludeSlSnaps) + return nil +} + func (c *unitContext) iCallCreateStorageGroupSnapshotWith(storageGroupID string) error { c.storageGroupSnap, c.err = c.client.CreateStorageGroupSnapshot(context.TODO(), symID, storageGroupID, c.storageGroupSnapSetting) return nil @@ -1843,11 +1879,16 @@ func (c *unitContext) iCallUpdateHostName(newName string) error { return nil } -func (c *unitContext) iCallCreateSGReplica() error { +func (c *unitContext) iCallCreateSGReplica(mode string) error { localSG, remoteSG := mock.DefaultStorageGroup, mock.DefaultStorageGroup // Using same names for local and remote storage groups remoteServiceLevel := mock.DefaultServiceLevel // Using the same service level as local - rdfgNumber := fmt.Sprintf("%d", mock.DefaultRDFGNo) - _, c.err = c.client.CreateSGReplica(context.TODO(), symID, mock.DefaultRemoteSymID, srdfMode, rdfgNumber, localSG, remoteSG, remoteServiceLevel, false) + var rdfgNumber string + if mode == "METRO" { + rdfgNumber = fmt.Sprintf("%d", mock.DefaultMetroRDFGNo) + } else { + rdfgNumber = fmt.Sprintf("%d", mock.DefaultAsyncRDFGNo) + } + _, c.err = c.client.CreateSGReplica(context.TODO(), symID, mock.DefaultRemoteSymID, mode, rdfgNumber, localSG, remoteSG, remoteServiceLevel, false) return nil } @@ -1885,11 +1926,11 @@ func (c *unitContext) theVolumesShouldBeReplicated(compliment string) error { func (c *unitContext) iCallGetStorageGroupRDFInfo() error { var sgrdf *types.StorageGroupRDFG - sgrdf, c.err = c.client.GetStorageGroupRDFInfo(context.TODO(), symID, mock.DefaultStorageGroup, fmt.Sprintf("%d", mock.DefaultRemoteRDFGNo)) + sgrdf, c.err = c.client.GetStorageGroupRDFInfo(context.TODO(), symID, mock.DefaultStorageGroup, fmt.Sprintf("%d", mock.DefaultAsyncRemoteRDFGNo)) if c.err == nil { if sgrdf.SymmetrixID != symID || sgrdf.StorageGroupName != mock.DefaultStorageGroup || - sgrdf.RdfGroupNumber != mock.DefaultRDFGNo { + sgrdf.RdfGroupNumber != mock.DefaultAsyncRDFGNo { c.err = fmt.Errorf("the returned storage group doesn't contain proper details") } } @@ -1901,7 +1942,7 @@ func (c *unitContext) iCallGetRDFDevicePairInfo() error { localVolID := c.volIDList[0] remoteVolID := c.volIDList[0] - devicePairInfo, c.err = c.client.GetRDFDevicePairInfo(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultRemoteRDFGNo), localVolID) + devicePairInfo, c.err = c.client.GetRDFDevicePairInfo(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultAsyncRemoteRDFGNo), localVolID) if c.err == nil { if devicePairInfo.LocalSymmID != symID || devicePairInfo.RemoteSymmID != mock.DefaultRemoteSymID || devicePairInfo.LocalVolumeName != localVolID || devicePairInfo.RemoteVolumeName != remoteVolID { @@ -1927,17 +1968,21 @@ func (c *unitContext) iCallGetProtectedStorageGroup() error { func (c *unitContext) iCallGetRDFGroup() error { var rdfGroup *types.RDFGroup - rdfGroup, c.err = c.client.GetRDFGroupByID(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultRemoteRDFGNo)) + rdfGroup, c.err = c.client.GetRDFGroupByID(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultAsyncRemoteRDFGNo)) if c.err == nil { - if !rdfGroup.Async || rdfGroup.RemoteSymmetrix != mock.DefaultRemoteSymID || rdfGroup.RdfgNumber != mock.DefaultRemoteRDFGNo { + if !rdfGroup.Async || rdfGroup.RemoteSymmetrix != mock.DefaultRemoteSymID || rdfGroup.RdfgNumber != mock.DefaultAsyncRemoteRDFGNo { c.err = fmt.Errorf("rdf group with incorrect details returned") } } return nil } -func (c *unitContext) iCallAddVolumesToProtectedStorageGroup() error { - c.err = c.client.AddVolumesToProtectedStorageGroup(context.TODO(), symID, mock.DefaultProtectedStorageGroup, mock.DefaultRemoteSymID, mock.DefaultProtectedStorageGroup, false, c.volIDList...) +func (c *unitContext) iCallAddVolumesToProtectedStorageGroup(mode string) error { + if mode == "ASYNC" { + c.err = c.client.AddVolumesToProtectedStorageGroup(context.TODO(), symID, mock.DefaultASYNCProtectedSG, mock.DefaultRemoteSymID, mock.DefaultASYNCProtectedSG, false, c.volIDList...) + } else { + c.err = c.client.AddVolumesToProtectedStorageGroup(context.TODO(), symID, mock.DefaultMETROProtectedSG, mock.DefaultRemoteSymID, mock.DefaultMETROProtectedSG, false, c.volIDList...) + } return nil } @@ -1945,7 +1990,7 @@ func (c *unitContext) iCallCreateVolumeInProtectedStorageGroupSWithNameAndSize(v volOpts := make(map[string]interface{}) volOpts["capacityUnit"] = "CYL" volOpts["enableMobility"] = "false" - c.vol, c.err = c.client.CreateVolumeInProtectedStorageGroupS(context.TODO(), symID, mock.DefaultRemoteSymID, mock.DefaultProtectedStorageGroup, mock.DefaultProtectedStorageGroup, volumeName, sizeInCylinders, volOpts) + c.vol, c.err = c.client.CreateVolumeInProtectedStorageGroupS(context.TODO(), symID, mock.DefaultRemoteSymID, mock.DefaultASYNCProtectedSG, mock.DefaultASYNCProtectedSG, volumeName, sizeInCylinders, volOpts) return nil } @@ -1954,13 +1999,17 @@ func (c *unitContext) iCallRemoveVolumesFromProtectedStorageGroup() error { return nil } -func (c *unitContext) iCallCreateRDFPair() error { - _, c.err = c.client.CreateRDFPair(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultRDFGNo), c.volIDList[0], ASYNC, "", false, false) +func (c *unitContext) iCallCreateRDFPair(mode string) error { + if mode == "ASYNC" { + _, c.err = c.client.CreateRDFPair(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultAsyncRDFGNo), c.volIDList[0], mode, "", false, false) + } else { + _, c.err = c.client.CreateRDFPair(context.TODO(), symID, fmt.Sprintf("%d", mock.DefaultMetroRDFGNo), c.volIDList[0], mode, "", false, false) + } return nil } func (c *unitContext) iCallExecuteAction(action string) error { - c.err = c.client.ExecuteReplicationActionOnSG(context.TODO(), symID, action, mock.DefaultStorageGroup, fmt.Sprintf("%d", mock.DefaultRDFGNo), false, false, false) + c.err = c.client.ExecuteReplicationActionOnSG(context.TODO(), symID, action, mock.DefaultASYNCProtectedSG, fmt.Sprintf("%d", mock.DefaultAsyncRDFGNo), false, false, true) return nil } @@ -2025,9 +2074,9 @@ func (c *unitContext) iCallExecuteCreateRDFGroup() error { PortWWN: "5000097200007003", }, } - createRDFgPayload.Label = mock.DefaultRDFLabel - createRDFgPayload.LocalRDFNum = mock.DefaultRDFGNo - createRDFgPayload.RemoteRDFNum = mock.DefaultRemoteRDFGNo + createRDFgPayload.Label = mock.DefaultAsyncRDFLabel + createRDFgPayload.LocalRDFNum = mock.DefaultAsyncRDFGNo + createRDFgPayload.RemoteRDFNum = mock.DefaultAsyncRemoteRDFGNo c.err = c.client.ExecuteCreateRDFGroup(context.TODO(), mock.DefaultSymmetrixID, createRDFgPayload) return nil } @@ -2239,6 +2288,24 @@ func (c *unitContext) iCallCreateSnapshotPolicyWith(snapshotPolicyID string) err return nil } +func (c *unitContext) iCallCreateSnapshotPolicyWithAndPayload(snapshotPolicyID, payload string) error { + opPayload := make(map[string]interface{}) + if payload == "cloudSnapshotPolicyDetails" { + opPayload["cloudSnapshotPolicyDetails"] = &types.CloudSnapshotPolicyDetails{ + CloudRetentionDays: 1, + CloudProviderName: "emc", + } + } + if payload == "localSnapshotPolicyDetails" { + opPayload["localSnapshotPolicyDetails"] = &types.LocalSnapshotPolicyDetails{ + Secure: true, + SnapshotCount: 1, + } + } + c.snapshotPolicy, c.err = c.client.CreateSnapshotPolicy(context.TODO(), symID, snapshotPolicyID, "1 Hour", 10, 2, 2, opPayload) + return nil +} + func (c *unitContext) iShouldGetSnapshotPolicytInformationIfNoError() error { if c.err != nil { return nil @@ -2352,6 +2419,12 @@ func (c *unitContext) iCallGetNASServerListWithParam() error { return nil } +func (c *unitContext) iCallGetNFSExportListWithParam() error { + query := types.QueryParams{queryName: "nfs-1"} + c.nfsExportList, c.err = c.client.GetNFSExportList(context.TODO(), symID, query) + return nil +} + func (c *unitContext) iCallGetNFSExportList() error { c.nfsExportList, c.err = c.client.GetNFSExportList(context.TODO(), symID, nil) return nil @@ -2483,6 +2556,20 @@ func (c *unitContext) iGetAValidNFSExportObjectIfNoError() error { return nil } +func (c *unitContext) iCallGetFileInterfaceByID(interfaceID string) error { + c.fileInterface, c.err = c.client.GetFileInterfaceByID(context.TODO(), symID, interfaceID) + return nil +} + +func (c *unitContext) iGetAValidFileInterfaceObjectIfNoError() error { + if c.err == nil { + if c.fileInterface == nil { + return fmt.Errorf("file interface nil") + } + } + return nil +} + func UnitTestContext(s *godog.ScenarioContext) { c := &unitContext{} s.Step(`^I induce error "([^"]*)"$`, c.iInduceError) @@ -2608,6 +2695,7 @@ func UnitTestContext(s *godog.ScenarioContext) { // SG Snapshot s.Step(`^I call GetStorageGroupSnapshots with "([^"]*)"$`, c.iCallGetStorageGroupSnapshotsWith) + s.Step(`^I call GetStorageGroupSnapshots with "([^"]*)" and param "([^"]*)"$`, c.iCallGetStorageGroupSnapshotsWithAndParam) s.Step(`^I should get storage group snapshot information if no error$`, c.iShouldGetStorageGroupSnapshotInformationIfNoError) s.Step(`^I call CreateStorageGroupSnapshot with "([^"]*)"$`, c.iCallCreateStorageGroupSnapshotWith) s.Step(`^I call GetStorageGroupSnapshotSnapIds with "([^"]*)" and "([^"]*)"$`, c.iCallGetStorageGroupSnapshotSnapIdsWithAnd) @@ -2643,19 +2731,19 @@ func UnitTestContext(s *godog.ScenarioContext) { s.Step(`^I recieve (\d+) targets$`, c.iRecieveTargets) s.Step(`^there should be no errors$`, c.thereShouldBeNoErrors) s.Step(`^I call UpdateHostName "([^"]*)"$`, c.iCallUpdateHostName) + // SRDF - s.Step(`^I call CreateSGReplica$`, c.iCallCreateSGReplica) + s.Step(`^I call CreateSGReplica with "([^"]*)"$`, c.iCallCreateSGReplica) s.Step(`^then SG should be replicated$`, c.thenSGShouldBeReplicated) s.Step(`^I call GetStorageGroupRDFInfo$`, c.iCallGetStorageGroupRDFInfo) - s.Step(`^I call GetStorageGroupRDFInfo$`, c.iCallGetStorageGroupRDFInfo) s.Step(`^I call GetRDFDevicePairInfo$`, c.iCallGetRDFDevicePairInfo) s.Step(`^I call GetProtectedStorageGroup$`, c.iCallGetProtectedStorageGroup) s.Step(`^I call GetRDFGroup$`, c.iCallGetRDFGroup) - s.Step(`^I call AddVolumesToProtectedStorageGroup$`, c.iCallAddVolumesToProtectedStorageGroup) + s.Step(`^I call AddVolumesToProtectedStorageGroup with "([^"]*)"$`, c.iCallAddVolumesToProtectedStorageGroup) s.Step(`^I call CreateVolumeInProtectedStorageGroupS with name "([^"]*)" and size (\d+)$`, c.iCallCreateVolumeInProtectedStorageGroupSWithNameAndSize) s.Step(`^the volumes should "([^"]*)" be replicated$`, c.theVolumesShouldBeReplicated) s.Step(`^I call RemoveVolumesFromProtectedStorageGroup$`, c.iCallRemoveVolumesFromProtectedStorageGroup) - s.Step(`^I call CreateRDFPair$`, c.iCallCreateRDFPair) + s.Step(`^I call CreateRDFPair with "([^"]*)"$`, c.iCallCreateRDFPair) s.Step(`^I call ExecuteAction "([^"]*)"$`, c.iCallExecuteAction) // Performance Metrics @@ -2679,6 +2767,7 @@ func UnitTestContext(s *godog.ScenarioContext) { // Snapshot Policy s.Step(`^I call CreateSnapshotPolicy with "([^"]*)"$`, c.iCallCreateSnapshotPolicyWith) + s.Step(`^I call CreateSnapshotPolicy with "([^"]*)" and payload "([^"]*)"$`, c.iCallCreateSnapshotPolicyWithAndPayload) s.Step(`^I call GetSnapshotPolicy with "([^"]*)"$`, c.iCallGetSnapshotPolicyWith) s.Step(`^I should get snapshot policy information if no error$`, c.iShouldGetSnapshotPolicytInformationIfNoError) s.Step(`^I call ModifySnapshotPolicy with "([^"]*)" and "([^"]*)" and "([^"]*)"$`, c.iCallModifySnapshotPolicyWithAndAnd) @@ -2711,4 +2800,7 @@ func UnitTestContext(s *godog.ScenarioContext) { s.Step(`^I get a valid NFSExport object if no error$`, c.iGetAValidNFSExportObjectIfNoError) s.Step(`^I call GetFileSystemListWithParam$`, c.iCallGetFileSystemListWithParam) s.Step(`^I call GetNASServerListWithParam$`, c.iCallGetNASServerListWithParam) + s.Step(`^I call GetNFSExportListWithParam`, c.iCallGetNFSExportListWithParam) + s.Step(`^I call GetFileInterfaceByID "([^"]*)"$`, c.iCallGetFileInterfaceByID) + s.Step(`^I get a valid fileInterface Object if no error$`, c.iGetAValidFileInterfaceObjectIfNoError) } diff --git a/unittest/file.feature b/unittest/file.feature index c7a1221..478908a 100644 --- a/unittest/file.feature +++ b/unittest/file.feature @@ -57,6 +57,13 @@ Feature: PMAX file test | "GetNFSExportListError" | "induced error" | | "InvalidResponse" | "EOF" | + @v2.4.0 + Scenario: TestCases for GetNFSExportList with Param + Given a valid connection + When I call GetNFSExportListWithParam + Then the error message contains "none" + And I get a valid NFS Export ID List if no error + @v2.4.0 Scenario Outline: Test cases for GetFileSystemByID Given a valid connection @@ -94,7 +101,6 @@ Feature: PMAX file test | "fs-1" | "none" | "ignored as it is not managed"| "ignored" | | "fs-1" | "CreateFileSystemError" | "induced error" | "" | - @v2.4.0 Scenario Outline: Test cases for ModifyFileSystem Given a valid connection @@ -257,4 +263,23 @@ Feature: PMAX file test | induced | errormsg | arrays | | "none" | "none" | "" | | "DeleteNFSExportError" | "induced error" | "" | - | "none" | "ignored as it is not managed" | "ignored" | \ No newline at end of file + | "none" | "ignored as it is not managed" | "ignored" | + + @v2.4.0 + Scenario Outline: Test cases for GetFileInterfaceByID + Given a valid connection + And I have an allowed list of + And I induce error + When I call GetFileInterfaceByID + Then the error message contains + And I get a valid fileInterface Object if no error + + Examples: + | id | induced | errormsg | arrays | + | "id1" | "none" | "none" | "" | + | "id3" | "none" | "Could not find" | "" | + | "id1" | "GetFileInterfaceError" | "induced error" | "" | + | "id1" | "InvalidResponse" | "EOF" | "" | + | "id1" | "httpStatus500" | "Internal Error" | "" | + | "id1" | "InvalidJSON" | "invalid character" | "" | + | "id1" | "none" | "ignored as it is not managed"| "ignored" | diff --git a/unittest/pmax.feature b/unittest/pmax.feature index de1d42b..24846fc 100644 --- a/unittest/pmax.feature +++ b/unittest/pmax.feature @@ -88,7 +88,7 @@ Feature: PMAX Client library | id | induced | errormsg | arrays | | "00001" | "none" | "none" | "" | | "00003" | "none" | "none" | "" | - | "00010" | "none" | "cannot be found" | "" | + | "00010" | "none" | "Could not find" | "" | | "00001" | "GetVolumeError" | "induced error" | "" | | "00001" | "httpStatus500" | "Internal Error" | "" | | "00001" | "InvalidJSON" | "invalid character" | "" | @@ -1228,4 +1228,4 @@ Scenario Outline: Test GetHostList Examples: | arrays | induced | errormsg | | "000197900046" | "GetArrayPerfKeyError" | "induced error" | - | "000197900046" | "none" | "none" | \ No newline at end of file + | "000197900046" | "none" | "none" | diff --git a/unittest/pmax_replication.feature b/unittest/pmax_replication.feature index ef26fa2..1c48f4f 100644 --- a/unittest/pmax_replication.feature +++ b/unittest/pmax_replication.feature @@ -52,7 +52,7 @@ Feature: PMAX replication test | "00001,00001" | "snapshot1" | "00001" | "none" | "" | "none" | | "00001,00002,00003" | "snapshot1" | "00002" | "none" | "" | "none" | | "00001" | "snapshot1" | "00002" | "none" | "" | "none" | - | "00001" | "snapshot1" | "00004" | "cannot be found" | "" | "none" | + | "00001" | "snapshot1" | "00004" | "Could not find volume" | "" | "none" | | "00001" | "snapshot1" | "00002" | "ignored as it is not managed" | "ignored" | "none" | | "00001" | "snapshot1" | "00002" | "induced error" | "" | "GetVolSnapsError" | @@ -74,7 +74,7 @@ Feature: PMAX replication test | "00002" | "snapshot1" | "none" | "" | "none" | | "00005" | "snapshot1" | "none" | "" | "none" | | "00003" | "snapshot1" | "none" | "" | "none" | - | "00007" | "snapshot1" | "cannot be found" | "" | "none" | + | "00007" | "snapshot1" | "Could not find volume" | "" | "none" | | "00007" | "snapshot1" | "ignored as it is not managed" | "ignored" | "none" | | "00007" | "snapshot1" | "induced error" | "" | "GetVolSnapsError" | @@ -91,7 +91,7 @@ Feature: PMAX replication test | volIDs | snapID | volID | errormsg | arrays | | "00001,00001" | "snapshot1" | "00001" | "none" | "" | | "00001" | "snapshot1" | "00002" | "none" | "" | - | "00001" | "snapshot1" | "00007" | "cannot be found" | "" | + | "00001" | "snapshot1" | "00007" | "Could not find volume" | "" | | "00001" | "snapshot1" | "00007" | "ignored as it is not managed" | "ignored" | Scenario Outline: Get a Generation Info for given Snapshot @@ -107,7 +107,7 @@ Feature: PMAX replication test | volIDs | snapID | volID | genID | errormsg | arrays | | "00001,00001" | "snapshot1" | "00001" | 1 | "none" | "" | | "00001" | "snapshot1" | "00002" | 0 | "none" | "" | - | "00001" | "snapshot1" | "00007" | 0 | "cannot be found" | "" | + | "00001" | "snapshot1" | "00007" | 0 | "Could not find volume" | "" | | "00001" | "snapshot1" | "00007" | 0 | "ignored as it is not managed" | "ignored" | Scenario Outline: Renaming a snapshot @@ -278,7 +278,7 @@ Feature: PMAX replication test | "00002" | "none" | "" | "none" | | "00003" | "none" | "" | "none" | | "00004" | "none" | "" | "none" | - | "00007" | "cannot be found" | "" | "none" | + | "00007" | "Could not find" | "" | "none" | | "00001" | "ignored as it is not managed" | "ignored" | "none" | | "00001" | "induced error" | "" | "GetPrivVolumeByIDError" | @@ -291,9 +291,22 @@ Feature: PMAX replication test And I should get storage group snapshot information if no error Examples: - | storageGroupID | errormsg | arrays | induced | - | "sg_1" | "none" | "" | "none" | - | "sg_1" | "induced error" | "" | "GetStorageGroupSnapshotError" | + | storageGroupID | errormsg | induced | + | "sg_1" | "none" | "none" | + | "sg_1" | "induced error" | "GetStorageGroupSnapshotError" | + + Scenario Outline: Testing GetStorageGroupSnapshots with params + Given a valid connection + And I call CreateStorageGroupSnapshot with "sg_1" + When I call GetStorageGroupSnapshots with and param + Then the error message contains + And I should get storage group snapshot information if no error + + Examples: + | storageGroupID | errormsg | param | + | "sg_1" | "none" | "exludeManualSnaps" | + | "sg_1" | "none" | "exludeSlSnaps" | + | "sg_1" | "none" | "exludeManualSnaps,exludeSlSnaps" | Scenario Outline: Testing GetStorageGroupSnapshotSnapIds Given a valid connection @@ -304,9 +317,9 @@ Feature: PMAX replication test And I should get storage group snapshot snap ids if no error Examples: - | storageGroupID | snapshotID | errormsg | arrays | induced | - | "sg_1" | "123" | "none" | "" | "none" | - | "sg_1" | "123" | "induced error" | "" | "GetStorageGroupSnapshotSnapError" | + | storageGroupID | snapshotID | errormsg | induced | + | "sg_1" | "123" | "none" | "none" | + | "sg_1" | "123" | "induced error" | "GetStorageGroupSnapshotSnapError" | Scenario Outline: Testing GetStorageGroupSnapshotSnap Given a valid connection @@ -317,9 +330,9 @@ Feature: PMAX replication test And I should get storage group snapshot snap detail information if no error Examples: - | storageGroupID | snapshotID | snapID | errormsg | arrays | induced | - | "sg_1" | "123" | "321" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "induced error" | "" | "GetStorageGroupSnapshotSnapDetailError" | + | storageGroupID | snapshotID | snapID | errormsg | induced | + | "sg_1" | "123" | "321" | "none" | "none" | + | "sg_1" | "123" | "321" | "induced error" | "GetStorageGroupSnapshotSnapDetailError" | Scenario Outline: Testing ModifyStorageGroupSnapshot @@ -331,19 +344,19 @@ Feature: PMAX replication test And I should modify storage group snapshot snap if no error Examples: - | storageGroupID | snapshotID | snapID | action | errormsg | arrays | induced | - | "sg_1" | "123" | "321" | "rename" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "restore" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "link" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "relink" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "unlink" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "setmode" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "timeToLive" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "secure" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "persist" | "none" | "" | "none" | - | "sg_1" | "123" | "321" | "rename" | "induced error" | "" | "GetStorageGroupSnapshotSnapModifyError" | - - Scenario Outline: Testing DeleteStorageGroupSnapshot + | storageGroupID | snapshotID | snapID | action | errormsg | induced | + | "sg_1" | "123" | "321" | "rename" | "none" | "none" | + | "sg_1" | "123" | "321" | "restore" | "none" | "none" | + | "sg_1" | "123" | "321" | "link" | "none" | "none" | + | "sg_1" | "123" | "321" | "relink" | "none" | "none" | + | "sg_1" | "123" | "321" | "unlink" | "none" | "none" | + | "sg_1" | "123" | "321" | "setmode" | "none" | "none" | + | "sg_1" | "123" | "321" | "timeToLive" | "none" | "none" | + | "sg_1" | "123" | "321" | "secure" | "none" | "none" | + | "sg_1" | "123" | "321" | "persist" | "none" | "none" | + | "sg_1" | "123" | "321" | "rename" | "induced error" | "GetStorageGroupSnapshotSnapModifyError" | + + Scenario Outline: Testing DeleteStorageGroupSnapshot Given a valid connection And I call CreateStorageGroupSnapshot with "sg_1" And I induce error @@ -351,10 +364,11 @@ Feature: PMAX replication test Then the error message contains Examples: - | storageGroupID |snapshotID | snapID | errormsg | induced | - | "sg_1" |"snapshot_1" | "snap_1" | "induced error" | "DeleteStorageGroupSnapshotError" | + | storageGroupID |snapshotID | snapID | errormsg | induced | + | "sg_1" |"snapshot_1" | "snap_1" | "induced error" | "DeleteStorageGroupSnapshotError" | + | "sg_1" |"snapshot_1" | "snap_1" | "none" | "none" | - Scenario Outline: Testing GetSnapshotPolicy + Scenario Outline: Testing GetSnapshotPolicy Given a valid connection And I call CreateSnapshotPolicy with "sp_1" And I induce error @@ -363,12 +377,33 @@ Feature: PMAX replication test And I should get snapshot policy information if no error Examples: - | snapshotPolicyID | errormsg | induced | - | "sp_1" | "none" | "none" | - | "sp_1" | "induced error" | "GetSnapshotPolicyError" | + | snapshotPolicyID | errormsg | induced | + | "sp_1" | "none" | "none" | + | "sp_1" | "induced error" | "GetSnapshotPolicyError" | - - Scenario Outline: Testing ModifySnapshotPolicy + Scenario Outline: Testing CreateSnapshotPolicy + Given a valid connection + And I induce error + And I call CreateSnapshotPolicy with "sp_1" + Then the error message contains + And I should get snapshot policy information if no error + + Examples: + | errormsg | induced | + | "none" | "none" | + | "induced error" | "CreateSnapshotPolicyError" | + + Scenario Outline: Testing CreateSnapshotPolicy with payload + Given a valid connection + And I call CreateSnapshotPolicy with "sp_1" and payload + And I should get snapshot policy information if no error + + Examples: + | payload | + | "cloudSnapshotPolicyDetails" | + | "localSnapshotPolicyDetails" | + + Scenario Outline: Testing ModifySnapshotPolicy Given a valid connection And I call CreateSnapshotPolicy with "sp_1" And I induce error @@ -381,7 +416,7 @@ Feature: PMAX replication test | "sp_1" | "Modify" | "none" | "sp_1_updated" | "none" | | "sp_1" | "modify" | "induced error" | "sp_1_updated" | "ModifySnapshotPolicyError" | -Scenario Outline: Testing AddRemoveStorageGrpFromSnapshotPolicy + Scenario Outline: Testing AddRemoveStorageGrpFromSnapshotPolicy Given a valid connection And I call CreateSnapshotPolicy with "sp_1" And I induce error @@ -390,13 +425,13 @@ Scenario Outline: Testing AddRemoveStorageGrpFromSnapshotPolicy And I should modify snapshot policy if no error Examples: - | snapshotPolicyID | action | errormsg | sgName | induced | - | "sp_1" | "AssociateToStorageGroups" | "none" | "sg_1" | "none" | - | "sp_1" | "DisassociateFromStorageGroups" | "none" | "sg_1" | "none" | - | "sp_1" | "AssociateToStorageGroups" | "induced error" | "sg_1" | "ModifySnapshotPolicyError" | - | "sp_1" | "DisassociateFromStorageGroups" | "induced error" | "sg_1" | "ModifySnapshotPolicyError" | + | snapshotPolicyID | action | errormsg | sgName | induced | + | "sp_1" | "AssociateToStorageGroups" | "none" | "sg_1" | "none" | + | "sp_1" | "DisassociateFromStorageGroups" | "none" | "sg_1" | "none" | + | "sp_1" | "AssociateToStorageGroups" | "induced error" | "sg_1" | "ModifySnapshotPolicyError" | + | "sp_1" | "DisassociateFromStorageGroups" | "induced error" | "sg_1" | "ModifySnapshotPolicyError" | -Scenario Outline: Testing Delete Snapshot Policy + Scenario Outline: Testing Delete Snapshot Policy Given a valid connection And I call CreateSnapshotPolicy with "sp_1" And I induce error @@ -404,11 +439,11 @@ Scenario Outline: Testing Delete Snapshot Policy Then the error message contains Examples: - | snapshotPolicyID | errormsg | induced | - | "sp_1" | "none" | "none" | - | "sp_1" | "induced error" | "DeleteSnapshotPolicyError" | + | snapshotPolicyID | errormsg | induced | + | "sp_1" | "none" | "none" | + | "sp_1" | "induced error" | "DeleteSnapshotPolicyError" | - Scenario Outline: Testing GetSnapshotPolicyList + Scenario Outline: Testing GetSnapshotPolicyList Given a valid connection And I induce error When I call GetSnapshotPolicyList @@ -416,6 +451,6 @@ Scenario Outline: Testing Delete Snapshot Policy And I should get list of snapshot policies if no error Examples: - | snapshotPolicyID | errormsg | induced | - | "sp_1" | "none" | "none" | - | "sp_1" | "induced error" | "GetSnapshotPolicyListError" | \ No newline at end of file + | errormsg | induced | + | "none" | "none" | + | "induced error" | "GetSnapshotPolicyListError" | diff --git a/unittest/srdf.feature b/unittest/srdf.feature index fae7094..c38e02f 100644 --- a/unittest/srdf.feature +++ b/unittest/srdf.feature @@ -1,12 +1,12 @@ Feature: PMAX SRDF test @srdf - Scenario Outline: Create a storage-group with volumes and protect it + Scenario Outline: Create a storage-group with volumes and protect it mode: ASYNC Given a valid connection And I have an allowed list of And I induce error And I have 5 volumes - When I call CreateSGReplica + When I call CreateSGReplica with "ASYNC" Then the error message contains And then SG should be replicated And the volumes should "" be replicated @@ -18,13 +18,31 @@ Feature: PMAX SRDF test | "none" | "ignored as it is not managed" | "ignored" | | "httpStatus500" | "Internal Error" | "" | + @srdf + Scenario Outline: Create a storage-group with volumes and protect it mode: METRO + Given a valid connection + And I have an allowed list of + And I induce error + And I have 5 volumes + When I call CreateSGReplica with "METRO" + Then the error message contains + And then SG should be replicated + And the volumes should "" be replicated + + Examples: + | induced | errormsg | arrays | + | "none" | "none" | "" | + | "InvalidJSON" | "invalid character" | "" | + | "none" | "ignored as it is not managed" | "ignored" | + | "httpStatus500" | "Internal Error" | "" | + @srdf Scenario Outline: Get SRDF info about a storage group Given a valid connection And I have an allowed list of And I induce error And I have 5 volumes - When I call CreateSGReplica + When I call CreateSGReplica with "ASYNC" And I call GetStorageGroupRDFInfo Then the error message contains @@ -41,7 +59,7 @@ Feature: PMAX SRDF test And I have an allowed list of And I induce error And I have 1 volumes - When I call CreateSGReplica + When I call CreateSGReplica with "ASYNC" And I call GetRDFDevicePairInfo Then the error message contains @@ -84,12 +102,12 @@ Feature: PMAX SRDF test | "httpStatus500" | "Internal Error" | "" | @srdf - Scenario Outline: Add volumes to protected storage-group + Scenario Outline: Add volumes to protected ASYNC storage-group Given a valid connection And I have an allowed list of And I induce error And I have volumes - When I call AddVolumesToProtectedStorageGroup + When I call AddVolumesToProtectedStorageGroup with "ASYNC" Then the error message contains And the volumes should "" be replicated @@ -100,6 +118,22 @@ Feature: PMAX SRDF test | 5 | "httpStatus500" | "Internal Error" | "" | | 0 | "none" | "at least one volume id has to be specified" | "" | + @srdf + Scenario Outline: Add volumes to protected METRO storage-group + Given a valid connection + And I have an allowed list of + And I induce error + And I have volumes + When I call AddVolumesToProtectedStorageGroup with "METRO" + Then the error message contains + And the volumes should "" be replicated + + Examples: + | vol | induced | errormsg | arrays | + | 5 | "none" | "none" | "" | + | 5 | "none" | "ignored as it is not managed" | "ignored" | + | 5 | "httpStatus500" | "Internal Error" | "" | + | 0 | "none" | "at least one volume id has to be specified" | "" | @srdf Scenario Outline: Test cases for Synchronous CreateVolumeInProtectedStorageGroupS @@ -127,7 +161,7 @@ Feature: PMAX SRDF test And I have an allowed list of And I induce error And I have volumes - And I call CreateSGReplica + And I call CreateSGReplica with "ASYNC" When I call RemoveVolumesFromProtectedStorageGroup Then the error message contains And the volumes should "not" be replicated @@ -146,7 +180,7 @@ Feature: PMAX SRDF test And I have an allowed list of And I induce error And I have 1 volumes - When I call CreateRDFPair + When I call CreateRDFPair with "ASYNC" Then the error message contains Examples: @@ -156,6 +190,22 @@ Feature: PMAX SRDF test | "none" | "ignored as it is not managed" | "ignored" | | "httpStatus500" | "Internal Error" | "" | + @srdf + Scenario Outline: Create an SRDF Pair + Given a valid connection + And I have an allowed list of + And I induce error + And I have 1 volumes + When I call CreateRDFPair with "METRO" + Then the error message contains + + Examples: + | induced | errormsg | arrays | + | "none" | "none" | "" | + | "InvalidJSON" | "invalid character" | "" | + | "none" | "ignored as it is not managed" | "ignored" | + | "httpStatus500" | "Internal Error" | "" | + @srdf Scenario Outline: Execute Action Given a valid connection @@ -166,16 +216,17 @@ Feature: PMAX SRDF test Then the error message contains Examples: - | induced | errormsg | action | arrays | - | "none" | "none" | "Suspend" | "" | - | "none" | "none" | "Resume" | "" | - | "none" | "none" | "Failback" | "" | - | "none" | "none" | "Failover" | "" | - | "none" | "none" | "Establish" | "" | - | "none" | "none" | "Swap" | "" | - | "none" | "not a supported action" | "Dance" | "" | - | "none" | "ignored as it is not managed" | "Suspend" | "ignored" | - | "httpStatus500" | "Internal Error" | "Suspend" | "" | + | induced | errormsg | action | arrays | + | "none" | "none" | "Suspend" | "" | + | "none" | "none" | "Resume" | "" | + | "none" | "none" | "Failback" | "" | + | "none" | "none" | "Failover" | "" | + | "none" | "none" | "Establish" | "" | + | "none" | "none" | "Swap" | "" | + | "none" | "not a supported action" | "Dance" | "" | + | "none" | "ignored as it is not managed" | "Suspend" | "ignored" | + | "httpStatus500" | "Internal Error" | "Suspend" | "" | + | "ExecuteActionError" | "induced error" | "Resume" | "" | @autosrdf Scenario Outline: GetFreeLocalAndRemoteRDFg - Create a SRDF Pair with auto SRDF group creation