Repository: ceph/ceph-csi
Pull request #2523 (2021-09-20 12:30:47)

rbd: detect migrated volhandle in DeleteVolume and delete rbd image
    rbd: detect migration volID in DeleteVolume() and delete rbd image
    
    This commit adds the logic to detect a passed in volumeID
    is a migrated volume ID and if yes, the driver connect to the
    backend cluster and clean/delete the image. The logic
    only applied if its a migration volume ID. The migration volume ID
    carry the information like mons, pool and image name which is
    good enough for the driver to identify and connect to the backend
    cluster for its operations.
    
    migration volID format:
    <mig>_mons-<monsHash>_image-<imageUID>_<poolHash>
    
    Details on the hash values:
    
    * MonsHash: this carry a hash value (md5sum) which will be acted as the
    `clusterID` for the operations in this context.
    
    * ImageUID: this is the unique UUID generated by kubernetes for the created
    volume.
    
    * PoolHash: this is an encoded string of pool name.

Updates #2509 
Signed-off-by: Humble Chirammal <hchiramm@redhat.com>

humblec: > @humblec is this PR tested already?

@Madhu-1 yes, I have tested this delete path and it is working.  

Reg, adding E2E, unfortunately  I dont see a straight way here, that said, the volHandle and Image name has to be manipulated either by following our normal PVC creation with proper SC, or the SC details and other attributes in the PVC/PV spec need to be changed after creation if its static PV workflow, both looks to be difficult or not possible to achieve. 

As our current E2E confirm other code paths are uneffected, may be go ahead without e2e ? 
humblec: @Madhu-1 @Yuggupta27 all comments are addressed except e2e https://github.com/ceph/ceph-csi/pull/2523#issuecomment-923665551. ptal.. Thanks.
 
Madhu-1: > > @humblec is this PR tested already?
> 
> @Madhu-1 yes, I have tested this delete path and it is working.
> 
> Reg, adding E2E, unfortunately I dont see a straight way here, that said, the volHandle and Image name has to be manipulated either by following our normal PVC creation with proper SC, or the SC details and other attributes in the PVC/PV spec need to be changed after creation if its static PV workflow, both looks to be difficult or not possible to achieve.
> 

I  think it can be done easily. Can you make use of the `getStaticPV`  function

* Create static PVC and static PV
* Create an application pod and verify you are able to get the pod running

The above steps are already done in static migration tests

extra steps are below
* when creating the static PV generate the volumeHandle use it
* Change the reclaimPolicy from `Retain` to delete which will trigger rbd volume deletion.
* 

> As our current E2E confirm other code paths are uneffected, may be go ahead without e2e ?


Madhu-1: @humblec as we are adding the monIp to the volumeHandle. In case of the mon changes in the ceph cluster what are the plans of recovery here?
humblec: > @humblec as we are adding the monIp to the volumeHandle. In case of the mon changes in the ceph cluster what are the plans of recovery here?

Admin has to make sure the `mons` are part of the configmap, considering these were part of the `SC` , thats something admin has to take care of. 
Madhu-1: > > @humblec as we are adding the monIp to the volumeHandle. In case of the mon changes in the ceph cluster what are the plans of recovery here?
> 
> Admin has to make sure the `mons` are part of the configmap, considering these were part of the `SC` , thats something admin has to take care of.

Am checking about the mon which is getting added to the volumeHandle. if the mon changes the rbd image cannot be deleted from the csi side.
humblec: > > > @humblec as we are adding the monIp to the volumeHandle. In case of the mon changes in the ceph cluster what are the plans of recovery here?
> > 
> > 
> > Admin has to make sure the `mons` are part of the configmap, considering these were part of the `SC` , thats something admin has to take care of.
> 
> Am checking about the mon which is getting added to the volumeHandle. if the mon changes the rbd image cannot be deleted from the csi side.

I get it, but again, its admins responsibility to make sure the monitors remain till the lifecycle of pvc considering those were part of the SC, iow, if we can not or kubernetes can not delete the volume due to all the monitors mentioned in SC, admin has to do the cleanup manually. more or less, considering its a string slice , if any one  of it remains its good enough or still  work. so an agreed schema and its a best effort on what kube/csi can do.  

ShyamsundarR: > > > > @humblec as we are adding the monIp to the volumeHandle. In case of the mon changes in the ceph cluster what are the plans of recovery here?
> > > 
> > > 
> > > Admin has to make sure the `mons` are part of the configmap, considering these were part of the `SC` , thats something admin has to take care of.
> > 
> > 
> > Am checking about the mon which is getting added to the volumeHandle. if the mon changes the rbd image cannot be deleted from the csi side.
> 
> I get it, but again, its admins responsibility to make sure the monitors remain till the lifecycle of pvc considering those were part of the SC, iow, if we can not or kubernetes can not delete the volume due to all the monitors mentioned in SC are not available, admin has to do the cleanup manually. more or less, considering its a string slice , if any one of it remains, its good enough or still work. so an agreed schema and its a best effort on what kube/csi can do.

This comment caught my curiosity, apologies for late suggestions to the PR.

So if I understand this right, the in-tree migration path has the ability to morph/create a volume handle and invoke CSI related gRPCs. IOW, pass in a volume handle crafted using the existing `volume` parameters.

Why not create a volume handle whose prefix is a hash of volumes.rbd.monitors and add to CSI ceph config this hash as a cluster ID and store new/changing MONs and other information here?

IOW, for in-tree migration to ceph-CSI, user/admin needs to update CSI config with appropriate mapping, and then open up the feature gate.

Possibly this was considered and not viable, but checking nevertheless.
humblec: 
> 
> Why not create a volume handle whose prefix is a hash of volumes.rbd.monitors and add to CSI ceph config this hash as a cluster ID and store new/changing MONs and other information here?
> IOW, for in-tree migration to ceph-CSI, user/admin needs to update CSI config with appropriate mapping, and then open up the feature gate.
> Possibly this was considered and not viable, but checking nevertheless.
> 

