Repository: ydb-platform/nbs
Pull request #3425: csi_driver: add volume context attribute to control number of virtio queues
Author: login: ak0rz
Labels: name: blockstore
description: Add this label to run only cloud/blockstore build and tests on PR
name: rebase
description: Add this label if you want to rebase your PR for test run
Created: 2025-04-25T12:51:35Z | Merged: 2025-05-15T08:27:00Z



Diff:
diff --git a/cloud/blockstore/tools/csi_driver/internal/driver/node.go b/cloud/blockstore/tools/csi_driver/internal/driver/node.go
index 66de42c08e0..d3dfd8eea66 100644
--- a/cloud/blockstore/tools/csi_driver/internal/driver/node.go
+++ b/cloud/blockstore/tools/csi_driver/internal/driver/node.go
@@ -47,8 +47,12 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const requestQueuesCountVolumeContextKey = "requestQueuesCount"
 const instanceIdKey = "instanceId"
 
+const defaultVhostQueuesCount = uint32(8)
+const maxQueuesCount = 65536
+
 const volumeOperationInProgress = "Another operation with volume %s is in progress"
 
 var vmModeCapabilities = []*csi.NodeServiceCapability{
@@ -108,6 +112,67 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // additional 1 for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := volumeContext[backendVolumeContextKey] == "nfs"
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			return vhostSettings{}, fmt.Errorf(
+				"failed to parse context attribute %q (=%q): %w",
+				requestQueuesCountVolumeContextKey,
+				requestQueuesCountCtx, err)
+		}
+		if requestQueuesCountParsed < 1 {
+			return vhostSettings{}, fmt.Errorf(
+				"context attribute %q must pass at least 1 request queue (=%d)",
+				requestQueuesCountVolumeContextKey,
+				requestQueuesCountParsed,
+			)
+		}
+
+		queuesCount := uint32(requestQueuesCountParsed)
+		if nfsBackend {
+			queuesCount = virtioFsVhostQueuesCount(queuesCount)
+		} else {
+			queuesCount = virtioBlkVhostQueuesCount(queuesCount)
+		}
+
+		if queuesCount > maxQueuesCount {
+			return vhostSettings{}, fmt.Errorf(
+				"result queues count exceeds limit of %d queues (%q=%d)",
+				maxQueuesCount,
+				requestQueuesCountVolumeContextKey,
+				queuesCount,
+			)
+		}
+
+		settings.queuesCount = queuesCount
+	}
+
+	return settings, nil
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 
 type nodeService struct {
@@ -222,7 +287,10 @@ func (s *nodeService) NodeStageVolume(
 		if s.vmMode {
 			nfsBackend := (req.VolumeContext[backendVolumeContextKey] == "nfs")
 
-			var err error
+			vhostSettings, err := readVhostSettings(req.VolumeContext)
+			if err != nil {
+				return nil, s.statusErrorf(codes.InvalidArgument, "%s", err.Error())
+			}
 			if instanceId := req.VolumeContext[instanceIdKey]; instanceId != "" {
 				nbsId, _ := parseVolumeId(req.VolumeId)
 				stageRecordPath := filepath.Join(req.StagingTargetPath, nbsId+".json")
@@ -241,14 +309,19 @@ func (s *nodeService) NodeStageVolume(
 				}
 
 				if nfsBackend {
-					err = s.nodeStageFileStoreAsVhostSocket(ctx, instanceId, nbsId)
+					err = s.nodeStageFileStoreAsVhostSocket(
+						ctx,
+						instanceId,
+						nbsId,
+						vhostSettings)
 				} else {
 					err = s.nodeStageDiskAsVhostSocket(
 						ctx,
 						instanceId,
 						nbsId,
 						req.VolumeContext,
-						req.VolumeCapability.GetMount())
+						req.VolumeCapability.GetMount(),
+						vhostSettings)
 				}
 
 				if err != nil {
@@ -382,12 +455,16 @@ func (s *nodeService) NodePublishVolume(
 			"ReadWriteMany access mode is supported only with nfs backend")
 	}
 
+	vhostSettings, err := readVhostSettings(req.VolumeContext)
+	if err != nil {
+		return nil, s.statusErrorf(codes.InvalidArgument, "%s", err.Error())
+	}
+
 	if _, opInProgress := s.volumeOps.LoadOrStore(req.VolumeId, nil); opInProgress {
 		return nil, s.statusErrorf(codes.Aborted, volumeOperationInProgress, req.VolumeId)
 	}
 	defer s.volumeOps.Delete(req.VolumeId)
 
-	var err error
 	switch req.VolumeCapability.GetAccessType().(type) {
 	case *csi.VolumeCapability_Mount:
 		if s.vmMode {
@@ -395,12 +472,13 @@ func (s *nodeService) NodePublishVolume(
 				err = s.nodePublishStagedVhostSocket(req, instanceId)
 			} else {
 				if nfsBackend {
-					err = s.nodePublishFileStoreAsVhostSocket(ctx, req)
+					err = s.nodePublishFileStoreAsVhostSocket(ctx, req, vhostSettings)
 				} else {
 					err = s.nodePublishDiskAsVhostSocket(
 						ctx,
 						req,
 						req.VolumeCapability.GetMount(),
+						vhostSettings,
 					)
 				}
 			}
@@ -494,7 +572,8 @@ func (s *nodeService) NodeGetInfo(
 func (s *nodeService) nodePublishDiskAsVhostSocket(
 	ctx context.Context,
 	req *csi.NodePublishVolumeRequest,
-	volumeCapabilities *csi.VolumeCapability_MountVolume) error {
+	volumeCapabilities *csi.VolumeCapability_MountVolume,
+	vhostSettings vhostSettings) error {
 
 	podId := s.getPodId(req)
 	diskId := req.VolumeId
@@ -518,7 +597,7 @@ func (s *nodeService) nodePublishDiskAsVhostSocket(
 		ClientId:         fmt.Sprintf("%s-%s", s.clientId, podId),
 		DeviceName:       deviceName,
 		IpcType:          vhostIpc,
-		VhostQueuesCount: 8,
+		VhostQueuesCount: vhostSettings.queuesCount,
 		VolumeAccessMode: nbsapi.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbsapi.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -592,7 +671,8 @@ func (s *nodeService) nodeStageDiskAsVhostSocket(
 	instanceId string,
 	diskId string,
 	volumeContext map[string]string,
-	volumeCapabilities *csi.VolumeCapability_MountVolume) error {
+	volumeCapabilities *csi.VolumeCapability_MountVolume,
+	vhostSettings vhostSettings) error {
 
 	log.Printf("csi.nodeStageDiskAsVhostSocket: %s %s %+v", instanceId, diskId, volumeContext)
 
@@ -614,7 +694,7 @@ func (s *nodeService) nodeStageDiskAsVhostSocket(
 		ClientId:         fmt.Sprintf("%s-%s", s.clientId, instanceId),
 		DeviceName:       deviceName,
 		IpcType:          vhostIpc,
-		VhostQueuesCount: 8,
+		VhostQueuesCount: vhostSettings.queuesCount,
 		VolumeAccessMode: nbsapi.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbsapi.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -853,7 +933,6 @@ func (s *nodeService) startNbsEndpointForNBD(
 		ClientId:         fmt.Sprintf("%s-%s", s.clientId, nbsInstanceId),
 		DeviceName:       deviceName,
 		IpcType:          nbdIpc,
-		VhostQueuesCount: 8,
 		VolumeAccessMode: nbsapi.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbsapi.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -885,7 +964,8 @@ func (s *nodeService) getNfsClient(fileSystemId string) nfsclient.EndpointClient
 
 func (s *nodeService) nodePublishFileStoreAsVhostSocket(
 	ctx context.Context,
-	req *csi.NodePublishVolumeRequest) error {
+	req *csi.NodePublishVolumeRequest,
+	vhostSettings vhostSettings) error {
 
 	filesystemId := req.VolumeId
 	podId := s.getPodId(req)
@@ -905,7 +985,7 @@ func (s *nodeService) nodePublishFileStoreAsVhostSocket(
 			SocketPath:       filepath.Join(endpointDir, nfsSocketName),
 			FileSystemId:     filesystemId,
 			ClientId:         fmt.Sprintf("%s-%s", s.clientId, podId),
-			VhostQueuesCount: 8,
+			VhostQueuesCount: vhostSettings.queuesCount,
 			Persistent:       true,
 		},
 	})
@@ -930,7 +1010,8 @@ func (s *nodeService) nodeStageFileStoreStartEndpoint(
 	ctx context.Context,
 	instanceId string,
 	filesystemId string,
-	endpointDir string) error {
+	endpointDir string,
+	vhostSettings vhostSettings) error {
 	if s.nfsClient == nil {
 		return fmt.Errorf("NFS client wasn't created")
 	}
@@ -940,7 +1021,7 @@ func (s *nodeService) nodeStageFileStoreStartEndpoint(
 			SocketPath:       filepath.Join(endpointDir, nfsSocketName),
 			FileSystemId:     filesystemId,
 			ClientId:         fmt.Sprintf("%s-%s", s.clientId, instanceId),
-			VhostQueuesCount: 8,
+			VhostQueuesCount: vhostSettings.queuesCount,
 			Persistent:       true,
 		},
 	})
@@ -984,7 +1065,8 @@ func (s *nodeService) nodeStageLocalFileStoreStartEndpoint(
 	ctx context.Context,
 	instanceId string,
 	fsConfig ExternalFsConfig,
-	endpointDir string) error {
+	endpointDir string,
+	vhostSettings vhostSettings) error {
 
 	log.Printf("csi.nodeStageLocalFileStoreStartEndpoint: instanceId=%s, fsConfig=%+v, endpointDir=%s",
 		instanceId,
@@ -1001,7 +1083,7 @@ func (s *nodeService) nodeStageLocalFileStoreStartEndpoint(
 				SocketPath:       filepath.Join(endpointDir, nfsSocketName),
 				FileSystemId:     fsConfig.Id,
 				ClientId:         fmt.Sprintf("%s-%s", s.clientId, instanceId),
-				VhostQueuesCount: 8,
+				VhostQueuesCount: vhostSettings.queuesCount,
 				Persistent:       true,
 			},
 		})
@@ -1075,7 +1157,8 @@ func (s *nodeService) nodeStageLocalFileStoreStartEndpoint(
 func (s *nodeService) nodeStageFileStoreAsVhostSocket(
 	ctx context.Context,
 	instanceId string,
-	filesystemId string) error {
+	filesystemId string,
+	vhostSettings vhostSettings) error {
 
 	log.Printf("csi.nodeStageFileStoreAsVhostSocket: %s %s", instanceId, filesystemId)
 
@@ -1087,9 +1170,9 @@ func (s *nodeService) nodeStageFileStoreAsVhostSocket(
 	var err error
 	externalFsConfig, isExternalFs := s.externalFsOverrides[filesystemId]
 	if isExternalFs {
-		err = s.nodeStageLocalFileStoreStartEndpoint(ctx, instanceId, externalFsConfig, endpointDir)
+		err = s.nodeStageLocalFileStoreStartEndpoint(ctx, instanceId, externalFsConfig, endpointDir, vhostSettings)
 	} else {
-		err = s.nodeStageFileStoreStartEndpoint(ctx, instanceId, filesystemId, endpointDir)
+		err = s.nodeStageFileStoreStartEndpoint(ctx, instanceId, filesystemId, endpointDir, vhostSettings)
 	}
 	if err != nil {
 		return err
diff --git a/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go b/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
index 5e1e0ee3d92..ed5296bbb97 100644
--- a/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
+++ b/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
@@ -12,6 +12,7 @@ import (
 	"os/user"
 	"path/filepath"
 	"slices"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -29,7 +30,14 @@ import (
 
 ////////////////////////////////////////////////////////////////////////////////
 
-func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, isLocalFsOverride bool) {
+func doTestPublishUnpublishVolumeForKubevirtHelper(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	isLocalFsOverride bool,
+	requestQueuesCountOpt *uint32,
+) {
+	t.Helper()
 	tempDir := t.TempDir()
 
 	nbsClient := mocks.NewNbsClientMock()
@@ -111,7 +119,12 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 
 	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
 
+	vhostQueuesCount := uint32(8) // explicit default value for unset behavior
 	if backend == "nbs" {
+		if requestQueuesCountOpt != nil {
+			volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(*requestQueuesCountOpt))
+			vhostQueuesCount = virtioBlkVhostQueuesCount(*requestQueuesCountOpt)
+		}
 		nbsClient.On("DescribeVolume", ctx, &nbs.TDescribeVolumeRequest{
 			DiskId: diskId,
 		}).Return(
@@ -128,7 +141,7 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 			ClientId:         actualClientId,
 			DeviceName:       deviceName,
 			IpcType:          nbs.EClientIpcType_IPC_VHOST,
-			VhostQueuesCount: 8,
+			VhostQueuesCount: vhostQueuesCount,
 			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 			Persistent:       true,
@@ -147,12 +160,16 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	}
 
 	if backend == "nfs" {
+		if requestQueuesCountOpt != nil {
+			volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(*requestQueuesCountOpt))
+			vhostQueuesCount = virtioFsVhostQueuesCount(*requestQueuesCountOpt)
+		}
 		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
 			Endpoint: &nfs.TEndpointConfig{
 				SocketPath:       nfsSocketPath,
 				FileSystemId:     diskId,
 				ClientId:         actualClientId,
-				VhostQueuesCount: 8,
+				VhostQueuesCount: vhostQueuesCount,
 				Persistent:       true,
 			},
 		}).Return(&nfs.TStartEndpointResponse{}, nil)
@@ -211,24 +228,99 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
-func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
-}
-
-func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
-}
-
-func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
-}
+func TestPublishUnpublishVolumeForKubevirt(t *testing.T) {
+	deviceName1 := "test-disk-name-42"
+	rqc32 := uint32(32)
+
+	testCases := []struct {
+		name                  string
+		backend               string
+		deviceNameOpt         *string
+		isLocalFsOverride     bool
+		requestQueuesCountOpt *uint32
+	}{
+		{
+			name:                  "DiskForKubevirt",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceName",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "FilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: &rqc32,
+		},
+	}
 
-func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+	for _, tc := range testCases {
+		tc := tc // Capture range variable.
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel() // Mark test as parallelizable.
+			doTestPublishUnpublishVolumeForKubevirtHelper(
+				t,
+				tc.backend,
+				tc.deviceNameOpt,
+				tc.isLocalFsOverride,
+				tc.requestQueuesCountOpt,
+			)
+		})
+	}
 }
 
-func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, perInstanceVolumes bool, isLocalFsOverride bool) {
+func doTestStagedPublishUnpublishVolumeForKubevirtHelper(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	perInstanceVolumes bool,
+	isLocalFsOverride bool,
+	requestQueuesCountOpt *uint32,
+) {
+	t.Helper()
 	tempDir := t.TempDir()
 
 	nbsClient := mocks.NewNbsClientMock()
@@ -281,7 +373,7 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 		nfsLocalFilestoreClient,
 		mounter,
 		[]string{},
-		false,
+		false, // enableDiscard is false for staged tests
 	)
 
 	accessMode := csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER
@@ -306,8 +398,13 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
 	}
 
+	vhostQueuesCount := uint32(8) // explicit default value for unset behavior
 	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
 	if backend == "nbs" {
+		if requestQueuesCountOpt != nil {
+			volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(*requestQueuesCountOpt))
+			vhostQueuesCount = virtioBlkVhostQueuesCount(*requestQueuesCountOpt)
+		}
 		nbsClient.On("StartEndpoint", ctx, &nbs.TStartEndpointRequest{
 			UnixSocketPath:   nbsSocketPath,
 			DiskId:           diskId,
@@ -315,7 +412,7 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 			ClientId:         actualClientId,
 			DeviceName:       deviceName,
 			IpcType:          nbs.EClientIpcType_IPC_VHOST,
-			VhostQueuesCount: 8,
+			VhostQueuesCount: vhostQueuesCount,
 			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 			Persistent:       true,
@@ -325,6 +422,7 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 			ClientProfile: &nbs.TClientProfile{
 				HostType: &hostType,
 			},
+			// VhostDiscardEnabled is false because enableDiscard for newNodeService is false
 		}).Return(&nbs.TStartEndpointResponse{}, nil)
 	}
 
@@ -334,12 +432,16 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	}
 
 	if backend == "nfs" {
+		if requestQueuesCountOpt != nil {
+			volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(*requestQueuesCountOpt))
+			vhostQueuesCount = virtioFsVhostQueuesCount(*requestQueuesCountOpt)
+		}
 		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
 			Endpoint: &nfs.TEndpointConfig{
 				SocketPath:       nfsSocketPath,
 				FileSystemId:     diskId,
 				ClientId:         actualClientId,
-				VhostQueuesCount: 8,
+				VhostQueuesCount: vhostQueuesCount,
 				Persistent:       true,
 			},
 		}).Return(&nfs.TStartEndpointResponse{}, nil)
@@ -421,38 +523,169 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	assert.True(t, os.IsNotExist(err))
 }
 
-func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
-}
-
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
-}
-
-func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
-}
+func TestStagedPublishUnpublishVolumeForKubevirt(t *testing.T) {
+	deviceName1 := "test-disk-name-42"
+	rqc32 := uint32(32)
+
+	testCases := []struct {
+		name                  string
+		backend               string
+		deviceNameOpt         *string
+		perInstanceVolumes    bool
+		isLocalFsOverride     bool
+		requestQueuesCountOpt *uint32
+	}{
+		// Legacy tests (perInstanceVolumes = false, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirtLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Per-instance volume tests (perInstanceVolumes = true, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirt",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceName",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Multiqueue tests (perInstanceVolumes = false, requestQueuesCountOpt = &rqc32)
+		// These correspond to the original "Multiqueue" tests which were legacy style.
+		{
+			name:                  "DiskForKubevirtMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "FilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: &rqc32,
+		},
+	}
 
-func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	for _, tc := range testCases {
+		tc := tc // Capture range variable.
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel() // Mark test as parallelizable.
+			doTestStagedPublishUnpublishVolumeForKubevirtHelper(
+				t,
+				tc.backend,
+				tc.deviceNameOpt,
+				tc.perInstanceVolumes,
+				tc.isLocalFsOverride,
+				tc.requestQueuesCountOpt,
+			)
+		})
+	}
 }
 
