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 6e67be5d57b..5d7c4134bc5 100644 --- a/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py +++ b/cloud/blockstore/tests/csi_driver/e2e_tests_part2/test.py @@ -168,3 +168,36 @@ def test_node_volume_expand_vm_mode(): raise finally: csi.cleanup_after_test(env, volume_name, access_type, [pod_id]) + + +def test_publish_volume_must_fail_after_fs_error(): + 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) + env.csi.publish_volume(pod_id, volume_name, pod_name, access_type) + + with open('/sys/fs/ext4/nbd0/trigger_fs_error', 'w') as f: + f.write("test error") + + env.csi.unpublish_volume(pod_id, volume_name, access_type) + + stage_path = "/var/lib/kubelet/plugins/kubernetes.io/csi/nbs.csi.nebius.ai/a/globalmount" + assert "ro" == get_access_mode(stage_path) + + try: + env.csi.publish_volume(pod_id, volume_name, pod_name, access_type) + assert False + except subprocess.CalledProcessError: + pass + + except subprocess.CalledProcessError as e: + csi.log_called_process_error(e) + raise + finally: + csi.cleanup_after_test(env, volume_name, access_type, [pod_id]) diff --git a/cloud/blockstore/tools/csi_driver/internal/driver/node.go b/cloud/blockstore/tools/csi_driver/internal/driver/node.go index 44c0572104b..e562f182824 100644 --- a/cloud/blockstore/tools/csi_driver/internal/driver/node.go +++ b/cloud/blockstore/tools/csi_driver/internal/driver/node.go @@ -623,6 +623,13 @@ func (s *nodeService) nodePublishDiskAsFilesystem( "Staging target path is not mounted: %w", req.VolumeId) } + readOnly, _ := s.mounter.IsFilesystemRemountedAsReadonly(req.StagingTargetPath) + if readOnly { + return s.statusErrorf( + codes.Internal, + "Filesystem was remounted as readonly") + } + mounted, _ = s.mounter.IsMountPoint(req.TargetPath) if !mounted { targetPerm := os.FileMode(0775) 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 2e30f952911..7402d2e715f 100644 --- a/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go +++ b/cloud/blockstore/tools/csi_driver/internal/driver/node_test.go @@ -545,6 +545,7 @@ func TestPublishUnpublishDiskForInfrakuber(t *testing.T) { mounter.On("IsMountPoint", stagingTargetPath).Return(true, nil) mounter.On("IsMountPoint", targetPath).Return(false, nil) + mounter.On("IsFilesystemRemountedAsReadonly", stagingTargetPath).Return(false, nil) mounter.On("Mount", stagingTargetPath, targetPath, "", []string{"bind"}).Return(nil) diff --git a/cloud/blockstore/tools/csi_driver/internal/mounter/iface.go b/cloud/blockstore/tools/csi_driver/internal/mounter/iface.go index f1e3ead296b..18d17f9fdfb 100644 --- a/cloud/blockstore/tools/csi_driver/internal/mounter/iface.go +++ b/cloud/blockstore/tools/csi_driver/internal/mounter/iface.go @@ -12,6 +12,7 @@ type Interface interface { HasBlockDevice(device string) (bool, error) IsFilesystemExisted(device string) (bool, error) + IsFilesystemRemountedAsReadonly(mountPoint string) (bool, error) MakeFilesystem(device string, fsType string) ([]byte, error) NeedResize(devicePath string, deviceMountPath string) (bool, error) diff --git a/cloud/blockstore/tools/csi_driver/internal/mounter/mock.go b/cloud/blockstore/tools/csi_driver/internal/mounter/mock.go index 10245237e4d..a319d5017f7 100644 --- a/cloud/blockstore/tools/csi_driver/internal/mounter/mock.go +++ b/cloud/blockstore/tools/csi_driver/internal/mounter/mock.go @@ -40,6 +40,11 @@ func (c *Mock) IsFilesystemExisted(device string) (bool, error) { return args.Get(0).(bool), args.Error(1) } +func (c *Mock) IsFilesystemRemountedAsReadonly(mountPoint string) (bool, error) { + args := c.Called(mountPoint) + return args.Get(0).(bool), args.Error(1) +} + func (c *Mock) MakeFilesystem(device string, fsType string) ([]byte, error) { args := c.Called(device, fsType) return args.Get(0).([]byte), args.Error(1) diff --git a/cloud/blockstore/tools/csi_driver/internal/mounter/mounter.go b/cloud/blockstore/tools/csi_driver/internal/mounter/mounter.go index 6c2d9bbedc6..440870f853f 100644 --- a/cloud/blockstore/tools/csi_driver/internal/mounter/mounter.go +++ b/cloud/blockstore/tools/csi_driver/internal/mounter/mounter.go @@ -78,6 +78,39 @@ func (m *mounter) IsFilesystemExisted(device string) (bool, error) { return err == nil && string(out) != "", nil } +func (m *mounter) IsFilesystemRemountedAsReadonly(mountPoint string) (bool, error) { + mountInfoList, err := mount.ParseMountInfo("/proc/self/mountinfo") + if err != nil { + return false, err + } + + for _, mountInfo := range mountInfoList { + if mountInfo.MountPoint == mountPoint { + // The filesystem was remounted as read-only + // if the mount options included a read-write option, while + // the superblock options specified a read-only option. + var readWriteFs = false + for _, mountOption := range mountInfo.MountOptions { + if mountOption == "rw" { + readWriteFs = true + break + } + } + + if !readWriteFs { + return false, nil + } + + for _, superOption := range mountInfo.SuperOptions { + if superOption == "ro" { + return true, nil + } + } + } + } + return false, nil +} + func (m *mounter) MakeFilesystem(device string, fsType string) ([]byte, error) { options := []string{"-t", fsType} if fsType == "ext4" {