Considering `monitors` and `clusterId` are the analogous ones, that was the very first thought. however  at migration time, taking out admin's burden as much as , is the expectation, with that in mind the current  form of vol handle has been considered. Also, all the mons going down is not a really an expected scenario, so this form helps to a very good extent. still I think , we can ask the admin to also have config update with mon hash as clusterID as long as its not that heavy change. Calculating hash ( md5sum) of mons as a clusterID and updating config map should be possible to request to admin before he turn on the migration.  let me try to adjust the PR with this. 




humblec: @ShyamsundarR  Used `monitors` hash (md5sum of monitors) as `clusterID` and additionally  encoded `pool name` in the handle. 

note: e2e is still pending
ShyamsundarR: > @ShyamsundarR Used `monitors` hash (md5sum of monitors) as `clusterID` and additionally encoded `pool name` in the handle. updated unit tests as well with new hash.
> 
> note: e2e is still pending

Just some corner cases to deal with in the migration handle design. Volumes may have varying existing MONs, and hence we may end up with multiple hashed versions of the same as the `clusterID` in the config.
humblec: > > @ShyamsundarR Used `monitors` hash (md5sum of monitors) as `clusterID` and additionally encoded `pool name` in the handle. updated unit tests as well with new hash.
> > note: e2e is still pending
> 
> Just some corner cases to deal with in the migration handle design. Volumes may have varying existing MONs, and hence we may end up with multiple hashed versions of the same as the `clusterID` in the config.

This shouldnt be an issue as this vol handle is applicable only for existing PVs, so before turning on the migration, admin has to make sure there is a clusterID created for each in tree SC based on the `monitors` field. If there is no difference in the monitors field in in tree SCs, he dont want to repeat it. For all existing PVs, the monitors are already recorded  inside pv->spec based on the existing SCs , so this is actually constant and the mapping should work as long as admin has created one for each SC->monitors string. 

humblec: > > @Madhu-1 yes, I have tested this delete path and it is working.
> > Reg, adding E2E, unfortunately I dont see a straight way here, that said, the volHandle and Image name has to be manipulated either by following our normal PVC creation with proper SC, or the SC details and other attributes in the PVC/PV spec need to be changed after creation if its static PV workflow, both looks to be difficult or not possible to achieve.
> 
> I think it can be done easily. Can you make use of the `getStaticPV` function
> 
>     * Create static PVC and static PV
> 
>     * Create an application pod and verify you are able to get the pod running
> 
> 
> The above steps are already done in static migration tests
> 
> extra steps are below
> 
>     * when creating the static PV generate the volumeHandle use it
> 
>     * Change the reclaimPolicy from `Retain` to delete which will trigger rbd volume deletion.

I doubt this will work, the reason being the staticPV functions record a SC ( a dummy one) in the object? 


humblec: > > ```
> > * when creating the static PV generate the volumeHandle use it
> > 
> > * Change the reclaimPolicy from `Retain` to delete which will trigger rbd volume deletion.
> > ```
> 
> I doubt this will work, the reason being the staticPV functions record a SC ( a dummy one) in the object?

As discussed, I can create a SC and then record proper SC in PV spec  and give a try, lets see how that goes. :+1: 

humblec: > > > ```
> > > * when creating the static PV generate the volumeHandle use it
> > > 
> > > * Change the reclaimPolicy from `Retain` to delete which will trigger rbd volume deletion.
> > > ```
> > 
> > 
> > I doubt this will work, the reason being the staticPV functions record a SC ( a dummy one) in the object?
> 
> As discussed, I can create a SC and then record proper SC in PV spec and give a try (eventhough its bit ugly :) ), lets see how that goes. +1

The SC was filled https://github.com/ceph/ceph-csi/pull/2523/commits/77e8009ffaa615f47db2afc6be11348e4599ad4a#diff-fb1cee4ac1e01fbab0746d1a459dc370b11d272949985a858f7ef74f41d47b7cR349) and test deleted pvc and pv, but it does not trigger  the deletecall to the driver as suspected,.

@Madhu-1 please confirm, so that I can remove the last commit which is just e2e and get this refreshed. 

Madhu-1: Yes you also need to change PV ReclaimPolicy to Delete at
https://github.com/ceph/ceph-csi/commit/77e8009ffaa615f47db2afc6be11348e4599ad4a#diff-fb1cee4ac1e01fbab0746d1a459dc370b11d272949985a858f7ef74f41d47b7cR381-R390

On Thu, Sep 23, 2021 at 5:23 PM Humble Devassy Chirammal <
***@***.***> wrote:

> * when creating the static PV generate the volumeHandle use it
>
> * Change the reclaimPolicy from `Retain` to delete which will trigger rbd volume deletion.
>
> I doubt this will work, the reason being the staticPV functions record a
> SC ( a dummy one) in the object?
>
> As discussed, I can create a SC and then record proper SC in PV spec and
> give a try (eventhough its bit ugly :) ), lets see how that goes. +1
>
> The SC was filled 77e8009
> #diff-fb1cee4ac1e01fbab0746d1a459dc370b11d272949985a858f7ef74f41d47b7cR349
> <https://github.com/ceph/ceph-csi/commit/77e8009ffaa615f47db2afc6be11348e4599ad4a#diff-fb1cee4ac1e01fbab0746d1a459dc370b11d272949985a858f7ef74f41d47b7cR349>)
> and test deleted pvc and pv, but it does not trigger the deletecall to the
> driver as suspected,.
>
> @Madhu-1 <https://github.com/Madhu-1> please confirm, so that I can
> remove the last commit which is just e2e and get this refreshed.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/ceph/ceph-csi/pull/2523#issuecomment-925739896>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AEKGNNYNDPV6G4QQLTUU5N3UDMIKBANCNFSM5EL3EILQ>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
>


-- 
Madhu Rajanna
Senior Software Engineer

Red Hat Bangalore, India <https://www.redhat.com>

***@***.***    M: +91-9741133155
<https://red.ht/sig>

humblec: > Yes you also need to change PV ReclaimPolicy to Delete at..

yeah, just noticed the version in CI run was different from the local commit, so refreshed the PR , hopefully it will go through. any way lets see.
humblec: > > Yes you also need to change PV ReclaimPolicy to Delete at..
> 
> yeah, just noticed the version in CI run was different from the local commit, so refreshed the PR , hopefully it will go through. any way lets see.