-func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
-}
+func TestReadVhostSettings(t *testing.T) {
+	t.Run("Invalid queues count value", func(t *testing.T) {
+		volumeContext := map[string]string{
+			requestQueuesCountVolumeContextKey: "invalid",
+		}
+		_, err := readVhostSettings(volumeContext)
+		require.ErrorContains(t, err, "failed to parse context attribute")
+	})
 
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
-}
+	t.Run("Negative queues count value", func(t *testing.T) {
+		volumeContext := map[string]string{
+			requestQueuesCountVolumeContextKey: "-1",
+		}
+		_, err := readVhostSettings(volumeContext)
+		require.ErrorContains(t, err, "failed to parse context attribute")
+	})
 
-func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
-}
+	t.Run("Zero queues count value", func(t *testing.T) {
+		volumeContext := map[string]string{
+			requestQueuesCountVolumeContextKey: "0",
+		}
+		_, err := readVhostSettings(volumeContext)
+		require.ErrorContains(t, err, "at least 1")
+	})
 
-func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	t.Run("Valid queues count value", func(t *testing.T) {
+		volumeContext := map[string]string{
+			requestQueuesCountVolumeContextKey: "128",
+		}
+		vhostSettings, err := readVhostSettings(volumeContext)
+		require.NoError(t, err)
+		assert.Equal(t, uint32(128), vhostSettings.queuesCount)
+	})
 }
 
 func TestPublishUnpublishDiskForInfrakuber(t *testing.T) {
@@ -531,7 +764,6 @@ func TestPublishUnpublishDiskForInfrakuber(t *testing.T) {
 		ClientId:         actualClientId,
 		DeviceName:       diskId,
 		IpcType:          ipcType,
-		VhostQueuesCount: 8,
 		VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -684,7 +916,6 @@ func TestPublishUnpublishDeviceForInfrakuber(t *testing.T) {
 			VolumeId:          diskId,
 			StagingTargetPath: stagingTargetPath,
 			VolumeCapability:  &volumeCapability,
-			VolumeContext:     volumeContext,
 		})
 		require.Error(t, err)
 		expectedError := "rpc error: code = Aborted desc = [n=testNodeId]: " +
@@ -700,7 +931,6 @@ func TestPublishUnpublishDeviceForInfrakuber(t *testing.T) {
 		ClientId:         actualClientId,
 		DeviceName:       diskId,
 		IpcType:          ipcType,
-		VhostQueuesCount: 8,
 		VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -1132,8 +1362,6 @@ func TestGrpcTimeoutForInfrakuber(t *testing.T) {
 		},
 	}
 
-	volumeContext := map[string]string{}
-
 	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
 	grpcError := nbsclient.ClientError{Code: nbsclient.E_GRPC_DEADLINE_EXCEEDED}
 	startEndpointError := fmt.Errorf("%w", grpcError)
@@ -1144,7 +1372,6 @@ func TestGrpcTimeoutForInfrakuber(t *testing.T) {
 		ClientId:         actualClientId,
 		DeviceName:       diskId,
 		IpcType:          ipcType,
-		VhostQueuesCount: 8,
 		VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
 		VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
 		Persistent:       true,
@@ -1164,7 +1391,6 @@ func TestGrpcTimeoutForInfrakuber(t *testing.T) {
 		VolumeId:          diskId,
 		StagingTargetPath: stagingTargetPath,
 		VolumeCapability:  &volumeCapability,
-		VolumeContext:     volumeContext,
 	})
 	require.Error(t, err)
 }


