Skip to content

Commit eb9bf7f

Browse files
pwschuurmanarkadeepsen
authored andcommitted
Add support for checking if a device is being used by a filesystem
1 parent 3c4464b commit eb9bf7f

File tree

6 files changed

+158
-9
lines changed

6 files changed

+158
-9
lines changed

pkg/deviceutils/device-utils.go

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"k8s.io/apimachinery/pkg/util/wait"
2828
"k8s.io/klog/v2"
29+
"k8s.io/mount-utils"
2930
pathutils "k8s.io/utils/path"
3031
"sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/resizefs"
3132
)
@@ -91,6 +92,13 @@ type DeviceUtils interface {
9192

9293
// Resize returns whether or not a device needs resizing.
9394
Resize(resizer resizefs.Resizefs, devicePath string, deviceMountPath string) (bool, error)
95+
96+
// IsDeviceFilesystemInUse returns if a device path with the specified fstype
97+
// TODO: Mounter is passed in in order to call GetDiskFormat()
98+
// This is currently only implemented in mounter_linux, not mounter_windows.
99+
// Refactor this interface and function call up the stack to the caller once it is
100+
// implemented in mounter_windows.
101+
IsDeviceFilesystemInUse(mounter *mount.SafeFormatAndMount, devicePath, devFsPath string) (bool, error)
94102
}
95103

