Skip to content

Commit 2781134

Browse files
authored
Merge pull request #1845 from pwschuurman/automated-cherry-pick-of-#1658-upstream-release-1.12
Automated cherry pick of #1658: Add support for checking if a device is being used by a filesystem
2 parents eaf27e4 + fa5ac2b commit 2781134

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"
@@ -392,21 +393,46 @@ func (ns *GCENodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUns
392393
return nil, status.Error(codes.Internal, fmt.Sprintf("NodeUnstageVolume failed: %v\nUnmounting arguments: %s\n", err.Error(), stagingTargetPath))
393394
}
394395

395-
devicePath, err := getDevicePath(ns, volumeID, "" /* partition, which is unused */)
396-
if err != nil {
397-
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())
398-
} else {
399-
if devFsPath, err := filepath.EvalSymlinks(devicePath); err != nil {
400-
klog.Warningf("filepath.EvalSymlinks(%q) failed when trying to disable device: %w (ignored, unstaging continues)", devicePath, err)
401-
} else if err := ns.DeviceUtils.DisableDevice(devFsPath); err != nil {
402-
klog.Warningf("Failed to disabled device %s (aka %s) for volume %s. Device may not be detached cleanly (ignored, unstaging continues): %w", devicePath, devFsPath, volumeID, err)
396+
if err := ns.safelyDisableDevice(volumeID); err != nil {
397+
var targetErr *ignoreableError
398+
if errors.As(err, &targetErr) {
399+
klog.Warningf("Device may not be detached cleanly for volume %s (ignored, unstaging continues): %v", volumeID, err)
400+
} else {
401+
return nil, status.Errorf(codes.Internal, "NodeUnstageVolume for volume %s failed: %v", volumeID, err)
403402
}
404403
}
405404

406405
klog.V(4).Infof("NodeUnstageVolume succeeded on %v from %s", volumeID, stagingTargetPath)
407406
return &csi.NodeUnstageVolumeResponse{}, nil
408407
}
409408

409+
// A private error wrapper used to pass control flow decisions up to the caller
410+
type ignoreableError struct{ error }
411+
412+
func (ns *GCENodeServer) safelyDisableDevice(volumeID string) error {
413+
devicePath, err := getDevicePath(ns, volumeID, "" /* partition, which is unused */)
414+
if err != nil {
415+
return &ignoreableError{fmt.Errorf("failed to find device path for volume %s: %v", volumeID, err.Error())}
416+
}
417+
418+
devFsPath, err := filepath.EvalSymlinks(devicePath)
419+
if err != nil {
420+
return &ignoreableError{fmt.Errorf("filepath.EvalSymlinks(%q) failed: %v", devicePath, err)}
421+
}
422+
423+
if inUse, err := ns.DeviceUtils.IsDeviceFilesystemInUse(ns.Mounter, devicePath, devFsPath); err != nil {
424+
return &ignoreableError{fmt.Errorf("failed to check if device %s (aka %s) is in use: %v", devicePath, devFsPath, err)}
425+
} else if inUse {
426+
return fmt.Errorf("device %s (aka %s) is still in use", devicePath, devFsPath)
427+
}
428+
429+
if err := ns.DeviceUtils.DisableDevice(devFsPath); err != nil {
430+
return &ignoreableError{fmt.Errorf("failed to disable device %s (aka %s): %v", devicePath, devFsPath, err)}
431+
}
432+
433+
return nil
434+
}
435+
410436
func (ns *GCENodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
411437
return &csi.NodeGetCapabilitiesResponse{
412438
Capabilities: ns.Driver.nscap,

test/e2e/tests/single_zone_e2e_test.go

+76
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,82 @@ var _ = Describe("GCE PD CSI Driver", func() {
13411341

13421342
Expect(err).To(BeNil(), "no error expected when passed valid compute url")
13431343
})
1344+
1345+
It("Should block unstage if filesystem mounted", func() {
1346+
testContext := getRandomTestContext()
1347+
1348+
p, z, _ := testContext.Instance.GetIdentity()
1349+
client := testContext.Client
1350+
instance := testContext.Instance
1351+
1352+
// Create Disk
1353+
volName, volID := createAndValidateUniqueZonalDisk(client, p, z, standardDiskType)
1354+
1355+
defer func() {
1356+
// Delete Disk
1357+
err := client.DeleteVolume(volID)
1358+
Expect(err).To(BeNil(), "DeleteVolume failed")
1359+
1360+
// Validate Disk Deleted
1361+
_, err = computeService.Disks.Get(p, z, volName).Do()
1362+
Expect(gce.IsGCEError(err, "notFound")).To(BeTrue(), "Expected disk to not be found")
1363+
}()
1364+
1365+
// Attach Disk
1366+
err := client.ControllerPublishVolume(volID, instance.GetNodeID(), false /* forceAttach */)
1367+
Expect(err).To(BeNil(), "ControllerPublishVolume failed with error for disk %v on node %v: %v", volID, instance.GetNodeID(), err)
1368+
1369+
defer func() {
1370+
// Detach Disk
1371+
err = client.ControllerUnpublishVolume(volID, instance.GetNodeID())
1372+
if err != nil {
1373+
klog.Errorf("Failed to detach disk: %v", err)
1374+
}
1375+
}()
1376+
1377+
// Stage Disk
1378+
stageDir := filepath.Join("/tmp/", volName, "stage")
1379+
err = client.NodeStageExt4Volume(volID, stageDir)
1380+
Expect(err).To(BeNil(), "failed to stage volume: %v", err)
1381+
1382+
// Create private bind mount
1383+
boundMountStageDir := filepath.Join("/tmp/bindmount", volName, "bindmount")
1384+
boundMountStageMkdirOutput, err := instance.SSH("mkdir", "-p", boundMountStageDir)
1385+
Expect(err).To(BeNil(), "mkdir failed on instance %v: output: %v: %v", instance.GetNodeID(), boundMountStageMkdirOutput, err)
1386+
bindMountOutput, err := instance.SSH("mount", "--rbind", "--make-private", stageDir, boundMountStageDir)
1387+
Expect(err).To(BeNil(), "Bind mount failed on instance %v: output: %v: %v", instance.GetNodeID(), bindMountOutput, err)
1388+
1389+
privateBindMountRemoved := false
1390+
unmountAndRmPrivateBindMount := func() {
1391+
if !privateBindMountRemoved {
1392+
// Umount and delete private mount staging directory
1393+
bindUmountOutput, err := instance.SSH("umount", boundMountStageDir)
1394+
Expect(err).To(BeNil(), "Bind mount failed on instance %v: output: %v: %v", instance.GetNodeID(), bindUmountOutput, err)
1395+
err = testutils.RmAll(instance, boundMountStageDir)
1396+
Expect(err).To(BeNil(), "Failed to rm mount stage dir %s: %v", boundMountStageDir, err)
1397+
}
1398+
privateBindMountRemoved = true
1399+
}
1400+
1401+
defer func() {
1402+
unmountAndRmPrivateBindMount()
1403+
}()
1404+
1405+
// Unstage Disk
1406+
err = client.NodeUnstageVolume(volID, stageDir)
1407+
Expect(err).ToNot(BeNil(), "Expected failure during unstage")
1408+
Expect(err).To(MatchError(ContainSubstring(("is still in use"))))
1409+
1410+
// Unmount private bind mount and try again
1411+
unmountAndRmPrivateBindMount()
1412+
1413+
// Unstage Disk
1414+
err = client.NodeUnstageVolume(volID, stageDir)
1415+
Expect(err).To(BeNil(), "Failed to unstage volume: %v", err)
1416+
fp := filepath.Join("/tmp/", volName)
1417+
err = testutils.RmAll(instance, fp)
1418+
Expect(err).To(BeNil(), "Failed to rm file path %s: %v", fp, err)
1419+
})
13441420
})
13451421

13461422
func equalWithinEpsilon(a, b, epsiolon int64) bool {

0 commit comments

Comments
 (0)