Reviews:
user: login: tpashkin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T13:07:23Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:15:08Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:19:03Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:19:56Z
user: login: tpashkin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T13:52:23Z
user: login: tpashkin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T13:55:11Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:18:49Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:19:26Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T20:54:31Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T20:58:33Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T21:00:32Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T21:02:11Z
user: login: antonmyagkov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T09:22:01Z
user: login: budevg
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T12:57:50Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:14:47Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:19:22Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:20:47Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T11:31:41Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T11:32:46Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T11:34:14Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:39:55Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:41:30Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:43:07Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:46:45Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:59:08Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:59:12Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:02:05Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:04:56Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:05:29Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:06:34Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:50:49Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:51:17Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:51:40Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:51:52Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:52:08Z
user: login: antonmyagkov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:43:28Z
user: login: antonmyagkov
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:48:58Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:54:41Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:55:28Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T09:00:22Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T09:04:52Z
user: login: SvartMetal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T22:29:25Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T09:13:34Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:30:46Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:31:09Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:33:07Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:40:10Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:41:53Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:42:49Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:01:50Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:03:29Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:04:18Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:05:39Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:07:11Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:12:53Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T09:00:06Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T09:01:44Z
user: login: jkuradobery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T09:03:58Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:02:42Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:05:29Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:36:02Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:38:49Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T21:21:27Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T21:23:15Z
user: login: antonmyagkov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T08:43:34Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:17:40Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:18:48Z
user: login: ak0rz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:30:33Z
user: login: SvartMetal
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T17:58:05Z
user: login: budevg
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T08:12:31Z