@Madhu-1 even with the delete policy, the same result as previous run. ptal. once confirmed I will remove the e2e commit. 
humblec: @Madhu-1 @Yuggupta27 all comments are addressed. e2e also added, but looks like that has to be removed, ptal .  thanks. 
humblec: > Please do check out the golangci-lint failure.

@yati1998 both linter and go test failure is  on the e2e commit which is about to get removed (soon) after confirmation from  @Madhu-1 on this.... so kept it that way .
humblec: > @Madhu-1 @Yuggupta27 all comments are addressed. e2e also added, but looks like that has to be removed, ptal . thanks.

Can you please review this ? 
Madhu-1: > > > Yes you also need to change PV ReclaimPolicy to Delete at..
> > 
> > 
> > yeah, just noticed the version in CI run was different from the local commit, so refreshed the PR , hopefully it will go through. any way lets see.
> 
> @Madhu-1 even with the delete policy, the same result as previous run. ptal. once confirmed I will remove the e2e commit.

```
kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
cephfs-pvc-sc   Bound    static-pv                                  1Gi        RWO            rook-cephfs

kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                   STORAGECLASS      REASON   AGE
static-pv                                  1Gi        RWO            Delete           Released   default/cephfs-pvc-sc   rook-cephfs                79s

kubectl delete pvc/cephfs-pvc-sc
persistentvolumeclaim "cephfs-pvc-sc" deleted


1 controller.go:1472] delete "static-pv": started
I0928 06:20:47.179040       1 connection.go:182] GRPC call: /csi.v1.Controller/DeleteVolume
I0928 06:20:47.179372       1 connection.go:183] GRPC request: {"secrets":"***stripped***","volume_id":"static-pv-handle"}
I0928 06:20:47.207441       1 connection.go:185] GRPC response: {}
I0928 06:20:47.207809       1 connection.go:186] GRPC error: rpc error: code = Internal desc = invalid VolumeID: error decoding volume ID (static-pv-handle): failed to decode CSI identifier, string underflow
E0928 06:20:47.208194       1 controller.go:1482] delete "static-pv": volume deletion failed: rpc error: code = Internal desc = invalid VolumeID: error decoding volume ID (static-pv-handle): failed to decode CSI identifier, string underflow
```

In my local testing I can see DeleteVolume Request for a static PV/PVC
humblec: @Madhu-1 Added annotation hack too, hopefully it helps to simulate the situation. lets see.. 
humblec: @Madhu-1 finally E2E is passing.  Thanks :+1:  ..  splitting of the commit is pending,  please take a final look ... will address commit split in next refresh..    
Madhu-1: Please reuse the same logic to add mon hash as clusterid and us the same in nodestage operator and remove the code to get clusterid from mon ip
humblec: > Please reuse the same logic to add mon hash as clusterid and us the same in nodestage operator and remove the code to get clusterid from mon ip

@Madhu-1 I was thinking to address it in a follow up PR, wdyt? 

Madhu-1: > > Please reuse the same logic to add mon hash as clusterid and us the same in nodestage operator and remove the code to get clusterid from mon ip
> 
> @Madhu-1 I was thinking to address it in a follow up PR, wdyt?

Am fine with that please mark it as todo and also update the e2e for same as we previously added in other ip for migration pvc
humblec: @Madhu-1 all the comments from last review have been addressed. ptal . thanks
mergify[bot]: This pull request now has conflicts with the target branch. Could you please resolve conflicts and force push the corrected changes? 🙏
humblec: >if it returns ErrImageNotFound same will be returned from the parseAndDeleteMigratedVolume function and that will be treated as the internalserver error in DeleteVolume?

@Madhu-1  thats what I intended with this version,  is it the suggestion to return `success` if the error was `image not found` ? 
humblec: > Need few changes else LGTM

@Madhu-1 all the comments are addressed. ptal at final version. Thanks :+1: 
humblec: @Mergifyio rebase
mergify[bot]: **Command `rebase`: success**
> **Branch has been successfully rebased**
> 

humblec: /retest  ci/centos/mini-e2e-helm/k8s-1.22
humblec: /retest ci/centos/upgrade-tests-cephfs
humblec: /retest ci/centos/mini-e2e-helm/k8s-1.22
humblec: /retest ci/centos/mini-e2e-helm/k8s-1.22
humblec: /retest ci/centos/mini-e2e/k8s-1.22
humblec: /retest ci/centos/mini-e2e-helm/k8s-1.22
humblec: /retest  ci/centos/mini-e2e/k8s-1.22
humblec: /retest ci/centos/mini-e2e/k8s-1.22

Patch:
diff --git a/internal/rbd/controllerserver.go b/internal/rbd/controllerserver.go
index 40fe2c805..0cc6c2e2e 100644
--- a/internal/rbd/controllerserver.go
+++ b/internal/rbd/controllerserver.go
@@ -833,6 +833,16 @@ func (cs *ControllerServer) DeleteVolume(
 	}
 	defer cs.OperationLocks.ReleaseDeleteLock(volumeID)
 
