diff --git a/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py b/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py index ae5b472f46e..6e67be5d57b 100644 --- a/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py +++ b/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py @@ -1,6 +1,8 @@ import pytest import subprocess +from pathlib import Path + import cloud.blockstore.tests.csi_driver.lib.csi_runner as csi @@ -59,6 +61,74 @@ def test_readonly_volume(mount_path, access_type, vm_mode, gid): csi.cleanup_after_test(env, volume_name, access_type, [pod_id]) +def test_mount_volume_group(): + # Scenario + # 1. create volume and publish volume without mount volume group + # 2. create directory and file + # 3. unpublish volume + # 4. create new group with specified GID + # 5. publish volume with mount volume group GID + # 6. check that mounted dir and existing files have specified GID + # 7. create new directory and file + # 8. check that new directory and file have specified GID + env, run = csi.init() + try: + volume_name = "example-disk" + volume_size = 1024 ** 3 + pod_name = "example-pod" + pod_id = "deadbeef1" + access_type = "mount" + env.csi.create_volume(name=volume_name, size=volume_size) + env.csi.stage_volume(volume_name, access_type) + + gid = 1013 + result = subprocess.run( + ["groupadd", "-g", str(gid), "test_group_" + str(gid)], + capture_output=True, + ) + assert result.returncode == 0 + + env.csi.publish_volume( + pod_id, + volume_name, + pod_name, + access_type + ) + + mount_path = Path("/var/lib/kubelet/pods") / pod_id / "volumes/kubernetes.io~csi" / volume_name / "mount" + test_dir1 = mount_path / "testdir1" + test_dir1.mkdir() + test_file1 = test_dir1 / "testfile1" + test_file1.touch() + + env.csi.unpublish_volume(pod_id, volume_name, access_type) + env.csi.publish_volume( + pod_id, + volume_name, + pod_name, + access_type, + volume_mount_group=str(gid) + ) + + assert gid == mount_path.stat().st_gid + assert gid == test_dir1.stat().st_gid + assert gid == test_file1.stat().st_gid + + test_file2 = mount_path / "testfile2" + test_file2.touch() + assert gid == test_file2.stat().st_gid + + test_dir2 = mount_path / "testdir2" + test_dir2.mkdir() + assert gid == test_dir2.stat().st_gid + + except subprocess.CalledProcessError as e: + csi.log_called_process_error(e) + raise + finally: + csi.cleanup_after_test(env, volume_name, access_type, [pod_id]) + + def test_node_volume_expand_vm_mode(): env, run = csi.init(vm_mode=True) try: diff --git a/cloud/blockstore/tools/csi_driver/deploy/manifests/4-storageclass.yaml b/cloud/blockstore/tools/csi_driver/deploy/manifests/4-storageclass.yaml index 7f08d470901..a153030f3a1 100644 --- a/cloud/blockstore/tools/csi_driver/deploy/manifests/4-storageclass.yaml +++ b/cloud/blockstore/tools/csi_driver/deploy/manifests/4-storageclass.yaml @@ -6,5 +6,3 @@ metadata: provisioner: nbs.csi.nebius.ai volumeBindingMode: Immediate allowVolumeExpansion: true -parameters: - fsType: ext4 diff --git a/cloud/blockstore/tools/csi_driver/internal/driver/node.go b/cloud/blockstore/tools/csi_driver/internal/driver/node.go index 3262e4bd10d..44c0572104b 100644 --- a/cloud/blockstore/tools/csi_driver/internal/driver/node.go +++ b/cloud/blockstore/tools/csi_driver/internal/driver/node.go @@ -11,6 +11,7 @@ import ( "log" "math" "os" + "os/exec" "path/filepath" "regexp" "strings" @@ -54,6 +55,13 @@ var vmModeCapabilities = []*csi.NodeServiceCapability{ }, }, }, + { + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_VOLUME_MOUNT_GROUP, + }, + }, + }, { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ @@ -73,6 +81,13 @@ var podModeCapabilities = []*csi.NodeServiceCapability{ }, }, }, + { + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_VOLUME_MOUNT_GROUP, + }, + }, + }, { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ @@ -643,6 +658,14 @@ func (s *nodeService) nodePublishDiskAsFilesystem( return err } + if mnt != nil && mnt.VolumeMountGroup != "" && !req.Readonly { + cmd := exec.Command("chown", "-R", ":"+mnt.VolumeMountGroup, req.TargetPath) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to chown %s to %q: %w, output %q", + mnt.VolumeMountGroup, req.TargetPath, err, out) + } + } + return nil } @@ -697,7 +720,7 @@ func (s *nodeService) nodeStageDiskAsFilesystem( return fmt.Errorf("failed to create staging directory: %w", err) } - mountOptions := []string{} + mountOptions := []string{"grpid"} if fsType == "ext4" { mountOptions = append(mountOptions, "errors=remount-ro") } @@ -718,6 +741,14 @@ func (s *nodeService) nodeStageDiskAsFilesystem( return fmt.Errorf("failed to format or mount filesystem: %w", err) } + if mnt != nil && mnt.VolumeMountGroup != "" { + cmd := exec.Command("chown", "-R", ":"+mnt.VolumeMountGroup, req.StagingTargetPath) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to chown %s to %q: %w, output %q", + mnt.VolumeMountGroup, req.StagingTargetPath, err, out) + } + } + if err := os.Chmod(req.StagingTargetPath, targetPerm); err != nil { return fmt.Errorf("failed to chmod target path: %w", 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 f57ae786c52..2e30f952911 100644 --- a/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go +++ b/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go @@ -6,10 +6,13 @@ import ( "context" "fmt" "io/fs" + "log" "os" "os/exec" + "os/user" "path/filepath" "slices" + "strings" "testing" "github.com/container-storage-interface/spec/lib/go/csi" @@ -439,12 +442,24 @@ func TestStagedPublishUnpublishLocalFilestoreForKubevirt(t *testing.T) { func TestPublishUnpublishDiskForInfrakuber(t *testing.T) { tempDir := t.TempDir() + groupId := "" + currentUser, err := user.Current() + require.NoError(t, err) + groups, err := currentUser.GroupIds() + require.NoError(t, err) + for _, group := range groups { + if group != "" && group != "0" { + groupId = group + } + } + log.Printf("groupId: %s", groupId) + nbsClient := mocks.NewNbsClientMock() mounter := csimounter.NewMock() ipcType := nbs.EClientIpcType_IPC_NBD nbdDeviceFile := filepath.Join(tempDir, "dev", "nbd3") - err := os.MkdirAll(nbdDeviceFile, fs.FileMode(0755)) + err = os.MkdirAll(nbdDeviceFile, fs.FileMode(0755)) require.NoError(t, err) ctx := context.Background() @@ -477,7 +492,11 @@ func TestPublishUnpublishDiskForInfrakuber(t *testing.T) { ) volumeCapability := csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{}, + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + VolumeMountGroup: groupId, + }, + }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, @@ -512,7 +531,7 @@ func TestPublishUnpublishDiskForInfrakuber(t *testing.T) { mockCallIsMountPoint := mounter.On("IsMountPoint", stagingTargetPath).Return(false, nil) mounter.On("FormatAndMount", nbdDeviceFile, stagingTargetPath, "ext4", - []string{"errors=remount-ro"}).Return(nil) + []string{"grpid", "errors=remount-ro"}).Return(nil) _, err = nodeService.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{ VolumeId: diskId, @@ -548,8 +567,11 @@ func TestPublishUnpublishDiskForInfrakuber(t *testing.T) { assert.True(t, fileInfo.IsDir()) assert.Equal(t, fs.FileMode(0775), fileInfo.Mode().Perm()) - _, err = exec.Command("ls", "-ldn", targetPath).CombinedOutput() + output, err := exec.Command("ls", "-ldn", targetPath).CombinedOutput() assert.False(t, os.IsNotExist(err)) + log.Printf("Target path: %s", output) + fields := strings.Fields(string(output)) + assert.Equal(t, groupId, fields[3]) mockCallCleanupMountPoint := mounter.On("CleanupMountPoint", targetPath).Return(nil)