Review comments:
diff_hunk: @@ -485,6 +492,28 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func _rawVirtioQueuesCount(volumeContext map[string]string) uint32 {
+	numRequestQueues := defaultNumRequestQueues
+	if numRequestQueuesCtx, found := volumeContext[numRequestQueuesVolumeContextKey]; found {
+		numRequestQueuesParsed, err := strconv.Atoi(numRequestQueuesCtx)
+		if err == nil && numRequestQueuesParsed > 0 {
+			numRequestQueues = uint32(numRequestQueuesParsed)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: tpashkin
body: What is gonna happen, if we pass a negative number? Let's avoid the problem altogether by using stroul or something
created_at: 2025-04-25T12:59:32Z
author_association: COLLABORATOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const numRequestQueuesVolumeContextKey = "numRequestQueues"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: tpashkin
body: Sorry to be that guy, but abbreviating only 1 word in a whole sentence doesn't seem to be reasonable. Why not just QueuesCount – you are using that form it in some functions already. Seems to be short and descriptive
created_at: 2025-04-25T13:07:17Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +492,28 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func _rawVirtioQueuesCount(volumeContext map[string]string) uint32 {
+	numRequestQueues := defaultNumRequestQueues
+	if numRequestQueuesCtx, found := volumeContext[numRequestQueuesVolumeContextKey]; found {
+		numRequestQueuesParsed, err := strconv.Atoi(numRequestQueuesCtx)
+		if err == nil && numRequestQueuesParsed > 0 {
+			numRequestQueues = uint32(numRequestQueuesParsed)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: `if()` block a line above explicitly avoids that case :)
We'll just won't change default
created_at: 2025-04-25T13:15:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const numRequestQueuesVolumeContextKey = "numRequestQueues"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: It's explicitly *request* queues. For `virtio-blk` they matches 1-1, but for `virtio-fs` it is not that simple, as per virtio spec.

I'd prefer to err on the safe side and explicitly declare that as number of *request* queues.

As for `num` part - well, I thought that this is a well-known abbreviation, but I can change it for `requestQueuesCount` for sure if you say so. It's not yet used anywhere. :)
created_at: 2025-04-25T13:19:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +492,28 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func _rawVirtioQueuesCount(volumeContext map[string]string) uint32 {
+	numRequestQueues := defaultNumRequestQueues
+	if numRequestQueuesCtx, found := volumeContext[numRequestQueuesVolumeContextKey]; found {
+		numRequestQueuesParsed, err := strconv.Atoi(numRequestQueuesCtx)
+		if err == nil && numRequestQueuesParsed > 0 {
+			numRequestQueues = uint32(numRequestQueuesParsed)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Although, probably we might want to fail whole allocation process there. Not sure what's better for resiliency here.
created_at: 2025-04-25T13:19:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +492,28 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func _rawVirtioQueuesCount(volumeContext map[string]string) uint32 {
+	numRequestQueues := defaultNumRequestQueues
+	if numRequestQueuesCtx, found := volumeContext[numRequestQueuesVolumeContextKey]; found {
+		numRequestQueuesParsed, err := strconv.Atoi(numRequestQueuesCtx)
+		if err == nil && numRequestQueuesParsed > 0 {
+			numRequestQueues = uint32(numRequestQueuesParsed)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: tpashkin
body: > if() block a line above explicitly avoids that case :)

Somehow missed it...

> Although, probably we might want to fail whole allocation process there. Not sure what's better for resiliency here.

Let's do that – let the client deal with the bogus request
created_at: 2025-04-25T13:52:23Z
author_association: COLLABORATOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const numRequestQueuesVolumeContextKey = "numRequestQueues"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: tpashkin
body: > I'd prefer to err on the safe side and explicitly declare that as number of request queues

Do we have any other queues, it's just `num-queues` in qemu, isn't it? Might as well be `RequestQueueCount` or even `VirtioRequestQueueCount`
created_at: 2025-04-25T13:55:11Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +492,28 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func _rawVirtioQueuesCount(volumeContext map[string]string) uint32 {
+	numRequestQueues := defaultNumRequestQueues
+	if numRequestQueuesCtx, found := volumeContext[numRequestQueuesVolumeContextKey]; found {
+		numRequestQueuesParsed, err := strconv.Atoi(numRequestQueuesCtx)
+		if err == nil && numRequestQueuesParsed > 0 {
+			numRequestQueues = uint32(numRequestQueuesParsed)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Ok, done. Now we have explicit `volumeContext` validation with meaningful errors!
created_at: 2025-04-25T14:18:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const numRequestQueuesVolumeContextKey = "numRequestQueues"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: No, for `virtio-fs` it's actually `num-request-queues`. :(
created_at: 2025-04-25T14:19:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const requestQueuesCountVolumeContextKey = "requestQueuesCount"
 const instanceIdKey = "instanceId"
 
+const defaultNumRequestQueues = uint32(8)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: defaultRequestQueuesCount
created_at: 2025-04-25T20:54:31Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: _ мы не используем

достаточно именовать с маленькой буквы чтобы обозначить приватность
created_at: 2025-04-25T20:58:33Z
author_association: COLLABORATOR
diff_hunk: @@ -127,7 +133,7 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 			ClientId:         actualClientId,
 			DeviceName:       deviceName,
 			IpcType:          nbs.EClientIpcType_IPC_VHOST,
-			VhostQueuesCount: 8,
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: я хочу видеть число 8 в тестах (для кейсов где дефолтное поведение не меняется)

created_at: 2025-04-25T21:00:32Z
author_association: COLLABORATOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const requestQueuesCountVolumeContextKey = "requestQueuesCount"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: кажется vhostQueuesCount больше подходит по смыслу
created_at: 2025-04-25T21:02:11Z
author_association: COLLABORATOR
diff_hunk: @@ -499,20 +561,25 @@ func (s *nodeService) nodePublishDiskAsVhostSocket(
 		return err
 	}
 
-	deviceName, found := volumeContext[deviceNameVolumeContextKey]
-	if !found {
-		deviceName = diskId
+	volumeCtx, err := validateVolumeContext(volumeContext)
+	if err != nil {
+		return err
+	}
+
+	deviceName := diskId
+	if volumeCtx.deviceName != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: budevg
body: instead of first validating context and creating struct where some of the fields optional and then based on this optional fields assigning default values (diskId, defaultNumRequestQueues)
Do everything in one function which returns struct that can be used in the code later
```go
volumeCtx, err := ParseVolumeCtx(req)
...
_, err = s.nbsClient.StartEndpoint(ctx, &nbsapi.TStartEndpointRequest{
		UnixSocketPath:   filepath.Join(endpointDir, nbsSocketName),
		DiskId:           diskId,
		InstanceId:       podId,
		ClientId:         fmt.Sprintf("%s-%s", s.clientId, podId),
		DeviceName:       volumeCtx.DeviceName(),
		IpcType:          vhostIpc,
		VhostQueuesCount: volumeCtx.VirtioBlkQueuesCount(),

````
created_at: 2025-04-28T08:58:46Z
author_association: COLLABORATOR
diff_hunk: @@ -499,20 +561,25 @@ func (s *nodeService) nodePublishDiskAsVhostSocket(
 		return err
 	}
 
-	deviceName, found := volumeContext[deviceNameVolumeContextKey]
-	if !found {
-		deviceName = diskId
+	volumeCtx, err := validateVolumeContext(volumeContext)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: budevg
body: you have now
```go
volumeContext := req.VolumeContext
...
volumeCtx, err := validateVolumeContext(volumeContext)
```
just pass req and keep consistent naming either `volumeContext` or `volumeCtx`
created_at: 2025-04-28T08:59:38Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := defaultNumRequestQueues
+	if volumeContext.requestQueuesCount != nil {
+		requestQueuesCount = *volumeContext.requestQueuesCount
+	}
+
+	return requestQueuesCount
+}
+
+func virtioBlkQueuesCount(volumeContext volumeContext) uint32 {
+	return _rawRequestQueuesCount(volumeContext)
+}
+
+func virtioFsQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := _rawRequestQueuesCount(volumeContext)
+
+	return requestQueuesCount + 1 // +1 for hiprio queue
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: budevg
body: this isn't backward compatible ... vm without vhostQueueCount in context would run with 8 queues with old driver and with 9 queues with new driver. This potential could break things we want to enable new behavior explicitly and keep the old behavior by default
created_at: 2025-04-28T09:05:16Z
author_association: COLLABORATOR
diff_hunk: @@ -499,20 +561,25 @@ func (s *nodeService) nodePublishDiskAsVhostSocket(
 		return err
 	}
 
-	deviceName, found := volumeContext[deviceNameVolumeContextKey]
-	if !found {
-		deviceName = diskId
+	volumeCtx, err := validateVolumeContext(volumeContext)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: antonmyagkov
body: Please invoke validateVolumeContext in NodeStageVolume/NodePublishVolume
created_at: 2025-04-28T09:22:01Z
author_association: COLLABORATOR
diff_hunk: @@ -44,8 +44,11 @@ const nbdIpc = nbsapi.EClientIpcType_IPC_NBD
 
 const backendVolumeContextKey = "backend"
 const deviceNameVolumeContextKey = "deviceName"
+const requestQueuesCountVolumeContextKey = "requestQueuesCount"
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Нет, у разных устройств в результате будет разное количество очередей. Но у всех (blk, scsi, fs) устройств в спецификации есть request queues, и только их мы и хотим настраивать. https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-4980002
created_at: 2025-04-30T08:14:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := defaultNumRequestQueues
+	if volumeContext.requestQueuesCount != nil {
+		requestQueuesCount = *volumeContext.requestQueuesCount
+	}
+
+	return requestQueuesCount
+}
+
+func virtioBlkQueuesCount(volumeContext volumeContext) uint32 {
+	return _rawRequestQueuesCount(volumeContext)
+}
+
+func virtioFsQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := _rawRequestQueuesCount(volumeContext)
+
+	return requestQueuesCount + 1 // +1 for hiprio queue
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Yeah, I know. Though existing implementation providing 1 less *request* queues which I consider to be a bug. It might produce 2 less if we would implement notification queues.

https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-4980002

As currently all compute VMs actually use only a single request queue - I don't see this as a huge problem. If new compute VM would be set up for more - they would setup new property explicitly.
created_at: 2025-04-30T08:19:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -127,7 +133,7 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 			ClientId:         actualClientId,
 			DeviceName:       deviceName,
 			IpcType:          nbs.EClientIpcType_IPC_VHOST,
-			VhostQueuesCount: 8,
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: А надо ли его сохранять? 8 здесь для файловых систем приведет к 7 или даже 6 request queues в реальности.
created_at: 2025-04-30T08:20:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Суффикс Ctx тут лишний.
created_at: 2025-04-30T11:31:41Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Слово ctx обычно используется в go для context.Context аргумента функций. Использование его в таком контексте мне кажется плохим тоном.
created_at: 2025-04-30T11:32:46Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := defaultNumRequestQueues
+	if volumeContext.requestQueuesCount != nil {
+		requestQueuesCount = *volumeContext.requestQueuesCount
+	}
+
+	return requestQueuesCount
+}
+
+func virtioBlkQueuesCount(volumeContext volumeContext) uint32 {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Это все выглядит как методы структуры volumeContext. Не вижу причины не сделать их методами.
created_at: 2025-04-30T11:34:14Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Зачем это усложнение с errs? errs = append(errs, ...) в двух местах в двух ветвях одного и того же условия. Можно сделать просто return и тогда можно избавиться от if else if else и сделать два плоских if-а c return.  
created_at: 2025-04-30T12:39:55Z
author_association: COLLABORATOR
diff_hunk: @@ -212,23 +218,44 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 }
 
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, nil)
 }
 
 func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, nil)
 }
 
 func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, nil)
 }
 
 func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, nil)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, &requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, &requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, &requestQueuesCount)
 }
 
-func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, perInstanceVolumes bool, isLocalFsOverride bool) {
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, &requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, perInstanceVolumes bool, isLocalFsOverride bool, requestQueuesCountOpt *int) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: Тут наверное есть смысл сделать mutli-line строку.
created_at: 2025-04-30T12:41:30Z
author_association: COLLABORATOR
diff_hunk: @@ -304,6 +331,12 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	if deviceNameOpt != nil {
 		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
 	}
+	if requestQueuesCountOpt != nil {
+		volumeContext[requestQueuesCountVolumeContextKey] = fmt.Sprintf("%d", *requestQueuesCountOpt)
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: Почему не strconv.Itoa?
created_at: 2025-04-30T12:43:07Z
author_association: COLLABORATOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: Может быть есть смысл сделать дедупликацию кода тут (не настаиваю)
created_at: 2025-04-30T12:46:45Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: https://github.com/uber-go/guide/blob/master/style.md#reduce-nesting
created_at: 2025-04-30T12:59:07Z
author_association: COLLABORATOR
diff_hunk: @@ -127,7 +133,7 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 			ClientId:         actualClientId,
 			DeviceName:       deviceName,
 			IpcType:          nbs.EClientIpcType_IPC_VHOST,
-			VhostQueuesCount: 8,
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Разбил тесты на старые и новые
created_at: 2025-04-30T14:59:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: outdated code
created_at: 2025-04-30T15:02:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Вообще в целом для всех валидаторов хорошая практика - собирать все ошибки скопом, а не отплевываться по одной. Это намного упрощает потом отладку API, и пользователю возвращаются сразу все ошибки в параметрах
created_at: 2025-04-30T15:04:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,6 +331,12 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	if deviceNameOpt != nil {
 		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
 	}
+	if requestQueuesCountOpt != nil {
+		volumeContext[requestQueuesCountVolumeContextKey] = fmt.Sprintf("%d", *requestQueuesCountOpt)
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: outdated code
но на самом деле в тестах вроде бы пофиг
created_at: 2025-04-30T15:05:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: в тестах всегда дедупликация vs однозначность

по мне так главное чтобы тесты были :)
created_at: 2025-04-30T15:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,6 +331,12 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	if deviceNameOpt != nil {
 		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
 	}
+	if requestQueuesCountOpt != nil {
+		volumeContext[requestQueuesCountVolumeContextKey] = fmt.Sprintf("%d", *requestQueuesCountOpt)
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: fixed
created_at: 2025-04-30T15:50:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -499,20 +561,25 @@ func (s *nodeService) nodePublishDiskAsVhostSocket(
 		return err
 	}
 
-	deviceName, found := volumeContext[deviceNameVolumeContextKey]
-	if !found {
-		deviceName = diskId
+	volumeCtx, err := validateVolumeContext(volumeContext)
+	if err != nil {
+		return err
+	}
+
+	deviceName := diskId
+	if volumeCtx.deviceName != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: fixed
created_at: 2025-04-30T15:51:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := defaultNumRequestQueues
+	if volumeContext.requestQueuesCount != nil {
+		requestQueuesCount = *volumeContext.requestQueuesCount
+	}
+
+	return requestQueuesCount
+}
+
+func virtioBlkQueuesCount(volumeContext volumeContext) uint32 {
+	return _rawRequestQueuesCount(volumeContext)
+}
+
+func virtioFsQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := _rawRequestQueuesCount(volumeContext)
+
+	return requestQueuesCount + 1 // +1 for hiprio queue
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: fixed too
created_at: 2025-04-30T15:51:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			ctx.requestQueuesCount = &requestQueuesCount
+		}
+	}
+
+	if deviceNameCtx, found := contextMap[deviceNameVolumeContextKey]; found {
+		ctx.deviceName = &deviceNameCtx
+	}
+
+	if len(errs) > 0 {
+		return ctx, fmt.Errorf("failed to validate volume context: %v", errors.Join(errs...))
+	}
+
+	return ctx, nil
+}
+
+func _rawRequestQueuesCount(volumeContext volumeContext) uint32 {
+	requestQueuesCount := defaultNumRequestQueues
+	if volumeContext.requestQueuesCount != nil {
+		requestQueuesCount = *volumeContext.requestQueuesCount
+	}
+
+	return requestQueuesCount
+}
+
+func virtioBlkQueuesCount(volumeContext volumeContext) uint32 {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: removed struct
created_at: 2025-04-30T15:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -212,23 +218,44 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 }
 
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, nil)
 }
 
 func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, nil)
 }
 
 func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, nil)
 }
 
 func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, nil)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, &requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, &requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, &requestQueuesCount)
 }
 
-func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, perInstanceVolumes bool, isLocalFsOverride bool) {
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, &requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, deviceNameOpt *string, perInstanceVolumes bool, isLocalFsOverride bool, requestQueuesCountOpt *int) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: done
created_at: 2025-04-30T15:52:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: antonmyagkov
body: @jkuradobery Я тут пытался сделать дедупликацию кода в тестах https://github.com/ydb-platform/nbs/pull/2804 Но не уверен что стало лучше. Если видишь улучшения в этом то могу допинать этот пр.
created_at: 2025-04-30T18:43:28Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Да, ты прав, но не в этом случае. У тебя либо одна ошибка, либо другая. 
created_at: 2025-04-30T18:54:41Z
author_association: COLLABORATOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: Мне в целом ок. Если нужно оперативно влить на текущей итерации выглядит ~ок.
created_at: 2025-04-30T18:55:28Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Скобки лишние. 
created_at: 2025-05-01T09:00:22Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: Короче я бы сделал максимально просто. Надо будет что-то более сложное валидировать - можно сделать по-другому. 
created_at: 2025-05-01T09:04:51Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: // additional 1 for hiprio queue
created_at: 2025-05-06T22:00:33Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: давай отформатируем эту длинную строчку

например так:
```
errs = append(
	errs,
	fmt.Errorf(
		"failed to parse context attribute %q (=%q): %w",
		requestQueuesCountVolumeContextKey,
		requestQueuesCountCtx,
		err
	),
)
```

created_at: 2025-05-06T22:02:44Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: и эту
created_at: 2025-05-06T22:03:19Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			if nfsBackend {
+				settings.queuesCount = virtioFsVhostQueuesCount(requestQueuesCount)
+			} else {
+				settings.queuesCount = virtioBlkVhostQueuesCount(requestQueuesCount)
+			}
+		}
+	} else {
+		settings.queuesCount = defaultVhostQueuesCount
+	}
+
+	if len(errs) > 0 {
+		return settings, errors.Join(errs...)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: в случае ошибки возвращаем дефолтное значение и ошибку

`return vhostSettings{}, errors.Join(errs...)`
created_at: 2025-05-06T22:05:26Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: что-то я не понял зачем этот массив ошибок

может просто выйдем при первой же попавшейся ошибке и вернём её же?
created_at: 2025-05-06T22:06:54Z
author_association: COLLABORATOR
diff_hunk: @@ -235,14 +291,20 @@ func (s *nodeService) NodeStageVolume(
 				}
 
 				if nfsBackend {
-					err = s.nodeStageFileStoreAsVhostSocket(ctx, instanceId, nbsId)
+					err = s.nodeStageFileStoreAsVhostSocket(
+						ctx,
+						instanceId,
+						nbsId,
+						vhostSettings)
 				} else {
 					err = s.nodeStageDiskAsVhostSocket(
 						ctx,
 						instanceId,
 						nbsId,
 						req.VolumeContext,
-						req.VolumeCapability.GetMount())
+						req.VolumeCapability.GetMount(),
+
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: лишний отступ
created_at: 2025-05-06T22:12:14Z
author_association: COLLABORATOR
diff_hunk: @@ -256,15 +318,16 @@ func (s *nodeService) NodeStageVolume(
 				return nil, s.statusError(codes.InvalidArgument,
 					"NFS mounts are only supported in VM mode")
 			} else {
-				err = s.nodeStageDiskAsFilesystem(ctx, req)
+				err = s.nodeStageDiskAsFilesystem(ctx, req, vhostSettings)
 			}
 		}
 	case *csi.VolumeCapability_Block:
 		if nfsBackend {
 			return nil, s.statusError(codes.InvalidArgument,
 				"'Block' volume mode is not supported with nfs backend")
 		} else {
-			err = s.nodeStageDiskAsBlockDevice(ctx, req)
+
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: лишний отступ
created_at: 2025-05-06T22:12:28Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: doTestPublishUnpublishVolumeForKubevirtWithReques**t**QueuesCount
created_at: 2025-05-06T22:18:11Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	isLocalFsOverride bool,
+	requestQueuesCount uint32,
+) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	nfsClient := mocks.NewNfsEndpointClientMock()
+	nfsLocalClient := mocks.NewNfsEndpointClientMock()
+	mounter := csimounter.NewMock()
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	podId := "test-pod-id-13"
+	actualClientId := "testClientId-test-pod-id-13"
+	diskId := "test-disk-id-42"
+	deviceName := diskId
+	if deviceNameOpt != nil {
+		deviceName = *deviceNameOpt
+	}
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+	sourcePath := filepath.Join(socketsDir, podId, diskId)
+	targetPath := filepath.Join(tempDir, "pods", podId, "volumes", diskId, "mount")
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	nbsSocketPath := filepath.Join(sourcePath, "nbs.sock")
+	nfsSocketPath := filepath.Join(sourcePath, "nfs.sock")
+
+	localFsOverrides := make(LocalFilestoreOverrideMap)
+	if isLocalFsOverride {
+		localFsOverrides[diskId] = LocalFilestoreOverride{
+			FsId:           diskId,
+			LocalMountPath: "/tmp/mnt/local_mount",
+		}
+	}
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		true, // vmMode
+		socketsDir,
+		targetFsPathPattern,
+		"", // targetBlkPathPattern
+		localFsOverrides,
+		nbsClient,
+		nfsClient,
+		nfsLocalClient,
+		mounter,
+		[]string{},
+		true, // enable discard
+	)
+
+	accessMode := csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER
+	if backend == "nfs" {
+		accessMode = csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER
+	}
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{
+			Mount: &csi.VolumeCapability_MountVolume{},
+		},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: accessMode,
+		},
+	}
+
+	volumeContext := map[string]string{
+		backendVolumeContextKey: backend,
+	}
+	if deviceNameOpt != nil {
+		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
+	}
+	volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(requestQueuesCount))
+
+	_, err := nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
+
+	if backend == "nbs" {
+		nbsClient.On("DescribeVolume", ctx, &nbs.TDescribeVolumeRequest{
+			DiskId: diskId,
+		}).Return(
+			&nbs.TDescribeVolumeResponse{
+				Volume: &nbs.TVolume{
+					StorageMediaKind: storagecoreapi.EStorageMediaKind_STORAGE_MEDIA_SSD,
+				},
+			},
+			nil)
+		nbsClient.On("StartEndpoint", ctx, &nbs.TStartEndpointRequest{
+			UnixSocketPath:   nbsSocketPath,
+			DiskId:           diskId,
+			InstanceId:       podId,
+			ClientId:         actualClientId,
+			DeviceName:       deviceName,
+			IpcType:          nbs.EClientIpcType_IPC_VHOST,
+			VhostQueuesCount: virtioBlkVhostQueuesCount(requestQueuesCount),
+			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
+			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
+			Persistent:       true,
+			NbdDevice: &nbs.TStartEndpointRequest_UseFreeNbdDeviceFile{
+				false,
+			},
+			ClientProfile: &nbs.TClientProfile{
+				HostType: &hostType,
+			},
+			VhostDiscardEnabled: true,
+		}).Return(&nbs.TStartEndpointResponse{}, nil)
+	}
+
+	expectedNfsClient := nfsClient
+	if isLocalFsOverride {
+		expectedNfsClient = nfsLocalClient
+	}
+
+	if backend == "nfs" {
+		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
+			Endpoint: &nfs.TEndpointConfig{
+				SocketPath:       nfsSocketPath,
+				FileSystemId:     diskId,
+				ClientId:         actualClientId,
+				VhostQueuesCount: virtioFsVhostQueuesCount(requestQueuesCount),
+				Persistent:       true,
+			},
+		}).Return(&nfs.TStartEndpointResponse{}, nil)
+	}
+
+	mounter.On("IsMountPoint", targetPath).Return(false, nil)
+	mounter.On("Mount", sourcePath, targetPath, "", []string{"bind"}).Return(nil)
+
+	_, err = nodeService.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		TargetPath:        targetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	fileInfo, err := os.Stat(sourcePath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0755), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(filepath.Join(sourcePath, "disk.img"))
+	assert.False(t, os.IsNotExist(err))
+	assert.False(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0644), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(targetPath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0775), fileInfo.Mode().Perm())
+
+	mounter.On("CleanupMountPoint", targetPath).Return(nil)
+
+	nbsClient.On("StopEndpoint", ctx, &nbs.TStopEndpointRequest{
+		UnixSocketPath: nbsSocketPath,
+	}).Return(&nbs.TStopEndpointResponse{}, nil)
+
+	expectedNfsClient.On("StopEndpoint", ctx, &nfs.TStopEndpointRequest{
+		SocketPath: nfsSocketPath,
+	}).Return(&nfs.TStopEndpointResponse{}, nil)
+
+	_, err = nodeService.NodeUnpublishVolume(ctx, &csi.NodeUnpublishVolumeRequest{
+		VolumeId:   diskId,
+		TargetPath: targetPath,
+	})
+	require.NoError(t, err)
+
+	_, err = os.Stat(filepath.Join(socketsDir, podId))
+	assert.True(t, os.IsNotExist(err))
+
+	_, err = nodeService.NodeUnstageVolume(ctx, &csi.NodeUnstageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+	})
+	require.NoError(t, err)
+}
+
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
 	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
 }
 
-func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
-}
+func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+}
+
+func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", &deviceName, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, true, requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: это случаем не копипаста уже существовавшего теста ]doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount`?

лучше переиспользовать тело теста, добавив параметр
created_at: 2025-05-06T22:22:59Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: возникает ощущение что это копипаста
created_at: 2025-05-06T22:23:44Z
author_association: COLLABORATOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: а мне не ОК, не надо копипастить код, тут можно легко обобщить код
created_at: 2025-05-06T22:25:38Z
author_association: COLLABORATOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: продублирую тут тоже

нужно избавиться от копипасты при проверке сценариев ошибки
created_at: 2025-05-06T22:27:11Z
author_association: COLLABORATOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: Давайте может сделаем t.Run()/t.Parallel в цикле с параметрами? Один тест, разные ошибки.
created_at: 2025-05-07T09:13:34Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Так ты определись - ты то 8 хочешь видеть явно, то параметризацию :)
created_at: 2025-05-07T11:30:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			if nfsBackend {
+				settings.queuesCount = virtioFsVhostQueuesCount(requestQueuesCount)
+			} else {
+				settings.queuesCount = virtioBlkVhostQueuesCount(requestQueuesCount)
+			}
+		}
+	} else {
+		settings.queuesCount = defaultVhostQueuesCount
+	}
+
+	if len(errs) > 0 {
+		return settings, errors.Join(errs...)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: И что в этом не так?
created_at: 2025-05-07T11:31:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Вообще он нужен именно для того чтобы собрать все ошибки. Паттерн валидатор.

Но прямо сейчас параметр один, поэтому убрал.

created_at: 2025-05-07T11:33:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	isLocalFsOverride bool,
+	requestQueuesCount uint32,
+) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	nfsClient := mocks.NewNfsEndpointClientMock()
+	nfsLocalClient := mocks.NewNfsEndpointClientMock()
+	mounter := csimounter.NewMock()
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	podId := "test-pod-id-13"
+	actualClientId := "testClientId-test-pod-id-13"
+	diskId := "test-disk-id-42"
+	deviceName := diskId
+	if deviceNameOpt != nil {
+		deviceName = *deviceNameOpt
+	}
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+	sourcePath := filepath.Join(socketsDir, podId, diskId)
+	targetPath := filepath.Join(tempDir, "pods", podId, "volumes", diskId, "mount")
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	nbsSocketPath := filepath.Join(sourcePath, "nbs.sock")
+	nfsSocketPath := filepath.Join(sourcePath, "nfs.sock")
+
+	localFsOverrides := make(LocalFilestoreOverrideMap)
+	if isLocalFsOverride {
+		localFsOverrides[diskId] = LocalFilestoreOverride{
+			FsId:           diskId,
+			LocalMountPath: "/tmp/mnt/local_mount",
+		}
+	}
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		true, // vmMode
+		socketsDir,
+		targetFsPathPattern,
+		"", // targetBlkPathPattern
+		localFsOverrides,
+		nbsClient,
+		nfsClient,
+		nfsLocalClient,
+		mounter,
+		[]string{},
+		true, // enable discard
+	)
+
+	accessMode := csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER
+	if backend == "nfs" {
+		accessMode = csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER
+	}
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{
+			Mount: &csi.VolumeCapability_MountVolume{},
+		},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: accessMode,
+		},
+	}
+
+	volumeContext := map[string]string{
+		backendVolumeContextKey: backend,
+	}
+	if deviceNameOpt != nil {
+		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
+	}
+	volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(requestQueuesCount))
+
+	_, err := nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
+
+	if backend == "nbs" {
+		nbsClient.On("DescribeVolume", ctx, &nbs.TDescribeVolumeRequest{
+			DiskId: diskId,
+		}).Return(
+			&nbs.TDescribeVolumeResponse{
+				Volume: &nbs.TVolume{
+					StorageMediaKind: storagecoreapi.EStorageMediaKind_STORAGE_MEDIA_SSD,
+				},
+			},
+			nil)
+		nbsClient.On("StartEndpoint", ctx, &nbs.TStartEndpointRequest{
+			UnixSocketPath:   nbsSocketPath,
+			DiskId:           diskId,
+			InstanceId:       podId,
+			ClientId:         actualClientId,
+			DeviceName:       deviceName,
+			IpcType:          nbs.EClientIpcType_IPC_VHOST,
+			VhostQueuesCount: virtioBlkVhostQueuesCount(requestQueuesCount),
+			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
+			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
+			Persistent:       true,
+			NbdDevice: &nbs.TStartEndpointRequest_UseFreeNbdDeviceFile{
+				false,
+			},
+			ClientProfile: &nbs.TClientProfile{
+				HostType: &hostType,
+			},
+			VhostDiscardEnabled: true,
+		}).Return(&nbs.TStartEndpointResponse{}, nil)
+	}
+
+	expectedNfsClient := nfsClient
+	if isLocalFsOverride {
+		expectedNfsClient = nfsLocalClient
+	}
+
+	if backend == "nfs" {
+		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
+			Endpoint: &nfs.TEndpointConfig{
+				SocketPath:       nfsSocketPath,
+				FileSystemId:     diskId,
+				ClientId:         actualClientId,
+				VhostQueuesCount: virtioFsVhostQueuesCount(requestQueuesCount),
+				Persistent:       true,
+			},
+		}).Return(&nfs.TStartEndpointResponse{}, nil)
+	}
+
+	mounter.On("IsMountPoint", targetPath).Return(false, nil)
+	mounter.On("Mount", sourcePath, targetPath, "", []string{"bind"}).Return(nil)
+
+	_, err = nodeService.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		TargetPath:        targetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	fileInfo, err := os.Stat(sourcePath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0755), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(filepath.Join(sourcePath, "disk.img"))
+	assert.False(t, os.IsNotExist(err))
+	assert.False(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0644), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(targetPath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0775), fileInfo.Mode().Perm())
+
+	mounter.On("CleanupMountPoint", targetPath).Return(nil)
+
+	nbsClient.On("StopEndpoint", ctx, &nbs.TStopEndpointRequest{
+		UnixSocketPath: nbsSocketPath,
+	}).Return(&nbs.TStopEndpointResponse{}, nil)
+
+	expectedNfsClient.On("StopEndpoint", ctx, &nfs.TStopEndpointRequest{
+		SocketPath: nfsSocketPath,
+	}).Return(&nfs.TStopEndpointResponse{}, nil)
+
+	_, err = nodeService.NodeUnpublishVolume(ctx, &csi.NodeUnpublishVolumeRequest{
+		VolumeId:   diskId,
+		TargetPath: targetPath,
+	})
+	require.NoError(t, err)
+
+	_, err = os.Stat(filepath.Join(socketsDir, podId))
+	assert.True(t, os.IsNotExist(err))
+
+	_, err = nodeService.NodeUnstageVolume(ctx, &csi.NodeUnstageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+	})
+	require.NoError(t, err)
+}
+
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
 	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
 }
 
-func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
-}
+func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+}
+
+func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", &deviceName, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, true, requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Ты хотел чтобы в моке оставалось ровно 8.
Я предлагаю рефакторинг тестов делать отдельно от введения новой логики