96104
type deviceUtils struct {

pkg/deviceutils/device-utils_linux.go

+22
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,31 @@ import (
2020
"fmt"
2121
"os"
2222
"path/filepath"
23+
24+
"k8s.io/mount-utils"
2325
)
2426

2527
func (_ *deviceUtils) DisableDevice(devicePath string) error {
2628
deviceName := filepath.Base(devicePath)
2729
return os.WriteFile(fmt.Sprintf("/sys/block/%s/device/state", deviceName), []byte("offline"), 0644)
2830
}
31+
32+
func (_ *deviceUtils) IsDeviceFilesystemInUse(mounter *mount.SafeFormatAndMount, devicePath, devFsPath string) (bool, error) {
33+
fstype, err := mounter.GetDiskFormat(devicePath)
34+
if err != nil {
35+
return false, fmt.Errorf("failed to get disk format for %s (aka %s): %v", devicePath, devFsPath, err)
36+
}
37+
38+
devFsName := filepath.Base(devFsPath)
39+
sysFsTypePath := fmt.Sprintf("/sys/fs/%s/%s", fstype, devFsName)
40+
stat, err := os.Stat(sysFsTypePath)
41+
if err != nil {
42+
if os.IsNotExist(err) {
43+
// Path doesn't exist, indicating the device is NOT in use
44+
return false, nil
45+
}
46+
return false, err
47+
}
48+
49+
return stat.IsDir(), nil
50+
}

pkg/deviceutils/device-utils_windows.go

+8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ limitations under the License.
1616

1717
package deviceutils
1818

19+
import "k8s.io/mount-utils"
20+
1921
func (_ *deviceUtils) DisableDevice(devicePath string) error {
2022
// No disabling is necessary on windows.
2123
return nil
2224
}
25+
26+
func (_ *deviceUtils) IsDeviceFilesystemInUse(mounter *mount.SafeFormatAndMount, devicePath, devFsPath string) (bool, error) {
27+
// We don't support checking if a device filesystem is captured elsewhere by the system
28+
// Return false, to skip this check. Assume the filesystem is not in use.
29+
return false, nil
30+
}

pkg/deviceutils/fake-device-utils.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ limitations under the License.
1414

1515
package deviceutils
1616

17-
import "sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/resizefs"
17+
import (
18+
"k8s.io/mount-utils"
19+
"sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/resizefs"
20+
)
1821

1922
type fakeDeviceUtils struct {
2023
skipResize bool
@@ -50,3 +53,9 @@ func (du *fakeDeviceUtils) Resize(resizer resizefs.Resizefs, devicePath string,
5053
}
5154
return resizer.Resize(devicePath, deviceMountPath)
5255
}
56+
57+
func (_ *fakeDeviceUtils) IsDeviceFilesystemInUse(mounter *mount.SafeFormatAndMount, devicePath, devFsPath string) (bool, error) {
58+
// We don't support checking if a device filesystem is captured elsewhere by the system
59+
// Return false, to skip this check.
60+
return false, nil
61+
}

pkg/gce-pd-csi-driver/node.go

+34-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package gceGCEDriver
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"os"
2122
"path/filepath"
@@ -450,21 +451,46 @@ func (ns *GCENodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUns
450451
return nil, status.Error(codes.Internal, fmt.Sprintf("NodeUnstageVolume failed: %v\nUnmounting arguments: %s\n", err.Error(), stagingTargetPath))
451452
}
452453

453-
devicePath, err := getDevicePath(ns, volumeID, "" /* partition, which is unused */)
454-
if err != nil {
455-
klog.Errorf("Failed to find device path for volume %s. Device may not be detached cleanly (error is ignored and unstaging is continuing): %v", volumeID, err.Error())
456-
} else {
457-
if devFsPath, err := filepath.EvalSymlinks(devicePath); err != nil {
458-
klog.Warningf("filepath.EvalSymlinks(%q) failed when trying to disable device: %v (ignored, unstaging continues)", devicePath, err)
459-
} else if err := ns.DeviceUtils.DisableDevice(devFsPath); err != nil {
460-
klog.Warningf("Failed to disabled device %s (aka %s) for volume %s. Device may not be detached cleanly (ignored, unstaging continues): %v", devicePath, devFsPath, volumeID, err)
454+
if err := ns.safelyDisableDevice(volumeID); err != nil {
455+
var targetErr *ignoreableError
456+
if errors.As(err, &targetErr) {
457+
klog.Warningf("Device may not be detached cleanly for volume %s (ignored, unstaging continues): %v", volumeID, err)
458+
} else {
459+
return nil, status.Errorf(codes.Internal, "NodeUnstageVolume for volume %s failed: %v", volumeID, err)
461460
}
462461
}
463462

464463
klog.V(4).Infof("NodeUnstageVolume succeeded on %v from %s", volumeID, stagingTargetPath)
465464
return &csi.NodeUnstageVolumeResponse{}, nil
466465
}
467466

467+
// A private error wrapper used to pass control flow decisions up to the caller
468+
type ignoreableError struct{ error }
469+
470+
func (ns *GCENodeServer) safelyDisableDevice(volumeID string) error {
471+
devicePath, err := getDevicePath(ns, volumeID, "" /* partition, which is unused */)
472+
if err != nil {
473+
return &ignoreableError{fmt.Errorf("failed to find device path for volume %s: %v", volumeID, err.Error())}
474+
}
475+
476+
devFsPath, err := filepath.EvalSymlinks(devicePath)
477+
if err != nil {
478+
return &ignoreableError{fmt.Errorf("filepath.EvalSymlinks(%q) failed: %v", devicePath, err)}
479+
}
480+
481+
if inUse, err := ns.DeviceUtils.IsDeviceFilesystemInUse(ns.Mounter, devicePath, devFsPath); err != nil {
482+
return &ignoreableError{fmt.Errorf("failed to check if device %s (aka %s) is in use: %v", devicePath, devFsPath, err)}
483+
} else if inUse {
484+
return fmt.Errorf("device %s (aka %s) is still in use", devicePath, devFsPath)
485+
}
486+
487+
if err := ns.DeviceUtils.DisableDevice(devFsPath); err != nil {
488+
return &ignoreableError{fmt.Errorf("failed to disable device %s (aka %s): %v", devicePath, devFsPath, err)}
489+
}
490+
491+
return nil
492+
}
493+
468494
func (ns *GCENodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
469495
return &csi.NodeGetCapabilitiesResponse{
470496
Capabilities: ns.Driver.nscap,

test/e2e/tests/single_zone_e2e_test.go

+76
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,82 @@ var _ = Describe("GCE PD CSI Driver", func() {
14011401
}()
14021402
})
14031403

1404+
It("Should block unstage if filesystem mounted", func() {
1405+
testContext := getRandomTestContext()
1406+
1407+
p, z, _ := testContext.Instance.GetIdentity()
1408+
client := testContext.Client
1409+
instance := testContext.Instance
1410+
1411+
// Create Disk
1412+
volName, volID := createAndValidateUniqueZonalDisk(client, p, z, standardDiskType)
1413+
1414+
defer func() {
1415+
// Delete Disk
1416+
err := client.DeleteVolume(volID)
1417+
Expect(err).To(BeNil(), "DeleteVolume failed")
1418+
1419+
// Validate Disk Deleted
1420+
_, err = computeService.Disks.Get(p, z, volName).Do()
1421+
Expect(gce.IsGCEError(err, "notFound")).To(BeTrue(), "Expected disk to not be found")
1422+
}()
1423+
1424+
// Attach Disk
1425+
err := client.ControllerPublishVolumeReadWrite(volID, instance.GetNodeID(), false /* forceAttach */)
1426+
Expect(err).To(BeNil(), "ControllerPublishVolume failed with error for disk %v on node %v: %v", volID, instance.GetNodeID(), err)
1427+
1428+
defer func() {
1429+
// Detach Disk
1430+
err = client.ControllerUnpublishVolume(volID, instance.GetNodeID())
1431+
if err != nil {
1432+
klog.Errorf("Failed to detach disk: %v", err)
1433+
}
1434+
}()
1435+
1436+
// Stage Disk
1437+
stageDir := filepath.Join("/tmp/", volName, "stage")
1438+
err = client.NodeStageExt4Volume(volID, stageDir)
1439+
Expect(err).To(BeNil(), "failed to stage volume: %v", err)
1440+
1441+
// Create private bind mount
1442+
boundMountStageDir := filepath.Join("/tmp/bindmount", volName, "bindmount")
1443+
boundMountStageMkdirOutput, err := instance.SSH("mkdir", "-p", boundMountStageDir)
1444+
Expect(err).To(BeNil(), "mkdir failed on instance %v: output: %v: %v", instance.GetNodeID(), boundMountStageMkdirOutput, err)
1445+
bindMountOutput, err := instance.SSH("mount", "--rbind", "--make-private", stageDir, boundMountStageDir)
1446+
Expect(err).To(BeNil(), "Bind mount failed on instance %v: output: %v: %v", instance.GetNodeID(), bindMountOutput, err)
1447+
1448+
privateBindMountRemoved := false
1449+
unmountAndRmPrivateBindMount := func() {
1450+
if !privateBindMountRemoved {
1451+
// Umount and delete private mount staging directory
1452+
bindUmountOutput, err := instance.SSH("umount", boundMountStageDir)
1453+
Expect(err).To(BeNil(), "Bind mount failed on instance %v: output: %v: %v", instance.GetNodeID(), bindUmountOutput, err)
1454+
err = testutils.RmAll(instance, boundMountStageDir)
1455+
Expect(err).To(BeNil(), "Failed to rm mount stage dir %s: %v", boundMountStageDir, err)
1456+
}
1457+
privateBindMountRemoved = true
1458+
}
1459+
1460+
defer func() {
1461+
unmountAndRmPrivateBindMount()
1462+
}()
1463+
1464+
// Unstage Disk
1465+
err = client.NodeUnstageVolume(volID, stageDir)
1466+
Expect(err).ToNot(BeNil(), "Expected failure during unstage")
1467+
Expect(err).To(MatchError(ContainSubstring(("is still in use"))))
1468+
1469+
// Unmount private bind mount and try again
1470+
unmountAndRmPrivateBindMount()
1471+
1472+
// Unstage Disk
1473+
err = client.NodeUnstageVolume(volID, stageDir)
1474+
Expect(err).To(BeNil(), "Failed to unstage volume: %v", err)
1475+
fp := filepath.Join("/tmp/", volName)
1476+
err = testutils.RmAll(instance, fp)
1477+
Expect(err).To(BeNil(), "Failed to rm file path %s: %v", fp, err)
1478+
})
1479+
14041480
type multiZoneTestConfig struct {
14051481
diskType string
14061482
readOnly bool

0 commit comments

Comments
 (0)