+	if isMigrationVolID(volumeID) {
+		log.DebugLog(ctx, "migration volume ID : %s", volumeID)
+		err = parseAndDeleteMigratedVolume(ctx, volumeID, cr)
+		if err != nil && !errors.Is(err, ErrImageNotFound) {
+			return nil, status.Error(codes.Internal, err.Error())
+		}
+
+		return &csi.DeleteVolumeResponse{}, nil
+	}
+
 	rbdVol, err := genVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
 	defer rbdVol.Destroy()
 	if err != nil {
diff --git a/internal/rbd/errors.go b/internal/rbd/errors.go
index 15916e15d..2443b8797 100644
--- a/internal/rbd/errors.go
+++ b/internal/rbd/errors.go
@@ -34,4 +34,12 @@ var (
 	ErrMissingStash = errors.New("missing stash")
 	// ErrFlattenInProgress is returned when flatten is in progress for an image.
 	ErrFlattenInProgress = errors.New("flatten in progress")
+	// ErrMissingMonitorsInVolID is returned when monitor information is missing in migration volID.
+	ErrMissingMonitorsInVolID = errors.New("monitor information can not be empty in volID")
+	// ErrMissingPoolNameInVolID is returned when pool information is missing in migration volID.
+	ErrMissingPoolNameInVolID = errors.New("pool information can not be empty in volID")
+	// ErrMissingImageNameInVolID is returned when image name information is missing in migration volID.
+	ErrMissingImageNameInVolID = errors.New("rbd image name information can not be empty in volID")
+	// ErrDecodeClusterIDFromMonsInVolID is returned when mons hash decoding on migration volID.
+	ErrDecodeClusterIDFromMonsInVolID = errors.New("failed to get clusterID from monitors hash in volID")
 )
diff --git a/internal/rbd/migration.go b/internal/rbd/migration.go
new file mode 100644
index 000000000..d76fab28e
--- /dev/null
+++ b/internal/rbd/migration.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2021 The Ceph-CSI Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package rbd
+
+import (
+	"context"
+	"encoding/hex"
+	"strings"
+
+	"github.com/ceph/ceph-csi/internal/util"
+	"github.com/ceph/ceph-csi/internal/util/log"
+)
+
+// isMigrationVolID validates if the passed in volID is a volumeID
+// of a migrated volume.
+func isMigrationVolID(volHash string) bool {
+	return strings.Contains(volHash, migIdentifier) &&
+		strings.Contains(volHash, migImageNamePrefix) && strings.Contains(volHash, migMonPrefix)
+}
+
+// parseMigrationVolID decodes the volume ID and generates a migrationVolID
+// struct which consists of mon, image name, pool and clusterID information.
+func parseMigrationVolID(vh string) (*migrationVolID, error) {
+	mh := &migrationVolID{}
+	handSlice := strings.Split(vh, migVolIDFieldSep)
+	if len(handSlice) < migVolIDTotalLength {
+		// its short of length in this case, so return error
+		return nil, ErrInvalidVolID
+	}
+	// Store pool
+	poolHash := strings.Join(handSlice[migVolIDSplitLength:], migVolIDFieldSep)
+	poolByte, dErr := hex.DecodeString(poolHash)
+	if dErr != nil {
+		return nil, ErrMissingPoolNameInVolID
+	}
+	mh.poolName = string(poolByte)
+	// Parse migration mons( for clusterID) and image
+	for _, field := range handSlice[:migVolIDSplitLength] {
+		switch {
+		case strings.Contains(field, migImageNamePrefix):
+			imageSli := strings.Split(field, migImageNamePrefix)
+			if len(imageSli) > 0 {
+				mh.imageName = migInTreeImagePrefix + imageSli[1]
+			}
+		case strings.Contains(field, migMonPrefix):
+			// ex: mons-7982de6a23b77bce50b1ba9f2e879cce
+			mh.clusterID = strings.Trim(field, migMonPrefix)
+		}
+	}
+	if mh.imageName == "" {
+		return nil, ErrMissingImageNameInVolID
+	}
+	if mh.poolName == "" {
+		return nil, ErrMissingPoolNameInVolID
+	}
+	if mh.clusterID == "" {
+		return nil, ErrDecodeClusterIDFromMonsInVolID
+	}
+
+	return mh, nil
+}
+
+// parseAndDeleteMigratedVolume get rbd volume details from the migration volID
+// and delete the volume from the cluster, return err if there was an error on the process.
+func parseAndDeleteMigratedVolume(ctx context.Context, volumeID string, cr *util.Credentials) error {
+	parsedMigHandle, err := parseMigrationVolID(volumeID)
+	if err != nil {
+		log.ErrorLog(ctx, "failed to parse migration volumeID: %s , err: %v", volumeID, err)
+
+		return err
+	}
+	rv := &rbdVolume{}
+
+	// fill details to rv struct from parsed migration handle
+	rv.RbdImageName = parsedMigHandle.imageName
+	rv.Pool = parsedMigHandle.poolName
+	rv.ClusterID = parsedMigHandle.clusterID
+	rv.Monitors, err = util.Mons(util.CsiConfigFile, rv.ClusterID)
+	if err != nil {
+		log.ErrorLog(ctx, "failed to fetch monitors using clusterID: %s, err: %v", rv.ClusterID, err)
+
+		return err
+	}
+
+	// connect to the volume.
+	err = rv.Connect(cr)
+	if err != nil {
+		log.ErrorLog(ctx, "failed to get connected to the rbd image : %s, err: %v", rv.RbdImageName, err)
+
+		return err
+	}
+	defer rv.Destroy()
+	// if connected , delete it
+	err = deleteImage(ctx, rv, cr)
+	if err != nil {
+		log.ErrorLog(ctx, "failed to delete rbd image : %s, err: %v", rv.RbdImageName, err)
+
+		return err
+	}
+
+	return nil
+}
diff --git a/internal/rbd/nodeserver.go b/internal/rbd/nodeserver.go
index 3640b221c..b90163e54 100644
--- a/internal/rbd/nodeserver.go
+++ b/internal/rbd/nodeserver.go
@@ -150,11 +150,10 @@ func healerStageTransaction(ctx context.Context, cr *util.Credentials, volOps *r
 }
 
 // getClusterIDFromMigrationVolume fills the clusterID for the passed in monitors.
-func getClusterIDFromMigrationVolume(parameters map[string]string) (string, error) {
+func getClusterIDFromMigrationVolume(monitors string) (string, error) {
 	var err error
 	var rclusterID string
-	mons := parameters["monitors"]
-	for _, m := range strings.Split(mons, ",") {
+	for _, m := range strings.Split(monitors, ",") {
 		rclusterID, err = util.GetClusterIDFromMon(m)
 		if err != nil && !errors.Is(err, util.ErrMissingConfigForMonitor) {
 			return "", err
@@ -294,7 +293,7 @@ func (ns *NodeServer) NodeStageVolume(
 	// Check this is a migration request because in that case, unlike other node stage requests
 	// it will be missing the clusterID, so fill it by fetching it from config file using mon.
 	if req.GetVolumeContext()[intreeMigrationKey] == intreeMigrationLabel && req.VolumeContext[util.ClusterIDKey] == "" {
-		cID, cErr := getClusterIDFromMigrationVolume(req.GetVolumeContext())
+		cID, cErr := getClusterIDFromMigrationVolume(req.GetVolumeContext()["monitors"])
 		if cErr != nil {
 			return nil, status.Error(codes.Internal, cErr.Error())
 		}
diff --git a/internal/rbd/rbd_util.go b/internal/rbd/rbd_util.go
index c08c3d4f3..07defa2e1 100644
--- a/internal/rbd/rbd_util.go
+++ b/internal/rbd/rbd_util.go
@@ -74,9 +74,23 @@ const (
 	thickProvisionMetaData = "true"
 	thinProvisionMetaData  = "false"
 
-	// these are the migration label key and value for parameters in volume context.
+	// migration label key and value for parameters in volume context.
 	intreeMigrationKey   = "migration"
 	intreeMigrationLabel = "true"
+	migInTreeImagePrefix = "kubernetes-dynamic-pvc-"
+	// migration volume handle identifiers.
+	// total length of fields in the migration volume handle.
+	migVolIDTotalLength = 4
+	// split boundary length of fields.
+	migVolIDSplitLength = 3
+	// separator for migration handle fields.
+	migVolIDFieldSep = "_"
+	// identifier of a migration vol handle.
+	migIdentifier = "mig"
+	// prefix of image field.
+	migImageNamePrefix = "image-"
+	// prefix in the handle for monitors field.
+	migMonPrefix = "mons-"
 )
 
 // rbdImage contains common attributes and methods for the rbdVolume and
@@ -175,6 +189,14 @@ type imageFeature struct {
 	dependsOn []string
 }
 
+// migrationvolID is a struct which consists of required fields of a rbd volume
+// from migrated volumeID.
+type migrationVolID struct {
+	imageName string
+	poolName  string
+	clusterID string
+}
+
 var supportedFeatures = map[string]imageFeature{
 	librbd.FeatureNameLayering: {
 		needRbdNbd: false,


Test patch:
diff --git a/e2e/cephfs.go b/e2e/cephfs.go
index 499a57490..629be40fe 100644
--- a/e2e/cephfs.go
+++ b/e2e/cephfs.go
@@ -533,12 +533,17 @@ var _ = Describe("cephfs", func() {
 				if err != nil {
 					e2elog.Failf("failed to delete storageclass with error %v", err)
 				}
+
 				// re-define configmap with information of multiple clusters.
-				subvolgrpInfo := map[string]string{
-					"clusterID-1": "subvolgrp1",
-					"clusterID-2": "subvolgrp2",
-				}
-				err = createCustomConfigMap(f.ClientSet, cephFSDirPath, subvolgrpInfo)
+				clusterInfo := map[string]map[string]string{}
+				clusterID1 := "clusterID-1"
+				clusterID2 := "clusterID-2"
+				clusterInfo[clusterID1] = map[string]string{}
+				clusterInfo[clusterID1]["subvolumeGroup"] = "subvolgrp1"
+				clusterInfo[clusterID2] = map[string]string{}
+				clusterInfo[clusterID2]["subvolumeGroup"] = "subvolgrp2"
+
+				err = createCustomConfigMap(f.ClientSet, cephFSDirPath, clusterInfo)
 				if err != nil {
 					e2elog.Failf("failed to create configmap with error %v", err)
 				}
diff --git a/e2e/configmap.go b/e2e/configmap.go
index de1d4b790..b02609b24 100644
--- a/e2e/configmap.go
+++ b/e2e/configmap.go
@@ -76,7 +76,10 @@ func createConfigMap(pluginPath string, c kubernetes.Interface, f *framework.Fra
 }
 
 // createCustomConfigMap provides multiple clusters information.
-func createCustomConfigMap(c kubernetes.Interface, pluginPath string, subvolgrpInfo map[string]string) error {
+func createCustomConfigMap(
+	c kubernetes.Interface,
+	pluginPath string,
+	clusterInfo map[string]map[string]string) error {
 	path := pluginPath + configMap
 	cm := v1.ConfigMap{}
 	err := unmarshal(path, &cm)
@@ -90,22 +93,36 @@ func createCustomConfigMap(c kubernetes.Interface, pluginPath string, subvolgrpI
 	}
 	// get clusterIDs
 	var clusterID []string
-	for key := range subvolgrpInfo {
+	for key := range clusterInfo {
 		clusterID = append(clusterID, key)
 	}
-	conmap := []util.ClusterInfo{
-		{
-			ClusterID: clusterID[0],
-			Monitors:  mons,
-		},
-		{
-			ClusterID: clusterID[1],
-			Monitors:  mons,
-		},
+	conmap := make([]util.ClusterInfo, len(clusterID))
+
+	for i, j := range clusterID {
+		conmap[i].ClusterID = j
+		conmap[i].Monitors = mons
 	}
-	for i := 0; i < len(subvolgrpInfo); i++ {
-		conmap[i].CephFS.SubvolumeGroup = subvolgrpInfo[clusterID[i]]
+
+	// fill radosNamespace and subvolgroups
+	for cluster, confItems := range clusterInfo {
+		for i, j := range confItems {
+			switch i {
+			case "subvolumeGroup":
+				for c := range conmap {
+					if conmap[c].ClusterID == cluster {
+						conmap[c].CephFS.SubvolumeGroup = j
+					}
+				}
+			case "radosNamespace":
+				for c := range conmap {
+					if conmap[c].ClusterID == cluster {
+						conmap[c].RadosNamespace = j
+					}
+				}
+			}
+		}
 	}
+
 	data, err := json.Marshal(conmap)
 	if err != nil {
 		return err
diff --git a/e2e/migration.go b/e2e/migration.go
new file mode 100644
index 000000000..c2bad637e
--- /dev/null
+++ b/e2e/migration.go
@@ -0,0 +1,116 @@
+package e2e
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"strconv"
+	"strings"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/kubernetes/test/e2e/framework"
+)
+
+func validateRBDStaticMigrationPVDeletion(f *framework.Framework, appPath, scName string, isBlock bool) error {
+	opt := make(map[string]string)
+	var (
+		rbdImageName        = "kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412"
+		pvName              = "pv-name"
+		pvcName             = "pvc-name"
+		namespace           = f.UniqueName
+		sc                  = scName
+		provisionerAnnKey   = "pv.kubernetes.io/provisioned-by"
+		provisionerAnnValue = "rbd.csi.ceph.com"
+	)
+
+	c := f.ClientSet
+	PVAnnMap := make(map[string]string)
+	PVAnnMap[provisionerAnnKey] = provisionerAnnValue
+	mons, err := getMons(rookNamespace, c)
+	if err != nil {
+		return fmt.Errorf("failed to get mons: %w", err)
+	}
+	mon := strings.Join(mons, ",")
+	size := staticPVSize
+	// create rbd image
+	cmd := fmt.Sprintf(
+		"rbd create %s --size=%s --image-feature=layering %s",
+		rbdImageName,
+		staticPVSize,
+		rbdOptions(defaultRBDPool))
+
+	_, stdErr, err := execCommandInToolBoxPod(f, cmd, rookNamespace)
+	if err != nil {
+		return err
+	}
+	if stdErr != "" {
+		return fmt.Errorf("failed to create rbd image %s", stdErr)
+	}
+
+	opt["migration"] = "true"
+	opt["monitors"] = mon
+	opt["imageFeatures"] = staticPVImageFeature
+	opt["pool"] = defaultRBDPool
+	opt["staticVolume"] = strconv.FormatBool(true)
+	opt["imageName"] = rbdImageName
+
+	// Make volumeID similar to the migration volumeID
+	volID := composeIntreeMigVolID(mon, rbdImageName)
+	pv := getStaticPV(
+		pvName,
+		volID,
+		size,
+		rbdNodePluginSecretName,
+		cephCSINamespace,
+		sc,
+		provisionerAnnValue,
+		isBlock,
+		opt,
+		PVAnnMap,
+		deletePolicy)
+
+	_, err = c.CoreV1().PersistentVolumes().Create(context.TODO(), pv, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("PV Create API error: %w", err)
+	}
+
+	pvc := getStaticPVC(pvcName, pvName, size, namespace, sc, isBlock)
+
+	_, err = c.CoreV1().PersistentVolumeClaims(pvc.Namespace).Create(context.TODO(), pvc, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("PVC Create API error: %w", err)
+	}
+	// bind pvc to app
+	app, err := loadApp(appPath)
+	if err != nil {
+		return err
+	}
+
+	app.Namespace = namespace
+	app.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = pvcName
+	err = createApp(f.ClientSet, app, deployTimeout)
+	if err != nil {
+		return err
+	}
+
+	err = deletePVCAndApp("", f, pvc, app)
+	if err != nil {
+		return fmt.Errorf("failed to delete PVC and application with error %w", err)
+	}
+
+	return err
+}
+
+// composeIntreeMigVolID create a volID similar to intree migration volID
+// the migration volID format looks like below
+// mig-mons-<hash>-image-<UUID_<poolhash>
+// nolint:lll    // ex: "mig_mons-b7f67366bb43f32e07d8a261a7840da9_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c
+func composeIntreeMigVolID(mons, rbdImageName string) string {
+	poolField := hex.EncodeToString([]byte(defaultRBDPool))
+	monsField := monsPrefix + getMonsHash(mons)
+	imageUID := strings.Split(rbdImageName, intreeVolPrefix)[1:]
+	imageField := imagePrefix + imageUID[0]
+	vhSlice := []string{migIdentifier, monsField, imageField, poolField}
+
+	return strings.Join(vhSlice, "_")
+}
diff --git a/e2e/pod.go b/e2e/pod.go
index 5307781ed..a8242bd6a 100644
--- a/e2e/pod.go
+++ b/e2e/pod.go
@@ -374,6 +374,7 @@ func deletePod(name, ns string, c kubernetes.Interface, t int) error {
 	})
 }
 
+// nolint:unparam // currently skipNotFound is always false, this can change in the future
 func deletePodWithLabel(label, ns string, skipNotFound bool) error {
 	err := retryKubectlArgs(
 		ns,
diff --git a/e2e/rbd.go b/e2e/rbd.go
index ba02a72fc..781dc29af 100644
--- a/e2e/rbd.go
+++ b/e2e/rbd.go
@@ -366,6 +366,47 @@ var _ = Describe("RBD", func() {
 					}
 				})
 			}
+			By("validate RBD migration+static Block PVC Deletion", func() {
+				// create monitors hash by fetching monitors from the cluster.
+				mons, err := getMons(rookNamespace, c)
+				if err != nil {
+					e2elog.Failf("failed to get monitors %v", err)
+				}
+				mon := strings.Join(mons, ",")
+				inClusterID := getMonsHash(mon)
+
+				clusterInfo := map[string]map[string]string{}
+				clusterInfo[inClusterID] = map[string]string{}
+
+				// create custom configmap
+				err = createCustomConfigMap(f.ClientSet, rbdDirPath, clusterInfo)
+				if err != nil {
+					e2elog.Failf("failed to create configmap with error %v", err)
+				}
+				err = createRBDStorageClass(f.ClientSet, f, "migrationsc", nil, nil, deletePolicy)
+				if err != nil {
+					e2elog.Failf("failed to create storageclass with error %v", err)
+				}
+				// restart csi pods for the configmap to take effect.
+				err = recreateCSIRBDPods(f)
+				if err != nil {
+					e2elog.Failf("failed to recreate rbd csi pods with error %v", err)
+				}
+				err = validateRBDStaticMigrationPVDeletion(f, rawAppPath, "migrationsc", true)
+				if err != nil {
+					e2elog.Failf("failed to validate rbd migrated static block pv with error %v", err)
+				}
+				// validate created backend rbd images
+				validateRBDImageCount(f, 0, defaultRBDPool)
+				err = deleteConfigMap(rbdDirPath)
+				if err != nil {
+					e2elog.Failf("failed to delete configmap with error %v", err)
+				}
+				err = createConfigMap(rbdDirPath, f.ClientSet, f)
+				if err != nil {
+					e2elog.Failf("failed to create configmap with error %v", err)
+				}
+			})
 
 			By("create a PVC and validate owner", func() {
 				err := validateImageOwner(pvcPath, f)
diff --git a/e2e/rbd_helper.go b/e2e/rbd_helper.go
index 1ab6eaeec..9a9d1a115 100644
--- a/e2e/rbd_helper.go
+++ b/e2e/rbd_helper.go
@@ -974,3 +974,22 @@ func waitToRemoveImagesFromTrash(f *framework.Framework, poolName string, t int)
 
 	return err
 }
+
+func recreateCSIRBDPods(f *framework.Framework) error {
+	err := deletePodWithLabel("app in (ceph-csi-rbd, csi-rbdplugin, csi-rbdplugin-provisioner)",
+		cephCSINamespace, false)
+	if err != nil {
+		return fmt.Errorf("failed to delete pods with labels with error %w", err)
+	}
+	// wait for csi pods to come up
+	err = waitForDaemonSets(rbdDaemonsetName, cephCSINamespace, f.ClientSet, deployTimeout)
+	if err != nil {
+		return fmt.Errorf("timeout waiting for daemonset pods with error %w", err)
+	}
+	err = waitForDeploymentComplete(rbdDeploymentName, cephCSINamespace, f.ClientSet, deployTimeout)
+	if err != nil {
+		return fmt.Errorf("timeout waiting for deployment to be in running state with error %w", err)
+	}
+
+	return nil
+}
diff --git a/e2e/staticpvc.go b/e2e/staticpvc.go
index a78893a34..0bccae778 100644
--- a/e2e/staticpvc.go
+++ b/e2e/staticpvc.go
@@ -12,16 +12,26 @@ import (
 	"k8s.io/kubernetes/test/e2e/framework"
 )
 
+const (
+	staticPVSize         = "4Gi"
+	staticPVImageFeature = "layering"
+	monsPrefix           = "mons-"
+	imagePrefix          = "image-"
+	migIdentifier        = "mig"
+	intreeVolPrefix      = "kubernetes-dynamic-pvc-"
+)
+
+// nolint:unparam // currently name receive pvName, this can change in the future
 func getStaticPV(
 	name, volName, size, secretName, secretNS, sc, driverName string,
 	blockPV bool,
-	options map[string]string) *v1.PersistentVolume {
+	options, annotations map[string]string, policy v1.PersistentVolumeReclaimPolicy) *v1.PersistentVolume {
 	pv := &v1.PersistentVolume{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: name,
 		},
 		Spec: v1.PersistentVolumeSpec{
-			PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimRetain,
+			PersistentVolumeReclaimPolicy: policy,
 			Capacity: v1.ResourceList{
 				v1.ResourceStorage: resource.MustParse(size),
 			},
@@ -49,10 +59,17 @@ func getStaticPV(
 		volumeMode := v1.PersistentVolumeFilesystem
 		pv.Spec.VolumeMode = &volumeMode
 	}
+	if len(annotations) > 0 {
+		pv.Annotations = make(map[string]string)
+		for k, v := range annotations {
+			pv.Annotations[k] = v
+		}
+	}
 
 	return pv
 }
 
+// nolint:unparam // currently name receive same name, this can change in the future
 func getStaticPVC(name, pvName, size, ns, sc string, blockPVC bool) *v1.PersistentVolumeClaim {
 	pvc := &v1.PersistentVolumeClaim{
 		ObjectMeta: metav1.ObjectMeta{
@@ -104,12 +121,12 @@ func validateRBDStaticPV(f *framework.Framework, appPath string, isBlock, checkI
 	}
 	// remove new line present in fsID
 	fsID = strings.Trim(fsID, "\n")
-	size := "4Gi"
+	size := staticPVSize
 	// create rbd image
 	cmd := fmt.Sprintf(
-		"rbd create %s --size=%d --image-feature=layering %s",
+		"rbd create %s --size=%s --image-feature=layering %s",
 		rbdImageName,
-		4096,
+		staticPVSize,
 		rbdOptions(defaultRBDPool))
 
 	_, e, err = execCommandInToolBoxPod(f, cmd, rookNamespace)
@@ -121,7 +138,7 @@ func validateRBDStaticPV(f *framework.Framework, appPath string, isBlock, checkI
 	}
 	opt["clusterID"] = fsID
 	if !checkImgFeat {
-		opt["imageFeatures"] = "layering"
+		opt["imageFeatures"] = staticPVImageFeature
 	}
 	opt["pool"] = defaultRBDPool
 	opt["staticVolume"] = strconv.FormatBool(true)
@@ -138,7 +155,8 @@ func validateRBDStaticPV(f *framework.Framework, appPath string, isBlock, checkI
 		sc,
 		"rbd.csi.ceph.com",
 		isBlock,
-		opt)
+		opt,
+		nil, retainPolicy)
 
 	_, err = c.CoreV1().PersistentVolumes().Create(context.TODO(), pv, metav1.CreateOptions{})
 	if err != nil {
@@ -207,7 +225,7 @@ func validateRBDStaticMigrationPV(f *framework.Framework, appPath string, isBloc
 		return fmt.Errorf("failed to get mons: %w", err)
 	}
 	mon := strings.Join(mons, ",")
-	size := "4Gi"
+	size := staticPVSize
 	// create rbd image
 	cmd := fmt.Sprintf(
 		"rbd create %s --size=%d --image-feature=layering %s",
@@ -225,7 +243,7 @@ func validateRBDStaticMigrationPV(f *framework.Framework, appPath string, isBloc
 
 	opt["migration"] = "true"
 	opt["monitors"] = mon
-	opt["imageFeatures"] = "layering"
+	opt["imageFeatures"] = staticPVImageFeature
 	opt["pool"] = defaultRBDPool
 	opt["staticVolume"] = strconv.FormatBool(true)
 	opt["imageName"] = rbdImageName
@@ -238,7 +256,7 @@ func validateRBDStaticMigrationPV(f *framework.Framework, appPath string, isBloc
 		sc,
 		"rbd.csi.ceph.com",
 		isBlock,
-		opt)
+		opt, nil, retainPolicy)
 
 	_, err = c.CoreV1().PersistentVolumes().Create(context.TODO(), pv, metav1.CreateOptions{})
 	if err != nil {
@@ -378,7 +396,18 @@ func validateCephFsStaticPV(f *framework.Framework, appPath, scPath string) erro
 	opt["fsName"] = fsName
 	opt["staticVolume"] = strconv.FormatBool(true)
 	opt["rootPath"] = rootPath
-	pv := getStaticPV(pvName, pvName, "4Gi", secretName, cephCSINamespace, sc, "cephfs.csi.ceph.com", false, opt)
+	pv := getStaticPV(
+		pvName,
+		pvName,
+		staticPVSize,
+		secretName,
+		cephCSINamespace,
+		sc,
+		"cephfs.csi.ceph.com",
+		false,
+		opt,
+		nil,
+		retainPolicy)
 	_, err = c.CoreV1().PersistentVolumes().Create(context.TODO(), pv, metav1.CreateOptions{})
 	if err != nil {
 		return fmt.Errorf("failed to create PV: %w", err)
diff --git a/e2e/utils.go b/e2e/utils.go
index 8989ab7dc..b691c1809 100644
--- a/e2e/utils.go
+++ b/e2e/utils.go
@@ -2,6 +2,7 @@ package e2e
 
 import (
 	"context"
+	"crypto/md5" //nolint:gosec // hash generation
 	"encoding/base64"
 	"encoding/json"
 	"errors"
@@ -102,6 +103,10 @@ func getMons(ns string, c kubernetes.Interface) ([]string, error) {
 	return services, nil
 }
 
+func getMonsHash(mons string) string {
+	return fmt.Sprintf("%x", md5.Sum([]byte(mons))) //nolint:gosec // hash generation
+}
+
 func getStorageClass(path string) (scv1.StorageClass, error) {
 	sc := scv1.StorageClass{}
 	err := unmarshal(path, &sc)
diff --git a/internal/rbd/migration_test.go b/internal/rbd/migration_test.go
new file mode 100644
index 000000000..a32e0d294
--- /dev/null
+++ b/internal/rbd/migration_test.go
@@ -0,0 +1,173 @@
+/*
+Copyright 2021 The Ceph-CSI Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package rbd
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestIsMigrationVolID(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name     string
+		args     string
+		migVolID bool
+	}{
+		{
+			"correct volume ID",
+			"mig_mons-b7f67366bb43f32e07d8a261a7840da9_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			true,
+		},
+		{
+			"Wrong volume ID",
+			"wrong_volume_ID",
+			false,
+		},
+		{
+			"wrong mons prefixed volume ID",
+			"mig_mon-b7f67366bb43f32e07d8a261a7840da9_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			false,
+		},
+		{
+			"wrong image prefixed volume ID",
+			"mig_imae-e0b45b52-7e09-47d3-8f1b-806995fa4412_pool_replica_pool",
+			false,
+		},
+		{
+			"wrong volume ID",
+			"mig_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_pool_replica_pool",
+			false,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got := isMigrationVolID(tt.args)
+			if got != tt.migVolID {
+				t.Errorf("isMigrationVolID() = %v, want %v", got, tt.migVolID)
+			}
+		})
+	}
+}
+
+func TestParseMigrationVolID(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name    string
+		args    string
+		want    *migrationVolID
+		wantErr bool
+	}{
+		{
+			"correct volume ID",
+			"mig_mons-b7f67366bb43f32e07d8a261a7840da9_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			&migrationVolID{
+				// monitors:  "10.70.53.126:6789",
+				imageName: "kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412",
+				poolName:  "pool_replica_pool",
+				clusterID: "b7f67366bb43f32e07d8a261a7840da9",
+			},
+			false,
+		},
+		{
+			"volume ID without mons",
+			"mig_kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c",
+			nil,
+			true,
+		},
+		{
+			"volume ID without image",
+			"mig_pool-706f6f6c5f7265706c6963615f706f6f6c",
+			nil,
+			true,
+		},
+		{
+			"volume ID without pool",
+			"mig",
+			nil,
+			true,
+		},
+		{
+			"correct volume ID with single mon",
+			"mig_mons-7982de6a23b77bce50b1ba9f2e879cce_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			&migrationVolID{
+				// monitors:  "10.70.53.126:6789,10.70.53.156:6789",
+				imageName: "kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412",
+				poolName:  "pool_replica_pool",
+				clusterID: "7982de6a23b77bce50b1ba9f2e879cce",
+			},
+			false,
+		},
+		{
+			"correct volume ID with more than one mon",
+			"mig_mons-7982de6a23b77bce50b1ba9f2e879cce_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			&migrationVolID{
+				// monitors:  "10.70.53.126:6789,10.70.53.156:6789",
+				imageName: "kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412",
+				poolName:  "pool_replica_pool",
+				clusterID: "7982de6a23b77bce50b1ba9f2e879cce",
+			},
+			false,
+		},
+		{
+			"correct volume ID with '_' pool name",
+			"mig_mons-7982de6a23b77bce50b1ba9f2e879cce_image-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			&migrationVolID{
+				// monitors:  "10.70.53.126:6789,10.70.53.156:6789",
+				imageName: "kubernetes-dynamic-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412",
+				poolName:  "pool_replica_pool",
+				clusterID: "7982de6a23b77bce50b1ba9f2e879cce",
+			},
+			false,
+		},
+		{
+			"volume ID with unallowed migration version string",
+			"migrate-beta_mons-b7f67366bb43f32e07d8a261a7840da9_kubernetes-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			nil,
+			true,
+		},
+		{
+			"volume ID with unallowed image name",
+			"mig_mons-b7f67366bb43f32e07d8a261a7840da9_kubernetes-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			nil,
+			true,
+		},
+
+		{
+			"volume ID without 'mon-' prefix string",
+			"mig_b7f67366bb43f32e07d8a261a7840da9_kubernetes-pvc-e0b45b52-7e09-47d3-8f1b-806995fa4412_706f6f6c5f7265706c6963615f706f6f6c", //nolint:lll // migration volID
+			nil,
+			true,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := parseMigrationVolID(tt.args)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseMigrationVolID() error = %v, wantErr %v", err, tt.wantErr)
+
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ParseMigrationVolID() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}