created_at: 2025-05-07T11:40:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Он был обобщен в предыдущих чейнджах.

Так-то вообще было бы неплохо тесты писать декларативно, но давайте рефакторинг тестов делать отдельно от новой функциональности.
created_at: 2025-05-07T11:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: А еще лучше принести ginkgo/gomega и переписать декларативно
created_at: 2025-05-07T11:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: это ты определись, а я уже определился

каким образом ты связал логически 8 и параметризацию тестов?

параметризация тестов нужна для избавления от копипасты и не обязательно копипастить чтобы где-то явно подставить 8-ку

параметрический тест это функция с аргументами (requestQueuesCount например), вызываться эта функция может из конкретного теста (принимая 8-ку в кач-ве requestQueuesCount) 
created_at: 2025-05-07T20:01:50Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	isLocalFsOverride bool,
+	requestQueuesCount uint32,
+) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	nfsClient := mocks.NewNfsEndpointClientMock()
+	nfsLocalClient := mocks.NewNfsEndpointClientMock()
+	mounter := csimounter.NewMock()
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	podId := "test-pod-id-13"
+	actualClientId := "testClientId-test-pod-id-13"
+	diskId := "test-disk-id-42"
+	deviceName := diskId
+	if deviceNameOpt != nil {
+		deviceName = *deviceNameOpt
+	}
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+	sourcePath := filepath.Join(socketsDir, podId, diskId)
+	targetPath := filepath.Join(tempDir, "pods", podId, "volumes", diskId, "mount")
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	nbsSocketPath := filepath.Join(sourcePath, "nbs.sock")
+	nfsSocketPath := filepath.Join(sourcePath, "nfs.sock")
+
+	localFsOverrides := make(LocalFilestoreOverrideMap)
+	if isLocalFsOverride {
+		localFsOverrides[diskId] = LocalFilestoreOverride{
+			FsId:           diskId,
+			LocalMountPath: "/tmp/mnt/local_mount",
+		}
+	}
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		true, // vmMode
+		socketsDir,
+		targetFsPathPattern,
+		"", // targetBlkPathPattern
+		localFsOverrides,
+		nbsClient,
+		nfsClient,
+		nfsLocalClient,
+		mounter,
+		[]string{},
+		true, // enable discard
+	)
+
+	accessMode := csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER
+	if backend == "nfs" {
+		accessMode = csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER
+	}
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{
+			Mount: &csi.VolumeCapability_MountVolume{},
+		},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: accessMode,
+		},
+	}
+
+	volumeContext := map[string]string{
+		backendVolumeContextKey: backend,
+	}
+	if deviceNameOpt != nil {
+		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
+	}
+	volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(requestQueuesCount))
+
+	_, err := nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
+
+	if backend == "nbs" {
+		nbsClient.On("DescribeVolume", ctx, &nbs.TDescribeVolumeRequest{
+			DiskId: diskId,
+		}).Return(
+			&nbs.TDescribeVolumeResponse{
+				Volume: &nbs.TVolume{
+					StorageMediaKind: storagecoreapi.EStorageMediaKind_STORAGE_MEDIA_SSD,
+				},
+			},
+			nil)
+		nbsClient.On("StartEndpoint", ctx, &nbs.TStartEndpointRequest{
+			UnixSocketPath:   nbsSocketPath,
+			DiskId:           diskId,
+			InstanceId:       podId,
+			ClientId:         actualClientId,
+			DeviceName:       deviceName,
+			IpcType:          nbs.EClientIpcType_IPC_VHOST,
+			VhostQueuesCount: virtioBlkVhostQueuesCount(requestQueuesCount),
+			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
+			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
+			Persistent:       true,
+			NbdDevice: &nbs.TStartEndpointRequest_UseFreeNbdDeviceFile{
+				false,
+			},
+			ClientProfile: &nbs.TClientProfile{
+				HostType: &hostType,
+			},
+			VhostDiscardEnabled: true,
+		}).Return(&nbs.TStartEndpointResponse{}, nil)
+	}
+
+	expectedNfsClient := nfsClient
+	if isLocalFsOverride {
+		expectedNfsClient = nfsLocalClient
+	}
+
+	if backend == "nfs" {
+		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
+			Endpoint: &nfs.TEndpointConfig{
+				SocketPath:       nfsSocketPath,
+				FileSystemId:     diskId,
+				ClientId:         actualClientId,
+				VhostQueuesCount: virtioFsVhostQueuesCount(requestQueuesCount),
+				Persistent:       true,
+			},
+		}).Return(&nfs.TStartEndpointResponse{}, nil)
+	}
+
+	mounter.On("IsMountPoint", targetPath).Return(false, nil)
+	mounter.On("Mount", sourcePath, targetPath, "", []string{"bind"}).Return(nil)
+
+	_, err = nodeService.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		TargetPath:        targetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	fileInfo, err := os.Stat(sourcePath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0755), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(filepath.Join(sourcePath, "disk.img"))
+	assert.False(t, os.IsNotExist(err))
+	assert.False(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0644), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(targetPath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0775), fileInfo.Mode().Perm())
+
+	mounter.On("CleanupMountPoint", targetPath).Return(nil)
+
+	nbsClient.On("StopEndpoint", ctx, &nbs.TStopEndpointRequest{
+		UnixSocketPath: nbsSocketPath,
+	}).Return(&nbs.TStopEndpointResponse{}, nil)
+
+	expectedNfsClient.On("StopEndpoint", ctx, &nfs.TStopEndpointRequest{
+		SocketPath: nfsSocketPath,
+	}).Return(&nfs.TStopEndpointResponse{}, nil)
+
+	_, err = nodeService.NodeUnpublishVolume(ctx, &csi.NodeUnpublishVolumeRequest{
+		VolumeId:   diskId,
+		TargetPath: targetPath,
+	})
+	require.NoError(t, err)
+
+	_, err = os.Stat(filepath.Join(socketsDir, podId))
+	assert.True(t, os.IsNotExist(err))
+
+	_, err = nodeService.NodeUnstageVolume(ctx, &csi.NodeUnstageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+	})
+	require.NoError(t, err)
+}
+
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
 	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
 }
 
-func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
-}
+func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+}
+
+func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", &deviceName, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, true, requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: не понял как это связано

не понимаю почему тут сложно не добавлять копипасту?

created_at: 2025-05-07T20:03:29Z
author_association: COLLABORATOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: >  ginkgo/gomega и переписать декларативно

не понял к чему эта фраза
created_at: 2025-05-07T20:04:18Z
author_association: COLLABORATOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			if nfsBackend {
+				settings.queuesCount = virtioFsVhostQueuesCount(requestQueuesCount)
+			} else {
+				settings.queuesCount = virtioBlkVhostQueuesCount(requestQueuesCount)
+			}
+		}
+	} else {
+		settings.queuesCount = defaultVhostQueuesCount
+	}
+
+	if len(errs) > 0 {
+		return settings, errors.Join(errs...)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: ты возвращаешь settings в случае ошибки, а принято возвращать дефолтное значение, т.е. vhostSettings{} 
created_at: 2025-05-07T20:05:39Z
author_association: COLLABORATOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: добавлять копипасту не надо
created_at: 2025-05-07T20:07:11Z
author_association: COLLABORATOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: SvartMetal
body: > Вообще в целом для всех валидаторов хорошая практика - собирать все ошибки скопом, а не отплевываться по одной. Это намного упрощает потом отладку API, и пользователю возвращаются сразу все ошибки в параметрах

так а для чего здесь собирать все ошибки? в вызывающем коде ты разве обрабатываешь все ошибки?

почему здесь не прервать исполнение после первой же попавшейся ошибки, как мы обычно делаем
created_at: 2025-05-07T20:12:53Z
author_association: COLLABORATOR
diff_hunk: @@ -108,6 +112,66 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // additional 1 for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := volumeContext[backendVolumeContextKey] == "nfs"
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: jkuradobery
body: ```
if err != nil{
return
}

if .. < 1{
return
}
```
else не нужно. 
created_at: 2025-05-08T09:00:06Z
author_association: COLLABORATOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Ок, смотри текущую версию.
created_at: 2025-05-08T09:01:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: jkuradobery
body: > А еще лучше принести ginkgo/gomega и переписать декларативно


Спасибо за предложение, мы подумаем над этим. 
На вкус и цвет конечно, в моей практике декларативные тестовые фремворки приносили скорее боль. Предположу что это out of scope текущего ревью. 
created_at: 2025-05-08T09:03:58Z
author_association: COLLABORATOR
diff_hunk: @@ -454,6 +876,189 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
 	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
 }
 
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nbs", &deviceName, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, false, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestStagedPublishUnpublishVolumeForKubevirtRequestQueuesCount(t, "nfs", nil, false, true, requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(LocalFilestoreOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: Ок, в скоупе этого ревью я отрефачил существующие тесты. Посмотрите плз.
created_at: 2025-05-08T12:02:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +108,55 @@ var podModeCapabilities = []*csi.NodeServiceCapability{
 	},
 }
 
+type vhostSettings struct {
+	queuesCount uint32
+}
+
+func virtioBlkVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount
+}
+
+func virtioFsVhostQueuesCount(requestQueuesCount uint32) uint32 {
+	return requestQueuesCount + 1 // for hiprio queue
+}
+
+func readVhostSettings(volumeContext map[string]string) (vhostSettings, error) {
+	errs := make([]error, 0)
+	settings := vhostSettings{
+		queuesCount: defaultVhostQueuesCount,
+	}
+
+	if volumeContext == nil { // return default settings
+		return settings, nil
+	}
+
+	nfsBackend := (volumeContext[backendVolumeContextKey] == "nfs")
+
+	if requestQueuesCountCtx, found := volumeContext[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to parse context attribute %q (=%q): %w", requestQueuesCountVolumeContextKey, requestQueuesCountCtx, err))
+		} else if requestQueuesCountParsed < 1 || requestQueuesCountParsed > 65536 {
+			errs = append(errs, fmt.Errorf("context attribute %q must be in range [1..65536]", requestQueuesCountVolumeContextKey))
+		} else {
+			requestQueuesCount := uint32(requestQueuesCountParsed)
+			if nfsBackend {
+				settings.queuesCount = virtioFsVhostQueuesCount(requestQueuesCount)
+			} else {
+				settings.queuesCount = virtioBlkVhostQueuesCount(requestQueuesCount)
+			}
+		}
+	} else {
+		settings.queuesCount = defaultVhostQueuesCount
+	}
+
+	if len(errs) > 0 {
+		return settings, errors.Join(errs...)
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: Это достаточно спорная история, так как по конвенции ни в одном из случаев возвращаемое значение невалидно при `err != nil`.

Но для ускорения ревью ок, сделаю как ты хочешь.
created_at: 2025-05-08T12:05:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,24 +217,439 @@ func doTestPublishUnpublishVolumeForKubevirt(t *testing.T, backend string, devic
 	require.NoError(t, err)
 }
 
+func doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(
+	t *testing.T,
+	backend string,
+	deviceNameOpt *string,
+	isLocalFsOverride bool,
+	requestQueuesCount uint32,
+) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	nfsClient := mocks.NewNfsEndpointClientMock()
+	nfsLocalClient := mocks.NewNfsEndpointClientMock()
+	mounter := csimounter.NewMock()
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	podId := "test-pod-id-13"
+	actualClientId := "testClientId-test-pod-id-13"
+	diskId := "test-disk-id-42"
+	deviceName := diskId
+	if deviceNameOpt != nil {
+		deviceName = *deviceNameOpt
+	}
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+	sourcePath := filepath.Join(socketsDir, podId, diskId)
+	targetPath := filepath.Join(tempDir, "pods", podId, "volumes", diskId, "mount")
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	nbsSocketPath := filepath.Join(sourcePath, "nbs.sock")
+	nfsSocketPath := filepath.Join(sourcePath, "nfs.sock")
+
+	localFsOverrides := make(LocalFilestoreOverrideMap)
+	if isLocalFsOverride {
+		localFsOverrides[diskId] = LocalFilestoreOverride{
+			FsId:           diskId,
+			LocalMountPath: "/tmp/mnt/local_mount",
+		}
+	}
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		true, // vmMode
+		socketsDir,
+		targetFsPathPattern,
+		"", // targetBlkPathPattern
+		localFsOverrides,
+		nbsClient,
+		nfsClient,
+		nfsLocalClient,
+		mounter,
+		[]string{},
+		true, // enable discard
+	)
+
+	accessMode := csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER
+	if backend == "nfs" {
+		accessMode = csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER
+	}
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{
+			Mount: &csi.VolumeCapability_MountVolume{},
+		},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: accessMode,
+		},
+	}
+
+	volumeContext := map[string]string{
+		backendVolumeContextKey: backend,
+	}
+	if deviceNameOpt != nil {
+		volumeContext[deviceNameVolumeContextKey] = *deviceNameOpt
+	}
+	volumeContext[requestQueuesCountVolumeContextKey] = strconv.Itoa(int(requestQueuesCount))
+
+	_, err := nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	hostType := nbs.EHostType_HOST_TYPE_DEFAULT
+
+	if backend == "nbs" {
+		nbsClient.On("DescribeVolume", ctx, &nbs.TDescribeVolumeRequest{
+			DiskId: diskId,
+		}).Return(
+			&nbs.TDescribeVolumeResponse{
+				Volume: &nbs.TVolume{
+					StorageMediaKind: storagecoreapi.EStorageMediaKind_STORAGE_MEDIA_SSD,
+				},
+			},
+			nil)
+		nbsClient.On("StartEndpoint", ctx, &nbs.TStartEndpointRequest{
+			UnixSocketPath:   nbsSocketPath,
+			DiskId:           diskId,
+			InstanceId:       podId,
+			ClientId:         actualClientId,
+			DeviceName:       deviceName,
+			IpcType:          nbs.EClientIpcType_IPC_VHOST,
+			VhostQueuesCount: virtioBlkVhostQueuesCount(requestQueuesCount),
+			VolumeAccessMode: nbs.EVolumeAccessMode_VOLUME_ACCESS_READ_WRITE,
+			VolumeMountMode:  nbs.EVolumeMountMode_VOLUME_MOUNT_LOCAL,
+			Persistent:       true,
+			NbdDevice: &nbs.TStartEndpointRequest_UseFreeNbdDeviceFile{
+				false,
+			},
+			ClientProfile: &nbs.TClientProfile{
+				HostType: &hostType,
+			},
+			VhostDiscardEnabled: true,
+		}).Return(&nbs.TStartEndpointResponse{}, nil)
+	}
+
+	expectedNfsClient := nfsClient
+	if isLocalFsOverride {
+		expectedNfsClient = nfsLocalClient
+	}
+
+	if backend == "nfs" {
+		expectedNfsClient.On("StartEndpoint", ctx, &nfs.TStartEndpointRequest{
+			Endpoint: &nfs.TEndpointConfig{
+				SocketPath:       nfsSocketPath,
+				FileSystemId:     diskId,
+				ClientId:         actualClientId,
+				VhostQueuesCount: virtioFsVhostQueuesCount(requestQueuesCount),
+				Persistent:       true,
+			},
+		}).Return(&nfs.TStartEndpointResponse{}, nil)
+	}
+
+	mounter.On("IsMountPoint", targetPath).Return(false, nil)
+	mounter.On("Mount", sourcePath, targetPath, "", []string{"bind"}).Return(nil)
+
+	_, err = nodeService.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		TargetPath:        targetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.NoError(t, err)
+
+	fileInfo, err := os.Stat(sourcePath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0755), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(filepath.Join(sourcePath, "disk.img"))
+	assert.False(t, os.IsNotExist(err))
+	assert.False(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0644), fileInfo.Mode().Perm())
+
+	fileInfo, err = os.Stat(targetPath)
+	assert.False(t, os.IsNotExist(err))
+	assert.True(t, fileInfo.IsDir())
+	assert.Equal(t, fs.FileMode(0775), fileInfo.Mode().Perm())
+
+	mounter.On("CleanupMountPoint", targetPath).Return(nil)
+
+	nbsClient.On("StopEndpoint", ctx, &nbs.TStopEndpointRequest{
+		UnixSocketPath: nbsSocketPath,
+	}).Return(&nbs.TStopEndpointResponse{}, nil)
+
+	expectedNfsClient.On("StopEndpoint", ctx, &nfs.TStopEndpointRequest{
+		SocketPath: nfsSocketPath,
+	}).Return(&nfs.TStopEndpointResponse{}, nil)
+
+	_, err = nodeService.NodeUnpublishVolume(ctx, &csi.NodeUnpublishVolumeRequest{
+		VolumeId:   diskId,
+		TargetPath: targetPath,
+	})
+	require.NoError(t, err)
+
+	_, err = os.Stat(filepath.Join(socketsDir, podId))
+	assert.True(t, os.IsNotExist(err))
+
+	_, err = nodeService.NodeUnstageVolume(ctx, &csi.NodeUnstageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+	})
+	require.NoError(t, err)
+}
+
 func TestPublishUnpublishDiskForKubevirt(t *testing.T) {
 	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false)
 }
 
-func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
-}
+func TestPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	doTestPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false)
+}
+
+func TestPublishUnpublishFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
+	doTestPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true)
+}
+
+func TestPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nbs", &deviceName, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, false, requestQueuesCount)
+}
+
+func TestPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	var requestQueuesCount uint32 = 32
+	doTestPublishUnpublishVolumeForKubevirtRequesQueuesCount(t, "nfs", nil, true, requestQueuesCount)
+}
+
+func doTestStagedPublishUnpublishVolumeForKubevirt(
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: убрал
created_at: 2025-05-08T12:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -485,6 +497,56 @@ func (s *nodeService) NodeGetInfo(
 	}, nil
 }
 
+func validateVolumeContext(contextMap map[string]string) (volumeContext, error) {
+	ctx := volumeContext{}
+
+	if contextMap == nil {
+		return ctx, fmt.Errorf("volume context is empty")
+	}
+
+	errs := make([]error, 0)
+	if requestQueuesCountCtx, found := contextMap[requestQueuesCountVolumeContextKey]; found {
+		requestQueuesCountParsed, err := strconv.ParseUint(requestQueuesCountCtx, 10, 32)
+		if err != nil {
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: > в вызывающем коде ты разве обрабатываешь все ошибки?

Речь не про вызывающий код, а про клиента CSI драйвера. Если клиент увидит все ошибки разом - это проще чинить, чем править по одному параметру за раз.
created_at: 2025-05-08T12:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,38 +524,299 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	assert.True(t, os.IsNotExist(err))
 }
 
-func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
-}
+func TestStagedPublishUnpublishVolumeForKubevirtTable(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: TestStagedPublishUnpublishVolumeForKubevirt
created_at: 2025-05-10T21:21:26Z
author_association: COLLABORATOR
diff_hunk: @@ -422,38 +524,299 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	assert.True(t, os.IsNotExist(err))
 }
 
-func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
-}
+func TestStagedPublishUnpublishVolumeForKubevirtTable(t *testing.T) {
+	deviceName1 := "test-disk-name-42"
+	rqc32 := uint32(32)
+
+	testCases := []struct {
+		name                  string
+		backend               string
+		deviceNameOpt         *string
+		perInstanceVolumes    bool
+		isLocalFsOverride     bool
+		requestQueuesCountOpt *uint32
+	}{
+		// Legacy tests (perInstanceVolumes = false, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirtLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Per-instance volume tests (perInstanceVolumes = true, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirt",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceName",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Multiqueue tests (perInstanceVolumes = false, requestQueuesCountOpt = &rqc32)
+		// These correspond to the original "Multiqueue" tests which were legacy style.
+		{
+			name:                  "DiskForKubevirtMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "FilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: &rqc32,
+		},
+	}
 
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	for _, tc := range testCases {
+		tc := tc // Capture range variable.
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel() // Mark test as parallelizable.
+			doTestStagedPublishUnpublishVolumeForKubevirtHelper(
+				t,
+				tc.backend,
+				tc.deviceNameOpt,
+				tc.perInstanceVolumes,
+				tc.isLocalFsOverride,
+				tc.requestQueuesCountOpt,
+			)
+		})
+	}
 }
 
-func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
-}
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
 
-func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
-}
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
 
-func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
-}
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
 
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(ExternalFsOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
 }
 
-func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+func TestStagedPublishUnpublishDiskForKubevirtZeroQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(ExternalFsOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: SvartMetal
body: осталось избавиться от этой копипасты
created_at: 2025-05-10T21:23:14Z
author_association: COLLABORATOR
diff_hunk: @@ -855,7 +939,7 @@ func (s *nodeService) startNbsEndpointForNBD(
 		ClientId:         fmt.Sprintf("%s-%s", s.clientId, nbsInstanceId),
 		DeviceName:       deviceName,
 		IpcType:          nbdIpc,
-		VhostQueuesCount: 8,
+		VhostQueuesCount: vhostSettings.queuesCount,
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: antonmyagkov
body: Здесь просто нужно удалить этот параметр и убрать vhostSettings. vhost в этой конфигурации не используется.

Давай удалим в этом pr иначе всеравно конфликт будет. А свой я закрою https://github.com/ydb-platform/nbs/pull/3385
created_at: 2025-05-14T08:43:34Z
author_association: COLLABORATOR
diff_hunk: @@ -421,37 +454,220 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
 	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false, nil)
 }
 
 func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false, nil)
 }
 
 func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true)
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, true, nil)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameMultiqueue(t *testing.T) {
+	deviceName := "test-disk-name-42"
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishLocalFilestoreForKubevirtMultiqueue(t *testing.T) {
+	requestQueuesCount := 32
+	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true, &requestQueuesCount)
+}
+
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: убрал, написал специфичный низкоуровневый тест на ошибки
created_at: 2025-05-14T17:17:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -422,38 +524,299 @@ func doTestStagedPublishUnpublishVolumeForKubevirt(t *testing.T, backend string,
 	assert.True(t, os.IsNotExist(err))
 }
 
-func TestStagedPublishUnpublishDiskForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, false, false)
-}
+func TestStagedPublishUnpublishVolumeForKubevirtTable(t *testing.T) {
+	deviceName1 := "test-disk-name-42"
+	rqc32 := uint32(32)
+
+	testCases := []struct {
+		name                  string
+		backend               string
+		deviceNameOpt         *string
+		perInstanceVolumes    bool
+		isLocalFsOverride     bool
+		requestQueuesCountOpt *uint32
+	}{
+		// Legacy tests (perInstanceVolumes = false, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirtLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameLegacy",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtLegacy",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Per-instance volume tests (perInstanceVolumes = true, requestQueuesCountOpt = nil)
+		{
+			name:                  "DiskForKubevirt",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceName",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "FilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: nil,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirt",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    true,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: nil,
+		},
+		// Multiqueue tests (perInstanceVolumes = false, requestQueuesCountOpt = &rqc32)
+		// These correspond to the original "Multiqueue" tests which were legacy style.
+		{
+			name:                  "DiskForKubevirtMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "DiskForKubevirtSetDeviceNameMultiqueue",
+			backend:               "nbs",
+			deviceNameOpt:         &deviceName1,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "FilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     false,
+			requestQueuesCountOpt: &rqc32,
+		},
+		{
+			name:                  "LocalFilestoreForKubevirtMultiqueue",
+			backend:               "nfs",
+			deviceNameOpt:         nil,
+			perInstanceVolumes:    false,
+			isLocalFsOverride:     true,
+			requestQueuesCountOpt: &rqc32,
+		},
+	}
 
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceNameLegacy(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, false, false)
+	for _, tc := range testCases {
+		tc := tc // Capture range variable.
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel() // Mark test as parallelizable.
+			doTestStagedPublishUnpublishVolumeForKubevirtHelper(
+				t,
+				tc.backend,
+				tc.deviceNameOpt,
+				tc.perInstanceVolumes,
+				tc.isLocalFsOverride,
+				tc.requestQueuesCountOpt,
+			)
+		})
+	}
 }
 
-func TestStagedPublishUnpublishFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, false)
-}
+func TestStagedPublishUnpublishDiskForKubevirtNegativeQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
 
-func TestStagedPublishUnpublishLocalFilestoreForKubevirtLegacy(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, false, true)
-}
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
 
-func TestStagedPublishUnpublishDiskForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", nil, true, false)
-}
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
 
-func TestStagedPublishUnpublishDiskForKubevirtSetDeviceName(t *testing.T) {
-	deviceName := "test-disk-name-42"
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nbs", &deviceName, true, false)
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(ExternalFsOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
+
+	volumeContext := map[string]string{
+		requestQueuesCountVolumeContextKey: "-1",
+	}
+
+	_, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
+		VolumeId:          diskId,
+		StagingTargetPath: stagingTargetPath,
+		VolumeCapability:  &volumeCapability,
+		VolumeContext:     volumeContext,
+	})
+	require.ErrorContains(t, err, "failed to parse context attribute")
 }
 
-func TestStagedPublishUnpublishFilestoreForKubevirt(t *testing.T) {
-	doTestStagedPublishUnpublishVolumeForKubevirt(t, "nfs", nil, true, false)
+func TestStagedPublishUnpublishDiskForKubevirtZeroQueueCount(t *testing.T) {
+	tempDir := t.TempDir()
+
+	nbsClient := mocks.NewNbsClientMock()
+	mounter := csimounter.NewMock()
+
+	nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3")
+	err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755))
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	nodeId := "testNodeId"
+	clientId := "testClientId"
+	diskId := "test-disk-id-42"
+	targetFsPathPattern := filepath.Join(tempDir, "pods/([a-z0-9-]+)/volumes/([a-z0-9-]+)/mount")
+	stagingTargetPath := "testStagingTargetPath"
+	socketsDir := filepath.Join(tempDir, "sockets")
+
+	nodeService := newNodeService(
+		nodeId,
+		clientId,
+		false,
+		socketsDir,
+		targetFsPathPattern,
+		"",
+		make(ExternalFsOverrideMap),
+		nbsClient,
+		nil,
+		nil,
+		nil,
+		mounter,
+		[]string{},
+		false,
+	)
+
+	volumeCapability := csi.VolumeCapability{
+		AccessType: &csi.VolumeCapability_Mount{},
+		AccessMode: &csi.VolumeCapability_AccessMode{
+			Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
+		},
+	}
path: cloud/blockstore/tools/csi_driver/internal/driver/node_test.go
user: login: ak0rz
body: убрал
created_at: 2025-05-14T17:18:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,7 +939,7 @@ func (s *nodeService) startNbsEndpointForNBD(
 		ClientId:         fmt.Sprintf("%s-%s", s.clientId, nbsInstanceId),
 		DeviceName:       deviceName,
 		IpcType:          nbdIpc,
-		VhostQueuesCount: 8,
+		VhostQueuesCount: vhostSettings.queuesCount,
path: cloud/blockstore/tools/csi_driver/internal/driver/node.go
user: login: ak0rz
body: ок, убрал!
created_at: 2025-05-14T17:30:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-25T12:51:45Z
body: Hi! Thank you for contributing!
The tests on this PR will run after a maintainer adds an `ok-to-test` label to this PR manually. Thank you for your patience!
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-30T19:11:24Z
body: <!-- status pr=3425, run=11196, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 4edfa4df21230ba711ec4869647cf770d4682f2e.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [4926](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14762018823/1/nebius-x86-64/summary/ya-test.html) | [4926](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14762018823/1/nebius-x86-64/summary/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-07T12:36:31Z
body: <!-- status pr=3425, run=11335, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 48eff44d118475e6e8bcb9e96c63b1f4c998f1e9.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [4935](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14882985352/1/nebius-x86-64/summary/ya-test.html) | [4935](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14882985352/1/nebius-x86-64/summary/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T09:14:20Z
body: <!-- status pr=3425, run=11348, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 0cd787b85482d5a97f13f1433b29b4f733fc5281.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [4946](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14902527115/1/nebius-x86-64/summary/ya-test.html) | [4946](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14902527115/1/nebius-x86-64/summary/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T12:36:13Z
body: <!-- status pr=3425, run=11361, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 74f51585174f1b3ca4bb0b793eb383f00f98eae4.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [4948](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14906055632/1/nebius-x86-64/summary/ya-test.html) | [4948](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/14906055632/1/nebius-x86-64/summary/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T18:04:46Z
body: <!-- status pr=3425, run=11553, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit b7287bffd53dcee3c97857e524437716ec39331f.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [4959](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15027350914/1/nebius-x86-64/summary/1/ya-test.html) | [4959](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15027350914/1/nebius-x86-64/summary/1/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR